ts-procedures 5.14.0 → 5.16.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 (277) hide show
  1. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -1
  2. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +59 -4
  3. package/agent_config/claude-code/skills/ts-procedures/patterns.md +103 -3
  4. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +36 -5
  5. package/agent_config/copilot/copilot-instructions.md +56 -7
  6. package/agent_config/cursor/cursorrules +56 -7
  7. package/build/client/call.d.ts +18 -9
  8. package/build/client/call.js +25 -19
  9. package/build/client/call.js.map +1 -1
  10. package/build/client/call.test.js +167 -17
  11. package/build/client/call.test.js.map +1 -1
  12. package/build/client/index.d.ts +1 -1
  13. package/build/client/index.js +18 -3
  14. package/build/client/index.js.map +1 -1
  15. package/build/client/index.test.js +104 -0
  16. package/build/client/index.test.js.map +1 -1
  17. package/build/client/resolve-options.d.ts +45 -0
  18. package/build/client/resolve-options.js +82 -0
  19. package/build/client/resolve-options.js.map +1 -0
  20. package/build/client/resolve-options.test.js +158 -0
  21. package/build/client/resolve-options.test.js.map +1 -0
  22. package/build/client/stream.d.ts +18 -9
  23. package/build/client/stream.js +24 -19
  24. package/build/client/stream.js.map +1 -1
  25. package/build/client/stream.test.js +102 -46
  26. package/build/client/stream.test.js.map +1 -1
  27. package/build/client/types.d.ts +68 -1
  28. package/build/client/types.js +1 -1
  29. package/build/codegen/bin/cli.d.ts +2 -0
  30. package/build/codegen/bin/cli.js +11 -0
  31. package/build/codegen/bin/cli.js.map +1 -1
  32. package/build/codegen/bin/cli.test.js +30 -0
  33. package/build/codegen/bin/cli.test.js.map +1 -1
  34. package/build/codegen/e2e.test.js +141 -0
  35. package/build/codegen/e2e.test.js.map +1 -1
  36. package/build/codegen/emit-client-runtime.js +3 -0
  37. package/build/codegen/emit-client-runtime.js.map +1 -1
  38. package/build/codegen/index.d.ts +1 -0
  39. package/build/codegen/index.js +1 -0
  40. package/build/codegen/index.js.map +1 -1
  41. package/build/codegen/pipeline.d.ts +1 -0
  42. package/build/codegen/pipeline.js +8 -2
  43. package/build/codegen/pipeline.js.map +1 -1
  44. package/build/codegen/pipeline.test.js +34 -1
  45. package/build/codegen/pipeline.test.js.map +1 -1
  46. package/docs/client-and-codegen.md +125 -2
  47. package/package.json +1 -1
  48. package/src/client/call.test.ts +202 -29
  49. package/src/client/call.ts +41 -28
  50. package/src/client/index.test.ts +117 -0
  51. package/src/client/index.ts +25 -8
  52. package/src/client/resolve-options.test.ts +205 -0
  53. package/src/client/resolve-options.ts +113 -0
  54. package/src/client/stream.test.ts +132 -107
  55. package/src/client/stream.ts +40 -25
  56. package/src/client/types.ts +74 -2
  57. package/src/codegen/bin/cli.test.ts +35 -0
  58. package/src/codegen/bin/cli.ts +11 -0
  59. package/src/codegen/e2e.test.ts +151 -0
  60. package/src/codegen/emit-client-runtime.ts +3 -0
  61. package/src/codegen/index.ts +2 -0
  62. package/src/codegen/pipeline.test.ts +46 -1
  63. package/src/codegen/pipeline.ts +9 -2
  64. package/src/implementations/http/README.md +11 -1
  65. package/build/src/client/call.d.ts +0 -14
  66. package/build/src/client/call.js +0 -47
  67. package/build/src/client/call.js.map +0 -1
  68. package/build/src/client/call.test.js +0 -124
  69. package/build/src/client/call.test.js.map +0 -1
  70. package/build/src/client/errors.d.ts +0 -25
  71. package/build/src/client/errors.js +0 -33
  72. package/build/src/client/errors.js.map +0 -1
  73. package/build/src/client/errors.test.d.ts +0 -1
  74. package/build/src/client/errors.test.js +0 -41
  75. package/build/src/client/errors.test.js.map +0 -1
  76. package/build/src/client/fetch-adapter.d.ts +0 -12
  77. package/build/src/client/fetch-adapter.js +0 -156
  78. package/build/src/client/fetch-adapter.js.map +0 -1
  79. package/build/src/client/fetch-adapter.test.d.ts +0 -1
  80. package/build/src/client/fetch-adapter.test.js +0 -271
  81. package/build/src/client/fetch-adapter.test.js.map +0 -1
  82. package/build/src/client/hooks.d.ts +0 -17
  83. package/build/src/client/hooks.js +0 -40
  84. package/build/src/client/hooks.js.map +0 -1
  85. package/build/src/client/hooks.test.d.ts +0 -1
  86. package/build/src/client/hooks.test.js +0 -163
  87. package/build/src/client/hooks.test.js.map +0 -1
  88. package/build/src/client/index.d.ts +0 -22
  89. package/build/src/client/index.js +0 -67
  90. package/build/src/client/index.js.map +0 -1
  91. package/build/src/client/index.test.d.ts +0 -1
  92. package/build/src/client/index.test.js +0 -231
  93. package/build/src/client/index.test.js.map +0 -1
  94. package/build/src/client/request-builder.d.ts +0 -13
  95. package/build/src/client/request-builder.js +0 -53
  96. package/build/src/client/request-builder.js.map +0 -1
  97. package/build/src/client/request-builder.test.d.ts +0 -1
  98. package/build/src/client/request-builder.test.js +0 -160
  99. package/build/src/client/request-builder.test.js.map +0 -1
  100. package/build/src/client/stream.d.ts +0 -27
  101. package/build/src/client/stream.js +0 -118
  102. package/build/src/client/stream.js.map +0 -1
  103. package/build/src/client/stream.test.d.ts +0 -1
  104. package/build/src/client/stream.test.js +0 -228
  105. package/build/src/client/stream.test.js.map +0 -1
  106. package/build/src/client/types.d.ts +0 -78
  107. package/build/src/client/types.js +0 -3
  108. package/build/src/client/types.js.map +0 -1
  109. package/build/src/codegen/bin/cli.d.ts +0 -45
  110. package/build/src/codegen/bin/cli.js +0 -246
  111. package/build/src/codegen/bin/cli.js.map +0 -1
  112. package/build/src/codegen/bin/cli.test.d.ts +0 -1
  113. package/build/src/codegen/bin/cli.test.js +0 -220
  114. package/build/src/codegen/bin/cli.test.js.map +0 -1
  115. package/build/src/codegen/constants.d.ts +0 -1
  116. package/build/src/codegen/constants.js +0 -2
  117. package/build/src/codegen/constants.js.map +0 -1
  118. package/build/src/codegen/e2e.test.d.ts +0 -1
  119. package/build/src/codegen/e2e.test.js +0 -464
  120. package/build/src/codegen/e2e.test.js.map +0 -1
  121. package/build/src/codegen/emit-client-runtime.d.ts +0 -9
  122. package/build/src/codegen/emit-client-runtime.js +0 -99
  123. package/build/src/codegen/emit-client-runtime.js.map +0 -1
  124. package/build/src/codegen/emit-client-runtime.test.d.ts +0 -1
  125. package/build/src/codegen/emit-client-runtime.test.js +0 -78
  126. package/build/src/codegen/emit-client-runtime.test.js.map +0 -1
  127. package/build/src/codegen/emit-client-types.d.ts +0 -8
  128. package/build/src/codegen/emit-client-types.js +0 -25
  129. package/build/src/codegen/emit-client-types.js.map +0 -1
  130. package/build/src/codegen/emit-client-types.test.d.ts +0 -1
  131. package/build/src/codegen/emit-client-types.test.js +0 -33
  132. package/build/src/codegen/emit-client-types.test.js.map +0 -1
  133. package/build/src/codegen/emit-errors.d.ts +0 -19
  134. package/build/src/codegen/emit-errors.js +0 -59
  135. package/build/src/codegen/emit-errors.js.map +0 -1
  136. package/build/src/codegen/emit-errors.test.d.ts +0 -1
  137. package/build/src/codegen/emit-errors.test.js +0 -175
  138. package/build/src/codegen/emit-errors.test.js.map +0 -1
  139. package/build/src/codegen/emit-index.d.ts +0 -12
  140. package/build/src/codegen/emit-index.js +0 -41
  141. package/build/src/codegen/emit-index.js.map +0 -1
  142. package/build/src/codegen/emit-index.test.d.ts +0 -1
  143. package/build/src/codegen/emit-index.test.js +0 -106
  144. package/build/src/codegen/emit-index.test.js.map +0 -1
  145. package/build/src/codegen/emit-scope.d.ts +0 -15
  146. package/build/src/codegen/emit-scope.js +0 -299
  147. package/build/src/codegen/emit-scope.js.map +0 -1
  148. package/build/src/codegen/emit-scope.test.d.ts +0 -1
  149. package/build/src/codegen/emit-scope.test.js +0 -559
  150. package/build/src/codegen/emit-scope.test.js.map +0 -1
  151. package/build/src/codegen/emit-types.d.ts +0 -43
  152. package/build/src/codegen/emit-types.js +0 -111
  153. package/build/src/codegen/emit-types.js.map +0 -1
  154. package/build/src/codegen/emit-types.test.d.ts +0 -1
  155. package/build/src/codegen/emit-types.test.js +0 -184
  156. package/build/src/codegen/emit-types.test.js.map +0 -1
  157. package/build/src/codegen/group-routes.d.ts +0 -23
  158. package/build/src/codegen/group-routes.js +0 -46
  159. package/build/src/codegen/group-routes.js.map +0 -1
  160. package/build/src/codegen/group-routes.test.d.ts +0 -1
  161. package/build/src/codegen/group-routes.test.js +0 -131
  162. package/build/src/codegen/group-routes.test.js.map +0 -1
  163. package/build/src/codegen/index.d.ts +0 -15
  164. package/build/src/codegen/index.js +0 -16
  165. package/build/src/codegen/index.js.map +0 -1
  166. package/build/src/codegen/naming.d.ts +0 -7
  167. package/build/src/codegen/naming.js +0 -21
  168. package/build/src/codegen/naming.js.map +0 -1
  169. package/build/src/codegen/naming.test.d.ts +0 -1
  170. package/build/src/codegen/naming.test.js +0 -40
  171. package/build/src/codegen/naming.test.js.map +0 -1
  172. package/build/src/codegen/pipeline.d.ts +0 -17
  173. package/build/src/codegen/pipeline.js +0 -78
  174. package/build/src/codegen/pipeline.js.map +0 -1
  175. package/build/src/codegen/pipeline.test.d.ts +0 -1
  176. package/build/src/codegen/pipeline.test.js +0 -269
  177. package/build/src/codegen/pipeline.test.js.map +0 -1
  178. package/build/src/codegen/resolve-envelope.d.ts +0 -7
  179. package/build/src/codegen/resolve-envelope.js +0 -46
  180. package/build/src/codegen/resolve-envelope.js.map +0 -1
  181. package/build/src/codegen/resolve-envelope.test.d.ts +0 -1
  182. package/build/src/codegen/resolve-envelope.test.js +0 -69
  183. package/build/src/codegen/resolve-envelope.test.js.map +0 -1
  184. package/build/src/errors.d.ts +0 -33
  185. package/build/src/errors.js +0 -91
  186. package/build/src/errors.js.map +0 -1
  187. package/build/src/errors.test.d.ts +0 -1
  188. package/build/src/errors.test.js +0 -122
  189. package/build/src/errors.test.js.map +0 -1
  190. package/build/src/exports.d.ts +0 -7
  191. package/build/src/exports.js +0 -8
  192. package/build/src/exports.js.map +0 -1
  193. package/build/src/implementations/http/doc-registry.d.ts +0 -12
  194. package/build/src/implementations/http/doc-registry.js +0 -114
  195. package/build/src/implementations/http/doc-registry.js.map +0 -1
  196. package/build/src/implementations/http/doc-registry.test.d.ts +0 -1
  197. package/build/src/implementations/http/doc-registry.test.js +0 -347
  198. package/build/src/implementations/http/doc-registry.test.js.map +0 -1
  199. package/build/src/implementations/http/express-rpc/index.d.ts +0 -94
  200. package/build/src/implementations/http/express-rpc/index.js +0 -185
  201. package/build/src/implementations/http/express-rpc/index.js.map +0 -1
  202. package/build/src/implementations/http/express-rpc/index.test.d.ts +0 -1
  203. package/build/src/implementations/http/express-rpc/index.test.js +0 -684
  204. package/build/src/implementations/http/express-rpc/index.test.js.map +0 -1
  205. package/build/src/implementations/http/express-rpc/types.d.ts +0 -11
  206. package/build/src/implementations/http/express-rpc/types.js +0 -2
  207. package/build/src/implementations/http/express-rpc/types.js.map +0 -1
  208. package/build/src/implementations/http/hono-api/index.d.ts +0 -102
  209. package/build/src/implementations/http/hono-api/index.js +0 -341
  210. package/build/src/implementations/http/hono-api/index.js.map +0 -1
  211. package/build/src/implementations/http/hono-api/index.test.d.ts +0 -1
  212. package/build/src/implementations/http/hono-api/index.test.js +0 -992
  213. package/build/src/implementations/http/hono-api/index.test.js.map +0 -1
  214. package/build/src/implementations/http/hono-api/types.d.ts +0 -13
  215. package/build/src/implementations/http/hono-api/types.js +0 -2
  216. package/build/src/implementations/http/hono-api/types.js.map +0 -1
  217. package/build/src/implementations/http/hono-rpc/index.d.ts +0 -92
  218. package/build/src/implementations/http/hono-rpc/index.js +0 -161
  219. package/build/src/implementations/http/hono-rpc/index.js.map +0 -1
  220. package/build/src/implementations/http/hono-rpc/index.test.d.ts +0 -1
  221. package/build/src/implementations/http/hono-rpc/index.test.js +0 -803
  222. package/build/src/implementations/http/hono-rpc/index.test.js.map +0 -1
  223. package/build/src/implementations/http/hono-rpc/types.d.ts +0 -11
  224. package/build/src/implementations/http/hono-rpc/types.js +0 -2
  225. package/build/src/implementations/http/hono-rpc/types.js.map +0 -1
  226. package/build/src/implementations/http/hono-stream/index.d.ts +0 -120
  227. package/build/src/implementations/http/hono-stream/index.js +0 -309
  228. package/build/src/implementations/http/hono-stream/index.js.map +0 -1
  229. package/build/src/implementations/http/hono-stream/index.test.d.ts +0 -1
  230. package/build/src/implementations/http/hono-stream/index.test.js +0 -1356
  231. package/build/src/implementations/http/hono-stream/index.test.js.map +0 -1
  232. package/build/src/implementations/http/hono-stream/types.d.ts +0 -15
  233. package/build/src/implementations/http/hono-stream/types.js +0 -2
  234. package/build/src/implementations/http/hono-stream/types.js.map +0 -1
  235. package/build/src/implementations/types.d.ts +0 -142
  236. package/build/src/implementations/types.js +0 -2
  237. package/build/src/implementations/types.js.map +0 -1
  238. package/build/src/index.d.ts +0 -165
  239. package/build/src/index.js +0 -253
  240. package/build/src/index.js.map +0 -1
  241. package/build/src/index.test.d.ts +0 -1
  242. package/build/src/index.test.js +0 -890
  243. package/build/src/index.test.js.map +0 -1
  244. package/build/src/schema/compute-schema.d.ts +0 -35
  245. package/build/src/schema/compute-schema.js +0 -41
  246. package/build/src/schema/compute-schema.js.map +0 -1
  247. package/build/src/schema/compute-schema.test.d.ts +0 -1
  248. package/build/src/schema/compute-schema.test.js +0 -107
  249. package/build/src/schema/compute-schema.test.js.map +0 -1
  250. package/build/src/schema/extract-json-schema.d.ts +0 -2
  251. package/build/src/schema/extract-json-schema.js +0 -12
  252. package/build/src/schema/extract-json-schema.js.map +0 -1
  253. package/build/src/schema/extract-json-schema.test.d.ts +0 -1
  254. package/build/src/schema/extract-json-schema.test.js +0 -23
  255. package/build/src/schema/extract-json-schema.test.js.map +0 -1
  256. package/build/src/schema/parser.d.ts +0 -28
  257. package/build/src/schema/parser.js +0 -170
  258. package/build/src/schema/parser.js.map +0 -1
  259. package/build/src/schema/parser.test.d.ts +0 -1
  260. package/build/src/schema/parser.test.js +0 -120
  261. package/build/src/schema/parser.test.js.map +0 -1
  262. package/build/src/schema/resolve-schema-lib.d.ts +0 -12
  263. package/build/src/schema/resolve-schema-lib.js +0 -11
  264. package/build/src/schema/resolve-schema-lib.js.map +0 -1
  265. package/build/src/schema/resolve-schema-lib.test.d.ts +0 -1
  266. package/build/src/schema/resolve-schema-lib.test.js +0 -17
  267. package/build/src/schema/resolve-schema-lib.test.js.map +0 -1
  268. package/build/src/schema/types.d.ts +0 -8
  269. package/build/src/schema/types.js +0 -2
  270. package/build/src/schema/types.js.map +0 -1
  271. package/build/src/stack-utils.d.ts +0 -25
  272. package/build/src/stack-utils.js +0 -95
  273. package/build/src/stack-utils.js.map +0 -1
  274. package/build/src/stack-utils.test.d.ts +0 -1
  275. package/build/src/stack-utils.test.js +0 -80
  276. package/build/src/stack-utils.test.js.map +0 -1
  277. /package/build/{src/client/call.test.d.ts → client/resolve-options.test.d.ts} +0 -0
@@ -7,6 +7,8 @@ import type {
7
7
  AdapterStreamResponse,
8
8
  ClientHooks,
9
9
  StreamDescriptor,
10
+ ProcedureCallDefaults,
11
+ ProcedureCallOptions,
10
12
  } from './types.js'
11
13
 
12
14
  // ── helpers ───────────────────────────────────────────────
@@ -47,6 +49,26 @@ function makeStreamAdapter(
47
49
  }
48
50
  }
49
51
 
52
+ interface RunConfig {
53
+ adapter: ClientAdapter
54
+ hooks?: ClientHooks
55
+ defaults?: ProcedureCallDefaults
56
+ options?: ProcedureCallOptions
57
+ descriptor?: StreamDescriptor
58
+ basePath?: string
59
+ }
60
+
61
+ function run<TYield, TReturn = void>({
62
+ adapter,
63
+ hooks = {},
64
+ defaults,
65
+ options,
66
+ descriptor = makeDescriptor(),
67
+ basePath = 'https://api.example.com',
68
+ }: RunConfig) {
69
+ return executeStream<TYield, TReturn>({ descriptor, basePath, adapter, hooks, defaults, options })
70
+ }
71
+
50
72
  // ── createTypedStream — SSE mode ──────────────────────────
51
73
 
52
74
  describe('createTypedStream — SSE mode', () => {
@@ -55,15 +77,10 @@ describe('createTypedStream — SSE mode', () => {
55
77
  { data: { count: 1 }, event: 'update' },
56
78
  { data: { count: 2 }, event: 'update' },
57
79
  ]
58
- const stream = createTypedStream<{ count: number }, void>(
59
- makeAsyncIterable(sseItems),
60
- 'sse'
61
- )
80
+ const stream = createTypedStream<{ count: number }, void>(makeAsyncIterable(sseItems), 'sse')
62
81
 
63
82
  const received: { count: number }[] = []
64
- for await (const item of stream) {
65
- received.push(item)
66
- }
83
+ for await (const item of stream) received.push(item)
67
84
  expect(received).toEqual([{ count: 1 }, { count: 2 }])
68
85
  })
69
86
 
@@ -74,34 +91,23 @@ describe('createTypedStream — SSE mode', () => {
74
91
  ]
75
92
  const stream = createTypedStream<{ count: number }, { total: number }>(
76
93
  makeAsyncIterable(sseItems),
77
- 'sse'
94
+ 'sse',
78
95
  )
79
96
 
80
97
  const yielded: { count: number }[] = []
81
- for await (const item of stream) {
82
- yielded.push(item)
83
- }
98
+ for await (const item of stream) yielded.push(item)
84
99
 
85
- // Only the 'update' event is yielded
86
100
  expect(yielded).toEqual([{ count: 1 }])
87
-
88
- // The 'return' event resolves .result
89
- const result = await stream.result
90
- expect(result).toEqual({ total: 99 })
101
+ await expect(stream.result).resolves.toEqual({ total: 99 })
91
102
  })
92
103
 
93
104
  it('resolves .result with undefined when no return event', async () => {
94
- const sseItems = [
95
- { data: 'hello', event: 'message' },
96
- ]
97
105
  const stream = createTypedStream<string, undefined>(
98
- makeAsyncIterable(sseItems),
99
- 'sse'
106
+ makeAsyncIterable([{ data: 'hello', event: 'message' }]),
107
+ 'sse',
100
108
  )
101
-
102
109
  for await (const _ of stream) { /* drain */ }
103
- const result = await stream.result
104
- expect(result).toBeUndefined()
110
+ await expect(stream.result).resolves.toBeUndefined()
105
111
  })
106
112
 
107
113
  it('rejects .result and re-throws on error', async () => {
@@ -111,30 +117,19 @@ describe('createTypedStream — SSE mode', () => {
111
117
  }
112
118
 
113
119
  const stream = createTypedStream<string, void>(errorIterable(), 'sse')
114
-
115
- // Consuming the stream should throw
116
120
  await expect(async () => {
117
121
  for await (const _ of stream) { /* drain */ }
118
122
  }).rejects.toThrow('stream broke')
119
-
120
- // .result should also reject
121
123
  await expect(stream.result).rejects.toThrow('stream broke')
122
124
  })
123
125
 
124
126
  it('handles SSE items without event field (defaults to yielding data)', async () => {
125
- const sseItems = [
126
- { data: 'a' },
127
- { data: 'b' },
128
- ]
129
127
  const stream = createTypedStream<string, void>(
130
- makeAsyncIterable(sseItems),
131
- 'sse'
128
+ makeAsyncIterable([{ data: 'a' }, { data: 'b' }]),
129
+ 'sse',
132
130
  )
133
-
134
131
  const yielded: string[] = []
135
- for await (const item of stream) {
136
- yielded.push(item)
137
- }
132
+ for await (const item of stream) yielded.push(item)
138
133
  expect(yielded).toEqual(['a', 'b'])
139
134
  })
140
135
  })
@@ -144,27 +139,16 @@ describe('createTypedStream — SSE mode', () => {
144
139
  describe('createTypedStream — text mode', () => {
145
140
  it('yields each chunk as-is', async () => {
146
141
  const chunks = ['chunk1', 'chunk2', 'chunk3']
147
- const stream = createTypedStream<string, void>(
148
- makeAsyncIterable(chunks),
149
- 'text'
150
- )
151
-
142
+ const stream = createTypedStream<string, void>(makeAsyncIterable(chunks), 'text')
152
143
  const received: string[] = []
153
- for await (const chunk of stream) {
154
- received.push(chunk)
155
- }
144
+ for await (const chunk of stream) received.push(chunk)
156
145
  expect(received).toEqual(chunks)
157
146
  })
158
147
 
159
148
  it('.result resolves to void on normal completion', async () => {
160
- const stream = createTypedStream<string, void>(
161
- makeAsyncIterable(['a', 'b']),
162
- 'text'
163
- )
164
-
149
+ const stream = createTypedStream<string, void>(makeAsyncIterable(['a', 'b']), 'text')
165
150
  for await (const _ of stream) { /* drain */ }
166
- const result = await stream.result
167
- expect(result).toBeUndefined()
151
+ await expect(stream.result).resolves.toBeUndefined()
168
152
  })
169
153
 
170
154
  it('rejects .result and re-throws on error', async () => {
@@ -172,13 +156,10 @@ describe('createTypedStream — text mode', () => {
172
156
  yield 'before error'
173
157
  throw new Error('text stream error')
174
158
  }
175
-
176
159
  const stream = createTypedStream<string, void>(errorIterable(), 'text')
177
-
178
160
  await expect(async () => {
179
161
  for await (const _ of stream) { /* drain */ }
180
162
  }).rejects.toThrow('text stream error')
181
-
182
163
  await expect(stream.result).rejects.toThrow('text stream error')
183
164
  })
184
165
  })
@@ -189,20 +170,11 @@ describe('executeStream', () => {
189
170
  it('calls adapter.stream and returns a TypedStream', async () => {
190
171
  const items = [{ data: 'hello', event: 'msg' }]
191
172
  const adapter = makeStreamAdapter({}, items)
192
-
193
- const stream = await executeStream<string, void>(
194
- makeDescriptor(),
195
- 'https://api.example.com',
196
- adapter,
197
- {},
198
- undefined
199
- )
173
+ const stream = await run<string, void>({ adapter })
200
174
 
201
175
  expect(adapter.stream).toHaveBeenCalledOnce()
202
176
  const received: string[] = []
203
- for await (const item of stream) {
204
- received.push(item)
205
- }
177
+ for await (const item of stream) received.push(item)
206
178
  expect(received).toEqual(['hello'])
207
179
  })
208
180
 
@@ -216,23 +188,15 @@ describe('executeStream', () => {
216
188
  }),
217
189
  }
218
190
 
219
- const globalHooks: ClientHooks = {
191
+ const hooks: ClientHooks = {
220
192
  onBeforeRequest: (ctx) => ({
221
193
  ...ctx,
222
194
  request: { ...ctx.request, headers: { 'x-stream-auth': 'stream-token' } },
223
195
  }),
224
196
  }
225
197
 
226
- const stream = await executeStream(
227
- makeDescriptor(),
228
- 'https://api.example.com',
229
- adapter,
230
- globalHooks,
231
- undefined
232
- )
233
- // Drain
234
- for await (const _ of stream) { /* noop */ }
235
-
198
+ const stream = await run({ adapter, hooks })
199
+ for await (const _ of stream) { /* drain */ }
236
200
  expect(capturedHeaders[0]?.['x-stream-auth']).toBe('stream-token')
237
201
  })
238
202
 
@@ -245,12 +209,11 @@ describe('executeStream', () => {
245
209
  return { status: 200, headers: {}, body: makeAsyncIterable([]) }
246
210
  }),
247
211
  }
248
- const globalHooks: ClientHooks = {
212
+ const hooks: ClientHooks = {
249
213
  onAfterResponse: () => { order.push('afterResponse') },
250
214
  }
251
215
 
252
- await executeStream(makeDescriptor(), 'https://api.example.com', adapter, globalHooks, undefined)
253
- // After executeStream returns (before iteration), afterResponse should have fired
216
+ await run({ adapter, hooks })
254
217
  expect(order).toEqual(['adapter', 'afterResponse'])
255
218
  })
256
219
 
@@ -263,10 +226,7 @@ describe('executeStream', () => {
263
226
  body: makeAsyncIterable([]),
264
227
  })),
265
228
  }
266
-
267
- await expect(
268
- executeStream(makeDescriptor(), 'https://api.example.com', adapter, {}, undefined)
269
- ).rejects.toThrow(ClientRequestError)
229
+ await expect(run({ adapter })).rejects.toThrow(ClientRequestError)
270
230
  })
271
231
 
272
232
  it('runs onError on adapter failure and re-throws', async () => {
@@ -276,13 +236,11 @@ describe('executeStream', () => {
276
236
  stream: vi.fn(async () => { throw adapterError }),
277
237
  }
278
238
  const receivedErrors: unknown[] = []
279
- const globalHooks: ClientHooks = {
239
+ const hooks: ClientHooks = {
280
240
  onError: (ctx) => { receivedErrors.push(ctx.error) },
281
241
  }
282
242
 
283
- await expect(
284
- executeStream(makeDescriptor(), 'https://api.example.com', adapter, globalHooks, undefined)
285
- ).rejects.toThrow('stream connection failed')
243
+ await expect(run({ adapter, hooks })).rejects.toThrow('stream connection failed')
286
244
  expect(receivedErrors[0]).toBe(adapterError)
287
245
  })
288
246
 
@@ -293,18 +251,13 @@ describe('executeStream', () => {
293
251
  ]
294
252
  const adapter = makeStreamAdapter({}, sseItems)
295
253
 
296
- const stream = await executeStream<{ n: number }, { final: boolean }>(
297
- makeDescriptor({ streamMode: 'sse' }),
298
- 'https://api.example.com',
254
+ const stream = await run<{ n: number }, { final: boolean }>({
299
255
  adapter,
300
- {},
301
- undefined
302
- )
256
+ descriptor: makeDescriptor({ streamMode: 'sse' }),
257
+ })
303
258
 
304
259
  const yielded: { n: number }[] = []
305
- for await (const item of stream) {
306
- yielded.push(item)
307
- }
260
+ for await (const item of stream) yielded.push(item)
308
261
  expect(yielded).toEqual([{ n: 1 }])
309
262
  await expect(stream.result).resolves.toEqual({ final: true })
310
263
  })
@@ -313,19 +266,91 @@ describe('executeStream', () => {
313
266
  const chunks = ['line1', 'line2']
314
267
  const adapter = makeStreamAdapter({}, chunks)
315
268
 
316
- const stream = await executeStream<string, void>(
317
- makeDescriptor({ streamMode: 'text' }),
318
- 'https://api.example.com',
269
+ const stream = await run<string, void>({
319
270
  adapter,
320
- {},
321
- undefined
322
- )
271
+ descriptor: makeDescriptor({ streamMode: 'text' }),
272
+ })
323
273
 
324
274
  const received: string[] = []
325
- for await (const chunk of stream) {
326
- received.push(chunk)
327
- }
275
+ for await (const chunk of stream) received.push(chunk)
328
276
  expect(received).toEqual(chunks)
329
277
  await expect(stream.result).resolves.toBeUndefined()
330
278
  })
279
+
280
+ // ── Per-call options ──
281
+
282
+ it('per-call timeout attaches a signal via AbortSignal.timeout', async () => {
283
+ const spy = vi.spyOn(AbortSignal, 'timeout')
284
+ try {
285
+ let observedSignal: AbortSignal | undefined
286
+ const adapter: ClientAdapter = {
287
+ request: vi.fn(async (): Promise<never> => { throw new Error('not expected') }),
288
+ stream: vi.fn(async (req: AdapterRequest): Promise<AdapterStreamResponse> => {
289
+ observedSignal = req.signal
290
+ return { status: 200, headers: {}, body: makeAsyncIterable([]) }
291
+ }),
292
+ }
293
+
294
+ await run({ adapter, options: { timeout: 5000 } })
295
+ expect(spy).toHaveBeenCalledWith(5000)
296
+ expect(observedSignal).toBeDefined()
297
+ } finally {
298
+ spy.mockRestore()
299
+ }
300
+ })
301
+
302
+ it('adapter receives a signal that reflects abort when the caller cancels', async () => {
303
+ const controller = new AbortController()
304
+ const adapter: ClientAdapter = {
305
+ request: vi.fn(async (): Promise<never> => { throw new Error('not expected') }),
306
+ stream: vi.fn(async (req: AdapterRequest): Promise<AdapterStreamResponse> => {
307
+ return new Promise((_resolve, reject) => {
308
+ const abort = () => reject(new Error('aborted'))
309
+ if (req.signal?.aborted) abort()
310
+ else req.signal?.addEventListener('abort', abort, { once: true })
311
+ })
312
+ }),
313
+ }
314
+
315
+ const promise = run({ adapter, options: { signal: controller.signal } })
316
+ // Let executeStream reach the adapter before aborting
317
+ await Promise.resolve()
318
+ controller.abort()
319
+ await expect(promise).rejects.toThrow('aborted')
320
+ })
321
+
322
+ it('per-call basePath overrides base path for streams', async () => {
323
+ const capturedUrls: string[] = []
324
+ const adapter: ClientAdapter = {
325
+ request: vi.fn(async (): Promise<never> => { throw new Error('not expected') }),
326
+ stream: vi.fn(async (req: AdapterRequest): Promise<AdapterStreamResponse> => {
327
+ capturedUrls.push(req.url)
328
+ return { status: 200, headers: {}, body: makeAsyncIterable([]) }
329
+ }),
330
+ }
331
+
332
+ const stream = await run({
333
+ adapter,
334
+ descriptor: makeDescriptor({ path: '/tail' }),
335
+ basePath: 'https://default.example.com',
336
+ options: { basePath: 'https://override.example.com' },
337
+ })
338
+ for await (const _ of stream) { /* drain */ }
339
+
340
+ expect(capturedUrls[0]).toBe('https://override.example.com/tail')
341
+ })
342
+
343
+ it('per-call meta is forwarded to the adapter', async () => {
344
+ let observedMeta: unknown
345
+ const adapter: ClientAdapter = {
346
+ request: vi.fn(async (): Promise<never> => { throw new Error('not expected') }),
347
+ stream: vi.fn(async (req: AdapterRequest): Promise<AdapterStreamResponse> => {
348
+ observedMeta = req.meta
349
+ return { status: 200, headers: {}, body: makeAsyncIterable([]) }
350
+ }),
351
+ }
352
+
353
+ await run({ adapter, options: { meta: { traceId: 'stream-trace' } as never } })
354
+ expect(observedMeta).toEqual({ traceId: 'stream-trace' })
355
+ })
331
356
  })
@@ -1,5 +1,6 @@
1
1
  import { buildAdapterRequest } from './request-builder.js'
2
2
  import { runBeforeRequest, runAfterResponse, runOnError } from './hooks.js'
3
+ import { applyRequestOptions, resolveBasePath } from './resolve-options.js'
3
4
  import { ClientRequestError } from './errors.js'
4
5
  import type {
5
6
  ClientAdapter,
@@ -7,6 +8,8 @@ import type {
7
8
  StreamDescriptor,
8
9
  TypedStream,
9
10
  AdapterResponse,
11
+ ProcedureCallDefaults,
12
+ ProcedureCallOptions,
10
13
  } from './types.js'
11
14
 
12
15
  // ── SSE item shape ────────────────────────────────────────
@@ -90,46 +93,58 @@ export function createTypedStream<TYield, TReturn = void>(
90
93
 
91
94
  // ── executeStream ─────────────────────────────────────────
92
95
 
96
+ export interface ExecuteStreamConfig {
97
+ descriptor: StreamDescriptor
98
+ basePath: string
99
+ adapter: ClientAdapter
100
+ hooks: ClientHooks
101
+ defaults?: ProcedureCallDefaults
102
+ options?: ProcedureCallOptions
103
+ }
104
+
93
105
  /**
94
106
  * Executes a streaming procedure call through the adapter.
95
107
  *
96
108
  * Flow:
97
- * 1. Build AdapterRequest from descriptor
98
- * 2. Run onBeforeRequest hooks
99
- * 3. Call adapter.stream()
100
- * 4. On adapter error: run onError hooks, re-throw
101
- * 5. Run onAfterResponse immediately (before iteration), body is null
102
- * 6. If non-2xx: throw ClientRequestError
103
- * 7. Return createTypedStream(streamResponse.body, descriptor.streamMode)
109
+ * 1. Resolve base path and build AdapterRequest
110
+ * 2. Apply request options (headers, signal, timeout, meta) from defaults + per-call
111
+ * 3. Run onBeforeRequest hooks
112
+ * 4. Call adapter.stream()
113
+ * 5. On adapter error: run onError hooks, re-throw
114
+ * 6. Run onAfterResponse immediately (before iteration), body is null
115
+ * 7. If non-2xx: throw ClientRequestError
116
+ * 8. Return createTypedStream(streamResponse.body, descriptor.streamMode)
104
117
  */
105
118
  export async function executeStream<TYield, TReturn = void>(
106
- descriptor: StreamDescriptor,
107
- basePath: string,
108
- adapter: ClientAdapter,
109
- globalHooks: ClientHooks,
110
- localHooks: ClientHooks | undefined
119
+ config: ExecuteStreamConfig,
111
120
  ): Promise<TypedStream<TYield, TReturn>> {
121
+ const { descriptor, basePath, adapter, hooks, defaults, options } = config
122
+
112
123
  // 1. Build the initial request
113
- let request = buildAdapterRequest(descriptor, basePath)
124
+ const resolvedBasePath = resolveBasePath(defaults, options, basePath)
125
+ let request = buildAdapterRequest(descriptor, resolvedBasePath)
126
+
127
+ // 2. Apply request-level options (headers, signal, timeout, meta)
128
+ request = applyRequestOptions(request, defaults, options)
114
129
 
115
- // 2. Run before-request hooks
130
+ // 3. Run before-request hooks
116
131
  const beforeCtx = await runBeforeRequest(
117
132
  { procedureName: descriptor.name, scope: descriptor.scope, request },
118
- globalHooks,
119
- localHooks
133
+ hooks,
134
+ options,
120
135
  )
121
136
  request = beforeCtx.request
122
137
 
123
- // 3. Call the adapter
138
+ // 4. Call the adapter
124
139
  let streamResponse
125
140
  try {
126
141
  streamResponse = await adapter.stream(request)
127
142
  } catch (err) {
128
- // 4. On adapter error: run error hooks, re-throw
143
+ // 5. On adapter error: run error hooks, re-throw
129
144
  await runOnError(
130
145
  { procedureName: descriptor.name, scope: descriptor.scope, request, error: err },
131
- globalHooks,
132
- localHooks
146
+ hooks,
147
+ options,
133
148
  )
134
149
  throw err
135
150
  }
@@ -141,14 +156,14 @@ export async function executeStream<TYield, TReturn = void>(
141
156
  body: null,
142
157
  }
143
158
 
144
- // 5. Run after-response hooks immediately (before iteration)
159
+ // 6. Run after-response hooks immediately (before iteration)
145
160
  await runAfterResponse(
146
161
  { procedureName: descriptor.name, scope: descriptor.scope, request, response: responseForHooks },
147
- globalHooks,
148
- localHooks
162
+ hooks,
163
+ options,
149
164
  )
150
165
 
151
- // 6. Check status after hooks (hooks may mutate responseForHooks.status)
166
+ // 7. Check status after hooks (hooks may mutate responseForHooks.status)
152
167
  if (responseForHooks.status < 200 || responseForHooks.status >= 300) {
153
168
  throw new ClientRequestError({
154
169
  status: responseForHooks.status,
@@ -159,6 +174,6 @@ export async function executeStream<TYield, TReturn = void>(
159
174
  })
160
175
  }
161
176
 
162
- // 7. Return the typed stream
177
+ // 8. Return the typed stream
163
178
  return createTypedStream<TYield, TReturn>(streamResponse.body, descriptor.streamMode)
164
179
  }
@@ -1,3 +1,35 @@
1
+ // ── Request Metadata ─────────────────────────────────────
2
+
3
+ /**
4
+ * Per-request metadata visible to adapters and hooks. Defined as an empty
5
+ * interface so consumers can augment it via TypeScript declaration merging
6
+ * to get end-to-end type safety for their own metadata fields.
7
+ *
8
+ * @example With a non-self-contained client:
9
+ * ```ts
10
+ * declare module 'ts-procedures/client' {
11
+ * interface RequestMeta {
12
+ * traceId?: string
13
+ * priority?: 'high' | 'low'
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * @example With a self-contained (code-generated) client:
19
+ * ```ts
20
+ * declare module './generated/_types' {
21
+ * interface RequestMeta {
22
+ * traceId?: string
23
+ * }
24
+ * }
25
+ * ```
26
+ *
27
+ * After augmentation, `request.meta.traceId` is typed everywhere — per-call
28
+ * options, hooks, and adapters.
29
+ */
30
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
31
+ export interface RequestMeta {}
32
+
1
33
  // ── Adapter ──────────────────────────────────────────────
2
34
 
3
35
  export interface ClientAdapter {
@@ -11,6 +43,11 @@ export interface AdapterRequest {
11
43
  headers?: Record<string, string>
12
44
  body?: unknown
13
45
  signal?: AbortSignal
46
+ /**
47
+ * Per-request metadata. Augment `RequestMeta` via declaration merging to
48
+ * type your own fields. See {@link RequestMeta}.
49
+ */
50
+ meta?: RequestMeta
14
51
  }
15
52
 
16
53
  export interface AdapterResponse {
@@ -81,14 +118,43 @@ export interface TypedStream<TYield, TReturn = void> extends AsyncIterable<TYiel
81
118
  result: Promise<TReturn>
82
119
  }
83
120
 
84
- // ── Client Instance ──────────────────────────────────────
121
+ // ── Request Options ──────────────────────────────────────
122
+
123
+ /**
124
+ * Request-level configuration that can be set as client-level defaults
125
+ * (via `CreateClientConfig.defaults`) or per-call (via `ProcedureCallOptions`).
126
+ *
127
+ * - `signal`: AbortSignal for cancellation. When both a default and per-call
128
+ * signal are provided, they're combined — whichever aborts first wins.
129
+ * - `timeout`: Timeout in milliseconds. Combined with `signal` the same way.
130
+ * A per-call `timeout: 0` disables an inherited default timeout.
131
+ * - `headers`: Extra headers merged into the request. Per-call keys win over
132
+ * default keys. Still subject to further mutation by `onBeforeRequest` hooks.
133
+ * - `basePath`: Override the base path for this call. Per-call > default > config.
134
+ * - `meta`: Per-request metadata typed via the {@link RequestMeta} interface.
135
+ * Merged shallowly (per-call keys win over default keys).
136
+ */
137
+ export interface ProcedureCallDefaults {
138
+ signal?: AbortSignal
139
+ timeout?: number
140
+ headers?: Record<string, string>
141
+ basePath?: string
142
+ meta?: RequestMeta
143
+ }
144
+
145
+ /**
146
+ * Per-call options. Extends both `ProcedureCallDefaults` (request config) and
147
+ * `ClientHooks` (hooks), so a single options bag covers both concerns.
148
+ */
149
+ export interface ProcedureCallOptions extends ProcedureCallDefaults, ClientHooks {}
85
150
 
86
- export type ProcedureCallOptions = ClientHooks
151
+ // ── Client Instance ──────────────────────────────────────
87
152
 
88
153
  export interface ClientInstance {
89
154
  basePath: string
90
155
  adapter: ClientAdapter
91
156
  hooks: ClientHooks
157
+ defaults: ProcedureCallDefaults
92
158
  call<TResponse>(descriptor: CallDescriptor, options?: ProcedureCallOptions): Promise<TResponse>
93
159
  stream<TYield, TReturn>(descriptor: StreamDescriptor, options?: ProcedureCallOptions): TypedStream<TYield, TReturn>
94
160
  }
@@ -100,4 +166,10 @@ export interface CreateClientConfig<TScopes> {
100
166
  basePath: string
101
167
  scopes: (client: ClientInstance) => TScopes
102
168
  hooks?: ClientHooks
169
+ /**
170
+ * Default request options applied to every call. Per-call options override
171
+ * these (except `signal`, which combines via AbortSignal.any — whichever
172
+ * fires first cancels the request).
173
+ */
174
+ defaults?: ProcedureCallDefaults
103
175
  }
@@ -170,6 +170,41 @@ describe('parseArgs', () => {
170
170
  const result = parseArgs(['--url', 'http://x.com/docs', '--out', './out'])
171
171
  expect(result.serviceName).toBeUndefined()
172
172
  })
173
+
174
+ it('parses --clean-out-dir flag', () => {
175
+ const result = parseArgs(['--url', 'http://x.com/docs', '--out', './out', '--clean-out-dir'])
176
+ expect(result.cleanOutDir).toBe(true)
177
+ })
178
+
179
+ it('defaults cleanOutDir to false when not provided', () => {
180
+ const result = parseArgs(['--url', 'http://x.com/docs', '--out', './out'])
181
+ expect(result.cleanOutDir).toBe(false)
182
+ })
183
+
184
+ it('parses --no-clean-out-dir flag', () => {
185
+ const result = parseArgs(['--url', 'http://x.com/docs', '--out', './out', '--no-clean-out-dir'])
186
+ expect(result.cleanOutDir).toBe(false)
187
+ })
188
+
189
+ it('CLI --no-clean-out-dir overrides config cleanOutDir', () => {
190
+ const config: CodegenConfig = {
191
+ url: 'http://config.com/docs',
192
+ outDir: './out',
193
+ cleanOutDir: true,
194
+ }
195
+ const result = parseArgs(['--no-clean-out-dir'], config)
196
+ expect(result.cleanOutDir).toBe(false)
197
+ })
198
+
199
+ it('config cleanOutDir is used as default', () => {
200
+ const config: CodegenConfig = {
201
+ url: 'http://config.com/docs',
202
+ outDir: './out',
203
+ cleanOutDir: true,
204
+ }
205
+ const result = parseArgs([], config)
206
+ expect(result.cleanOutDir).toBe(true)
207
+ })
173
208
  })
174
209
 
175
210
  describe('config file support', () => {