ts-procedures 7.2.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/README.md +65 -3
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +6 -8
  3. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +30 -33
  4. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +139 -53
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +208 -231
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +80 -153
  7. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
  8. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +4 -5
  9. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +4 -7
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono.md +223 -0
  11. package/agent_config/copilot/copilot-instructions.md +36 -48
  12. package/agent_config/cursor/cursorrules +36 -48
  13. package/build/client/call.js +4 -1
  14. package/build/client/call.js.map +1 -1
  15. package/build/client/call.test.js +23 -0
  16. package/build/client/call.test.js.map +1 -1
  17. package/build/client/fetch-adapter.js +3 -1
  18. package/build/client/fetch-adapter.js.map +1 -1
  19. package/build/client/fetch-adapter.test.js +11 -1
  20. package/build/client/fetch-adapter.test.js.map +1 -1
  21. package/build/client/index.test.js +7 -7
  22. package/build/client/index.test.js.map +1 -1
  23. package/build/client/request-builder.d.ts +1 -1
  24. package/build/client/request-builder.js +2 -2
  25. package/build/client/request-builder.js.map +1 -1
  26. package/build/client/stream.js +13 -2
  27. package/build/client/stream.js.map +1 -1
  28. package/build/client/stream.test.js +32 -7
  29. package/build/client/stream.test.js.map +1 -1
  30. package/build/client/typed-error-dispatch.test.js +8 -92
  31. package/build/client/typed-error-dispatch.test.js.map +1 -1
  32. package/build/client/types.d.ts +21 -3
  33. package/build/codegen/bin/cli.js +0 -0
  34. package/build/codegen/e2e.test.js +87 -23
  35. package/build/codegen/e2e.test.js.map +1 -1
  36. package/build/codegen/emit-errors.integration.test.js +1 -1
  37. package/build/codegen/emit-errors.integration.test.js.map +1 -1
  38. package/build/codegen/emit-scope.js +308 -47
  39. package/build/codegen/emit-scope.js.map +1 -1
  40. package/build/codegen/emit-scope.test.js +363 -110
  41. package/build/codegen/emit-scope.test.js.map +1 -1
  42. package/build/codegen/pipeline.test.js +7 -7
  43. package/build/codegen/pipeline.test.js.map +1 -1
  44. package/build/codegen/resolve-envelope.js +1 -1
  45. package/build/codegen/resolve-envelope.js.map +1 -1
  46. package/build/codegen/resolve-envelope.test.js +5 -5
  47. package/build/codegen/resolve-envelope.test.js.map +1 -1
  48. package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
  49. package/build/codegen/targets/_shared/route-slots.js +49 -8
  50. package/build/codegen/targets/_shared/route-slots.js.map +1 -1
  51. package/build/codegen/targets/_shared/route-slots.test.js +99 -26
  52. package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
  53. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
  54. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
  55. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
  56. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
  57. package/build/codegen/targets/kotlin/integration.test.js +6 -0
  58. package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
  59. package/build/codegen/targets/swift/access-level.test.js +8 -11
  60. package/build/codegen/targets/swift/access-level.test.js.map +1 -1
  61. package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
  62. package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
  63. package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
  64. package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
  65. package/build/codegen/targets/swift/integration.test.js +6 -0
  66. package/build/codegen/targets/swift/integration.test.js.map +1 -1
  67. package/build/create-http-stream.d.ts +58 -0
  68. package/build/create-http-stream.js +122 -0
  69. package/build/create-http-stream.js.map +1 -0
  70. package/build/create-http-stream.test.js +88 -0
  71. package/build/create-http-stream.test.js.map +1 -0
  72. package/build/create-http.d.ts +49 -0
  73. package/build/create-http.js +108 -0
  74. package/build/create-http.js.map +1 -0
  75. package/build/create-http.test.js +137 -0
  76. package/build/create-http.test.js.map +1 -0
  77. package/build/create-stream.d.ts +35 -0
  78. package/build/create-stream.js +123 -0
  79. package/build/create-stream.js.map +1 -0
  80. package/build/create-stream.test.js +428 -0
  81. package/build/create-stream.test.js.map +1 -0
  82. package/build/create.d.ts +28 -0
  83. package/build/create.js +82 -0
  84. package/build/create.js.map +1 -0
  85. package/build/create.test.js +483 -0
  86. package/build/create.test.js.map +1 -0
  87. package/build/exports.d.ts +2 -0
  88. package/build/implementations/http/astro/index.test.js +20 -12
  89. package/build/implementations/http/astro/index.test.js.map +1 -1
  90. package/build/implementations/http/doc-registry.js +1 -1
  91. package/build/implementations/http/doc-registry.js.map +1 -1
  92. package/build/implementations/http/doc-registry.test.js +36 -5
  93. package/build/implementations/http/doc-registry.test.js.map +1 -1
  94. package/build/implementations/http/error-dispatch.d.ts +76 -0
  95. package/build/implementations/http/error-dispatch.js +77 -0
  96. package/build/implementations/http/error-dispatch.js.map +1 -0
  97. package/build/implementations/http/error-dispatch.test.js +254 -0
  98. package/build/implementations/http/error-dispatch.test.js.map +1 -0
  99. package/build/implementations/http/error-taxonomy.d.ts +5 -5
  100. package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
  101. package/build/implementations/http/hono/docs/http-doc.js +42 -0
  102. package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
  103. package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
  104. package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
  105. package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
  106. package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
  107. package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
  108. package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
  109. package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
  110. package/build/implementations/http/hono/docs/stream-doc.js +42 -0
  111. package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
  112. package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
  113. package/build/implementations/http/hono/handlers/http-stream.js +123 -0
  114. package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
  115. package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
  116. package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
  117. package/build/implementations/http/hono/handlers/http.d.ts +10 -0
  118. package/build/implementations/http/hono/handlers/http.js +115 -0
  119. package/build/implementations/http/hono/handlers/http.js.map +1 -0
  120. package/build/implementations/http/hono/handlers/http.test.js +118 -0
  121. package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
  122. package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
  123. package/build/implementations/http/hono/handlers/rpc.js +32 -0
  124. package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
  125. package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
  126. package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
  127. package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
  128. package/build/implementations/http/hono/handlers/stream.js +147 -0
  129. package/build/implementations/http/hono/handlers/stream.js.map +1 -0
  130. package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
  131. package/build/implementations/http/hono/handlers/stream.test.js +177 -0
  132. package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
  133. package/build/implementations/http/hono/index.d.ts +57 -0
  134. package/build/implementations/http/hono/index.js +149 -0
  135. package/build/implementations/http/hono/index.js.map +1 -0
  136. package/build/implementations/http/hono/index.test.d.ts +1 -0
  137. package/build/implementations/http/hono/index.test.js +274 -0
  138. package/build/implementations/http/hono/index.test.js.map +1 -0
  139. package/build/implementations/http/hono/path.d.ts +17 -0
  140. package/build/implementations/http/hono/path.js +39 -0
  141. package/build/implementations/http/hono/path.js.map +1 -0
  142. package/build/implementations/http/hono/path.test.d.ts +1 -0
  143. package/build/implementations/http/hono/path.test.js +83 -0
  144. package/build/implementations/http/hono/path.test.js.map +1 -0
  145. package/build/implementations/http/hono/types.d.ts +51 -0
  146. package/build/implementations/http/hono/types.js.map +1 -0
  147. package/build/implementations/http/on-request-error.test.js +6 -96
  148. package/build/implementations/http/on-request-error.test.js.map +1 -1
  149. package/build/implementations/http/route-errors.test.js +11 -59
  150. package/build/implementations/http/route-errors.test.js.map +1 -1
  151. package/build/implementations/types.d.ts +43 -9
  152. package/build/index.d.ts +125 -115
  153. package/build/index.js +10 -222
  154. package/build/index.js.map +1 -1
  155. package/build/index.test.js +30 -822
  156. package/build/index.test.js.map +1 -1
  157. package/build/migration.test.d.ts +1 -0
  158. package/build/migration.test.js +34 -0
  159. package/build/migration.test.js.map +1 -0
  160. package/build/schema/compute-schema.d.ts +11 -3
  161. package/build/schema/compute-schema.js +13 -7
  162. package/build/schema/compute-schema.js.map +1 -1
  163. package/build/schema/parser.d.ts +11 -3
  164. package/build/schema/parser.js +49 -9
  165. package/build/schema/parser.js.map +1 -1
  166. package/build/stack-utils.js +8 -0
  167. package/build/stack-utils.js.map +1 -1
  168. package/build/types.d.ts +142 -0
  169. package/build/types.js.map +1 -0
  170. package/docs/astro-adapter.md +5 -5
  171. package/docs/core.md +34 -17
  172. package/docs/http-integrations.md +83 -170
  173. package/docs/streaming.md +3 -60
  174. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
  175. package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
  176. package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
  177. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
  178. package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
  179. package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
  180. package/package.json +4 -22
  181. package/src/client/call.test.ts +26 -0
  182. package/src/client/call.ts +4 -1
  183. package/src/client/fetch-adapter.test.ts +14 -1
  184. package/src/client/fetch-adapter.ts +3 -1
  185. package/src/client/index.test.ts +7 -7
  186. package/src/client/request-builder.ts +2 -2
  187. package/src/client/stream.test.ts +39 -7
  188. package/src/client/stream.ts +16 -2
  189. package/src/client/typed-error-dispatch.test.ts +7 -97
  190. package/src/client/types.ts +21 -3
  191. package/src/codegen/__fixtures__/users-envelope.json +119 -38
  192. package/src/codegen/e2e.test.ts +98 -24
  193. package/src/codegen/emit-errors.integration.test.ts +1 -1
  194. package/src/codegen/emit-scope.test.ts +395 -110
  195. package/src/codegen/emit-scope.ts +350 -55
  196. package/src/codegen/pipeline.test.ts +7 -7
  197. package/src/codegen/resolve-envelope.test.ts +5 -5
  198. package/src/codegen/resolve-envelope.ts +1 -1
  199. package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
  200. package/src/codegen/targets/_shared/route-slots.ts +48 -11
  201. package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
  202. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
  203. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
  204. package/src/codegen/targets/kotlin/integration.test.ts +19 -0
  205. package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
  206. package/src/codegen/targets/swift/access-level.test.ts +8 -11
  207. package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
  208. package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
  209. package/src/codegen/targets/swift/integration.test.ts +17 -0
  210. package/src/create-http-stream.test.ts +97 -0
  211. package/src/create-http-stream.ts +191 -0
  212. package/src/create-http.test.ts +163 -0
  213. package/src/create-http.ts +211 -0
  214. package/src/create-stream.test.ts +565 -0
  215. package/src/create-stream.ts +228 -0
  216. package/src/create.test.ts +658 -0
  217. package/src/create.ts +172 -0
  218. package/src/exports.ts +2 -0
  219. package/src/implementations/http/README.md +135 -95
  220. package/src/implementations/http/astro/README.md +4 -5
  221. package/src/implementations/http/astro/index.test.ts +25 -18
  222. package/src/implementations/http/doc-registry.test.ts +42 -5
  223. package/src/implementations/http/doc-registry.ts +1 -1
  224. package/src/implementations/http/error-dispatch.test.ts +283 -0
  225. package/src/implementations/http/error-dispatch.ts +176 -0
  226. package/src/implementations/http/error-taxonomy.ts +5 -5
  227. package/src/implementations/http/hono/docs/http-doc.ts +43 -0
  228. package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
  229. package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
  230. package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
  231. package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
  232. package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
  233. package/src/implementations/http/hono/handlers/http.test.ts +130 -0
  234. package/src/implementations/http/hono/handlers/http.ts +147 -0
  235. package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
  236. package/src/implementations/http/hono/handlers/rpc.ts +54 -0
  237. package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
  238. package/src/implementations/http/hono/handlers/stream.ts +208 -0
  239. package/src/implementations/http/hono/index.test.ts +329 -0
  240. package/src/implementations/http/hono/index.ts +204 -0
  241. package/src/implementations/http/hono/path.test.ts +96 -0
  242. package/src/implementations/http/hono/path.ts +59 -0
  243. package/src/implementations/http/hono/types.ts +93 -0
  244. package/src/implementations/http/on-request-error.test.ts +10 -116
  245. package/src/implementations/http/route-errors.test.ts +11 -77
  246. package/src/implementations/types.ts +44 -9
  247. package/src/index.test.ts +35 -1091
  248. package/src/index.ts +50 -474
  249. package/src/migration.test.ts +48 -0
  250. package/src/schema/compute-schema.ts +26 -12
  251. package/src/schema/parser.ts +62 -12
  252. package/src/stack-utils.ts +8 -0
  253. package/src/types.ts +133 -0
  254. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
  255. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
  256. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
  257. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
  258. package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
  259. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
  260. package/build/implementations/http/express-rpc/index.d.ts +0 -125
  261. package/build/implementations/http/express-rpc/index.js +0 -216
  262. package/build/implementations/http/express-rpc/index.js.map +0 -1
  263. package/build/implementations/http/express-rpc/index.test.js +0 -684
  264. package/build/implementations/http/express-rpc/index.test.js.map +0 -1
  265. package/build/implementations/http/express-rpc/types.d.ts +0 -11
  266. package/build/implementations/http/express-rpc/types.js.map +0 -1
  267. package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
  268. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
  269. package/build/implementations/http/hono-api/index.d.ts +0 -151
  270. package/build/implementations/http/hono-api/index.js +0 -344
  271. package/build/implementations/http/hono-api/index.js.map +0 -1
  272. package/build/implementations/http/hono-api/index.test.js +0 -992
  273. package/build/implementations/http/hono-api/index.test.js.map +0 -1
  274. package/build/implementations/http/hono-api/types.d.ts +0 -13
  275. package/build/implementations/http/hono-api/types.js.map +0 -1
  276. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
  277. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
  278. package/build/implementations/http/hono-rpc/index.d.ts +0 -130
  279. package/build/implementations/http/hono-rpc/index.js +0 -209
  280. package/build/implementations/http/hono-rpc/index.js.map +0 -1
  281. package/build/implementations/http/hono-rpc/index.test.js +0 -828
  282. package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
  283. package/build/implementations/http/hono-rpc/types.d.ts +0 -11
  284. package/build/implementations/http/hono-rpc/types.js +0 -2
  285. package/build/implementations/http/hono-rpc/types.js.map +0 -1
  286. package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
  287. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
  288. package/build/implementations/http/hono-stream/index.d.ts +0 -171
  289. package/build/implementations/http/hono-stream/index.js +0 -415
  290. package/build/implementations/http/hono-stream/index.js.map +0 -1
  291. package/build/implementations/http/hono-stream/index.test.js +0 -1383
  292. package/build/implementations/http/hono-stream/index.test.js.map +0 -1
  293. package/build/implementations/http/hono-stream/types.d.ts +0 -15
  294. package/build/implementations/http/hono-stream/types.js +0 -2
  295. package/build/implementations/http/hono-stream/types.js.map +0 -1
  296. package/src/implementations/http/express-rpc/README.md +0 -280
  297. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
  298. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  299. package/src/implementations/http/express-rpc/index.ts +0 -327
  300. package/src/implementations/http/express-rpc/types.ts +0 -16
  301. package/src/implementations/http/hono-api/README.md +0 -284
  302. package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
  303. package/src/implementations/http/hono-api/index.test.ts +0 -1341
  304. package/src/implementations/http/hono-api/index.ts +0 -519
  305. package/src/implementations/http/hono-api/types.ts +0 -16
  306. package/src/implementations/http/hono-rpc/README.md +0 -357
  307. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
  308. package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
  309. package/src/implementations/http/hono-rpc/index.ts +0 -320
  310. package/src/implementations/http/hono-rpc/types.ts +0 -16
  311. package/src/implementations/http/hono-stream/README.md +0 -559
  312. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
  313. package/src/implementations/http/hono-stream/index.test.ts +0 -1804
  314. package/src/implementations/http/hono-stream/index.ts +0 -622
  315. package/src/implementations/http/hono-stream/types.ts +0 -20
  316. /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
  317. /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
  318. /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
  319. /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
  320. /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
  321. /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
  322. /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
  323. /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
  324. /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
  325. /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
@@ -28,23 +28,20 @@ const envelope: DocEnvelope = {
28
28
  method: 'get',
29
29
  path: '/things/:id',
30
30
  fullPath: '/things/:id',
31
- jsonSchema: {},
32
- // The swift route emitter reads `route.schema.input.*` and
33
- // `route.schema.returnType` directly; mirror what the codegen
34
- // pipeline downstream expects rather than the doc-builder
35
- // `jsonSchema` shape.
36
- schema: {
37
- input: {
31
+ jsonSchema: {
32
+ req: {
38
33
  pathParams: {
39
34
  type: 'object',
40
35
  properties: { id: { type: 'string' } },
41
36
  required: ['id'],
42
37
  },
43
38
  },
44
- returnType: {
45
- type: 'object',
46
- properties: { id: { type: 'string' } },
47
- required: ['id'],
39
+ res: {
40
+ body: {
41
+ type: 'object',
42
+ properties: { id: { type: 'string' } },
43
+ required: ['id'],
44
+ },
48
45
  },
49
46
  },
50
47
  errors: [],
@@ -34,11 +34,9 @@ describe('emitSwiftRoute', () => {
34
34
  name: 'GetUser',
35
35
  method: 'GET',
36
36
  fullPath: '/users/:id',
37
- schema: {
38
- input: {
39
- pathParams: { type: 'object' },
40
- },
41
- returnType: { type: 'object' },
37
+ jsonSchema: {
38
+ req: { pathParams: { type: 'object' } },
39
+ res: { body: { type: 'object' } },
42
40
  },
43
41
  errors: [],
44
42
  } as unknown as AnyHttpRouteDoc
@@ -64,7 +62,10 @@ describe('emitSwiftRoute', () => {
64
62
  name: 'CreateUser',
65
63
  method: 'POST',
66
64
  fullPath: '/users',
67
- schema: { input: { body: { type: 'object' } }, returnType: { type: 'object' } },
65
+ jsonSchema: {
66
+ req: { body: { type: 'object' } },
67
+ res: { body: { type: 'object' } },
68
+ },
68
69
  errors: [],
69
70
  } as unknown as AnyHttpRouteDoc
70
71
 
@@ -84,7 +85,10 @@ describe('emitSwiftRoute', () => {
84
85
  name: 'GetUser',
85
86
  method: 'GET',
86
87
  fullPath: '/users/:id',
87
- schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
88
+ jsonSchema: {
89
+ req: { pathParams: { type: 'object' } },
90
+ res: { body: { type: 'object' } },
91
+ },
88
92
  errors: ['NotFound'],
89
93
  } as unknown as AnyHttpRouteDoc
90
94
 
@@ -103,14 +107,14 @@ describe('emitSwiftRoute', () => {
103
107
  it('silently skips error keys with no schema in the envelope map', () => {
104
108
  const route = {
105
109
  kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users',
106
- schema: {}, errors: ['UnknownTaxonomyKey'],
110
+ jsonSchema: {}, errors: ['UnknownTaxonomyKey'],
107
111
  } as unknown as AnyHttpRouteDoc
108
112
  const result = emitSwiftRoute(route, createStubSwiftEmitter({}), new Map())
109
113
  expect(result.code).not.toContain('enum Errors {')
110
114
  })
111
115
 
112
116
  it('returns skipped:true for stream routes', () => {
113
- const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
117
+ const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
114
118
  const result = emitSwiftRoute(route, createStubSwiftEmitter({}), noErrors)
115
119
  expect(result.code).toBe('')
116
120
  expect(result.skipped).toBe(true)
@@ -122,9 +126,9 @@ describe('emitSwiftRoute', () => {
122
126
  name: 'GetUser',
123
127
  method: 'GET',
124
128
  fullPath: '/users/:id',
125
- schema: {
126
- input: { pathParams: { type: 'object' } },
127
- returnType: { type: 'object' },
129
+ jsonSchema: {
130
+ req: { pathParams: { type: 'object' } },
131
+ res: { body: { type: 'object' } },
128
132
  },
129
133
  errors: ['NotFound'],
130
134
  } as unknown as AnyHttpRouteDoc
@@ -157,13 +161,13 @@ describe('emitSwiftRoute', () => {
157
161
  name: 'X',
158
162
  method: 'POST',
159
163
  fullPath: '/x/:id',
160
- schema: {
161
- input: {
164
+ jsonSchema: {
165
+ req: {
162
166
  pathParams: { type: 'object' },
163
167
  query: { type: 'object' },
164
168
  body: { type: 'object' },
165
169
  },
166
- returnType: { type: 'object' },
170
+ res: { body: { type: 'object' } },
167
171
  },
168
172
  errors: ['Z'],
169
173
  } as unknown as AnyHttpRouteDoc
@@ -189,7 +193,7 @@ describe('emitSwiftRoute', () => {
189
193
  name: 'X',
190
194
  method: 'GET',
191
195
  fullPath: '/x',
192
- schema: { returnType: { type: 'object' } },
196
+ jsonSchema: { res: { body: { type: 'object' } } },
193
197
  errors: [],
194
198
  } as unknown as AnyHttpRouteDoc
195
199
 
@@ -209,13 +213,92 @@ describe('emitSwiftRoute', () => {
209
213
  expect(calls[0]!.opts.uncountableWords).toEqual(['data', 'metadata'])
210
214
  })
211
215
 
216
+ it('emits ResponseHeaders slot when res.headers is declared on an api route', () => {
217
+ const route = {
218
+ kind: 'api',
219
+ name: 'DownloadUser',
220
+ method: 'GET',
221
+ fullPath: '/users/:id/download',
222
+ jsonSchema: {
223
+ req: { pathParams: { type: 'object' } },
224
+ res: {
225
+ body: { type: 'object' },
226
+ headers: { type: 'object' },
227
+ },
228
+ },
229
+ errors: [],
230
+ } as unknown as AnyHttpRouteDoc
231
+
232
+ const { emitter, calls } = makeSpyEmitter({
233
+ PathParams: ok('public struct PathParams: Codable { public let id: String }', 'PathParams'),
234
+ Response: ok('public struct Response: Codable { public let url: String }', 'Response'),
235
+ ResponseHeaders: ok(
236
+ 'public struct ResponseHeaders: Codable { public let xDownloadToken: String }',
237
+ 'ResponseHeaders',
238
+ ),
239
+ })
240
+
241
+ const result = emitSwiftRoute(route, emitter, noErrors)
242
+ expect(calls.map((c) => c.opts.rootTypeName)).toEqual(['PathParams', 'Response', 'ResponseHeaders'])
243
+ expect(result.code).toContain('public struct ResponseHeaders: Codable { public let xDownloadToken: String }')
244
+ })
245
+
246
+ it('does not emit ResponseHeaders when res.headers is absent', () => {
247
+ const route = {
248
+ kind: 'api',
249
+ name: 'GetUser',
250
+ method: 'GET',
251
+ fullPath: '/users/:id',
252
+ jsonSchema: {
253
+ req: { pathParams: { type: 'object' } },
254
+ res: { body: { type: 'object' } },
255
+ },
256
+ errors: [],
257
+ } as unknown as AnyHttpRouteDoc
258
+
259
+ const { emitter, calls } = makeSpyEmitter({
260
+ PathParams: ok('public struct PathParams: Codable { public let id: String }', 'PathParams'),
261
+ Response: ok('public struct Response: Codable { public let id: String }', 'Response'),
262
+ })
263
+
264
+ emitSwiftRoute(route, emitter, noErrors)
265
+ expect(calls.map((c) => c.opts.rootTypeName)).not.toContain('ResponseHeaders')
266
+ })
267
+
268
+ it('processes http-stream routes (not skipped) and emits Yield/ReturnType slots', () => {
269
+ const route = {
270
+ kind: 'http-stream',
271
+ name: 'TailLogs',
272
+ method: 'GET',
273
+ fullPath: '/logs/tail',
274
+ jsonSchema: {
275
+ req: { query: { type: 'object' } },
276
+ yield: { type: 'object' },
277
+ returnType: { type: 'object' },
278
+ },
279
+ errors: [],
280
+ } as unknown as AnyHttpRouteDoc
281
+
282
+ const { emitter, calls } = makeSpyEmitter({
283
+ Query: ok('public struct Query: Codable { public let filter: String? }', 'Query'),
284
+ Yield: ok('public struct Yield: Codable { public let line: String }', 'Yield'),
285
+ ReturnType: ok('public struct ReturnType: Codable { public let count: Int }', 'ReturnType'),
286
+ })
287
+
288
+ const result = emitSwiftRoute(route, emitter, noErrors)
289
+ expect(result.skipped).toBeFalsy()
290
+ expect(calls.map((c) => c.opts.rootTypeName)).toEqual(['Query', 'Yield', 'ReturnType'])
291
+ expect(result.code).toContain('public static let method = "GET"')
292
+ expect(result.code).toContain('public static let path = "/logs/tail"')
293
+ })
294
+
212
295
  it('does not include passthrough keys when caller omits them', () => {
213
296
  const route = {
214
297
  kind: 'api',
215
298
  name: 'X',
216
299
  method: 'GET',
217
300
  fullPath: '/x',
218
- schema: { returnType: { type: 'object' } },
301
+ jsonSchema: { res: { body: { type: 'object' } } },
219
302
  errors: [],
220
303
  } as unknown as AnyHttpRouteDoc
221
304
 
@@ -246,7 +329,7 @@ describe('emitSwiftRoute', () => {
246
329
  name: 'GetUser',
247
330
  method: 'GET',
248
331
  fullPath: '/users/:id',
249
- schema: { input: { pathParams: { type: 'object' } } },
332
+ jsonSchema: { req: { pathParams: { type: 'object' } } },
250
333
  errors: [],
251
334
  } as unknown as AnyHttpRouteDoc
252
335
 
@@ -265,7 +348,7 @@ describe('emitSwiftRoute', () => {
265
348
  name: 'GetUser',
266
349
  method: 'GET',
267
350
  fullPath: '/users/:id',
268
- schema: { input: { pathParams: { type: 'object' } } },
351
+ jsonSchema: { req: { pathParams: { type: 'object' } } },
269
352
  errors: ['NotFound'],
270
353
  } as unknown as AnyHttpRouteDoc
271
354
 
@@ -291,7 +374,7 @@ describe('emitSwiftRoute', () => {
291
374
  name: 'CreateUser',
292
375
  method: 'POST',
293
376
  fullPath: '/users',
294
- schema: {},
377
+ jsonSchema: {},
295
378
  errors: [],
296
379
  } as unknown as AnyHttpRouteDoc
297
380
  const result = emitSwiftRoute(route, createStubSwiftEmitter({}), noErrors, { accessLevel: 'internal' })
@@ -18,7 +18,10 @@ describe('emitSwiftScope', () => {
18
18
  name: 'GetUser',
19
19
  method: 'GET',
20
20
  fullPath: '/users/:id',
21
- schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
21
+ jsonSchema: {
22
+ req: { pathParams: { type: 'object' } },
23
+ res: { body: { type: 'object' } },
24
+ },
22
25
  errors: [],
23
26
  } as unknown as AnyHttpRouteDoc
24
27
 
@@ -41,8 +44,8 @@ describe('emitSwiftScope', () => {
41
44
  })
42
45
 
43
46
  it('joins multiple routes inside one scope enum, separated by a blank line', () => {
44
- const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
45
- const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
47
+ const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
48
+ const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
46
49
  const group: ScopeGroup = { scopeKey: 'users', camelCase: 'users', routes: [route1, route2] }
47
50
  const emitter = createStubSwiftEmitter({})
48
51
 
@@ -74,8 +77,8 @@ describe('emitSwiftScope', () => {
74
77
  })
75
78
 
76
79
  it('dedupes and sorts imports across all routes in the scope', () => {
77
- const route1 = { kind: 'api', name: 'A', method: 'GET', fullPath: '/a', schema: { returnType: { type: 'object' } }, errors: [] } as unknown as AnyHttpRouteDoc
78
- const route2 = { kind: 'api', name: 'B', method: 'GET', fullPath: '/b', schema: { returnType: { type: 'object' } }, errors: [] } as unknown as AnyHttpRouteDoc
80
+ const route1 = { kind: 'api', name: 'A', method: 'GET', fullPath: '/a', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
81
+ const route2 = { kind: 'api', name: 'B', method: 'GET', fullPath: '/b', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
79
82
  const group: ScopeGroup = { scopeKey: 'multi', camelCase: 'multi', routes: [route1, route2] }
80
83
  const emitter = {
81
84
  emit(_s: unknown, opts: SwiftEmitOptions): SwiftEmitResult {
@@ -93,7 +96,7 @@ describe('emitSwiftScope', () => {
93
96
  })
94
97
 
95
98
  it('threads all 6 passthrough opts to every emitter call', () => {
96
- const route = { kind: 'api', name: 'X', method: 'GET', fullPath: '/x', schema: { returnType: { type: 'object' } }, errors: [] } as unknown as AnyHttpRouteDoc
99
+ const route = { kind: 'api', name: 'X', method: 'GET', fullPath: '/x', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
97
100
  const group: ScopeGroup = { scopeKey: 'x', camelCase: 'x', routes: [route] }
98
101
 
99
102
  const calls: SwiftEmitOptions[] = []
@@ -130,7 +133,7 @@ describe('emitSwiftScope', () => {
130
133
  })
131
134
 
132
135
  it('applies accessLevel to the outer scope enum and the route enum wrappers', () => {
133
- const route = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
136
+ const route = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
134
137
  const group: ScopeGroup = { scopeKey: 'users', camelCase: 'users', routes: [route] }
135
138
  const emitter = createStubSwiftEmitter({})
136
139
 
@@ -142,8 +145,8 @@ describe('emitSwiftScope', () => {
142
145
  })
143
146
 
144
147
  it('collects skipped stream-route names', () => {
145
- const stream = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/u/stream', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
146
- const api = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/u', schema: { returnType: { type: 'object' } }, errors: [] } as unknown as AnyHttpRouteDoc
148
+ const stream = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/u/stream', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
149
+ const api = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/u', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
147
150
  const group: ScopeGroup = { scopeKey: 'u', camelCase: 'u', routes: [stream, api] }
148
151
 
149
152
  const emitter = createStubSwiftEmitter({
@@ -62,6 +62,23 @@ describe('swift codegen — integration', () => {
62
62
  'public struct Query: Codable {\n public let status: Status?\n public let limit: Int64?\n\n public enum Status: String, Codable {\n case active\n case inactive\n }\n}',
63
63
  'Query',
64
64
  ),
65
+
66
+ // DownloadUser
67
+ ResponseHeaders: ok(
68
+ 'public struct ResponseHeaders: Codable {\n public let xDownloadToken: String\n public let contentDisposition: String?\n\n enum CodingKeys: String, CodingKey {\n case xDownloadToken = "x-download-token"\n case contentDisposition = "content-disposition"\n }\n}',
69
+ 'ResponseHeaders',
70
+ ),
71
+
72
+ // WatchUsers (http-stream) — reuses Query and ResponseHeaders stubs from above;
73
+ // adds Yield and ReturnType which are unique to the stream route.
74
+ Yield: ok(
75
+ 'public struct Yield: Codable {\n public let id: String\n public let event: Event\n\n public enum Event: String, Codable {\n case created\n case updated\n case deleted\n }\n}',
76
+ 'Yield',
77
+ ),
78
+ ReturnType: ok(
79
+ 'public struct ReturnType: Codable {\n public let count: Int64\n}',
80
+ 'ReturnType',
81
+ ),
65
82
  })
66
83
 
67
84
  const files = await runPipeline({
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { Procedures } from './index.js'
3
+ import { Type } from 'typebox'
4
+ import { ProcedureValidationError } from './errors.js'
5
+
6
+ describe('CreateHttpStream basic generator', () => {
7
+ it('registers with kind: http-stream', () => {
8
+ const procs = Procedures()
9
+ procs.CreateHttpStream('Tail', {
10
+ path: '/streams/logs', method: 'get',
11
+ schema: {
12
+ req: { query: Type.Object({ source: Type.String() }) },
13
+ yield: Type.Object({ line: Type.String() }),
14
+ },
15
+ }, async function* () { yield { line: 'a' } })
16
+
17
+ expect(procs.getProcedure('Tail')?.kind).toBe('http-stream')
18
+ })
19
+
20
+ it('yields and returns', async () => {
21
+ const procs = Procedures()
22
+ const { Tail } = procs.CreateHttpStream('Tail', {
23
+ path: '/streams/logs', method: 'get',
24
+ schema: {
25
+ req: { query: Type.Object({ source: Type.String() }) },
26
+ yield: Type.Object({ line: Type.String() }),
27
+ returnType: Type.Object({ totalLines: Type.Number() }),
28
+ },
29
+ }, async function* (_, { query }) {
30
+ yield { line: `from-${query.source}-1` }
31
+ yield { line: `from-${query.source}-2` }
32
+ return { totalLines: 2 }
33
+ })
34
+
35
+ const lines: string[] = []
36
+ let returnVal: unknown
37
+ const { stream } = await Tail({} as any, { query: { source: 'app' } })
38
+ let result = await stream.next()
39
+ while (!result.done) {
40
+ lines.push(result.value.line)
41
+ result = await stream.next()
42
+ }
43
+ returnVal = result.value
44
+ expect(lines).toEqual(['from-app-1', 'from-app-2'])
45
+ expect(returnVal).toEqual({ totalLines: 2 })
46
+ })
47
+
48
+ it('rejects req.query validation error before opening stream', async () => {
49
+ const procs = Procedures()
50
+ const { Tail } = procs.CreateHttpStream('Tail', {
51
+ path: '/streams/logs', method: 'get',
52
+ schema: {
53
+ req: { query: Type.Object({ source: Type.String() }) },
54
+ yield: Type.Object({ line: Type.String() }),
55
+ },
56
+ }, async function* () { yield { line: 'should not run' } })
57
+
58
+ // Two independent async calls — each rejects before a stream is produced.
59
+ await expect(Tail({} as any, { query: {} as any })).rejects.toThrow(ProcedureValidationError)
60
+ await expect(Tail({} as any, { query: {} as any })).rejects.toThrow(/req\.query/)
61
+ })
62
+ })
63
+
64
+ describe('CreateHttpStream async-preamble shape (res.headers)', () => {
65
+ it('handler returning Promise<{ headers, stream }> surfaces headers in initialHeaders', async () => {
66
+ const procs = Procedures()
67
+ const { Tail } = procs.CreateHttpStream('Tail', {
68
+ path: '/streams/logs', method: 'get',
69
+ schema: {
70
+ req: { query: Type.Object({ source: Type.String() }) },
71
+ yield: Type.Object({ line: Type.String() }),
72
+ res: { headers: Type.Object({ 'x-stream-id': Type.String() }) },
73
+ },
74
+ }, async (_, { query }) => ({
75
+ headers: { 'x-stream-id': `id-${query.source}` },
76
+ stream: (async function* () { yield { line: 'a' } })(),
77
+ }))
78
+
79
+ const { stream, initialHeaders } = await Tail({} as any, { query: { source: 'app' } })
80
+ expect(initialHeaders).toEqual({ 'x-stream-id': 'id-app' })
81
+ const first = await stream.next()
82
+ expect(first.value).toEqual({ line: 'a' })
83
+ })
84
+
85
+ it('throws when handler returns malformed shape', async () => {
86
+ const procs = Procedures()
87
+ const { Bad } = procs.CreateHttpStream('Bad', {
88
+ path: '/streams', method: 'get',
89
+ schema: {
90
+ yield: Type.Number(),
91
+ res: { headers: Type.Object({}) },
92
+ },
93
+ }, (async () => 'not-a-shape') as any)
94
+
95
+ await expect(Bad({} as any, undefined)).rejects.toThrow(/did not resolve to/)
96
+ })
97
+ })
@@ -0,0 +1,191 @@
1
+ import { ProcedureError, ProcedureRegistrationError, ProcedureValidationError, ProcedureYieldValidationError } from './errors.js'
2
+ import { computeSchema } from './schema/compute-schema.js'
3
+ import { Prettify, TSchemaLib } from './schema/types.js'
4
+ import { captureDefinitionInfo } from './stack-utils.js'
5
+ import {
6
+ HttpMethod, TBuilderConfig, TStreamContext,
7
+ THttpStreamProcedureRegistration, TProcedureRegistration, TStreamProcedureRegistration, THttpProcedureRegistration,
8
+ } from './types.js'
9
+ import { checkPathParamConsistency } from './create-http.js'
10
+
11
+ /**
12
+ * Wraps a user's AsyncIterator in a generator that:
13
+ * - optionally validates each yielded value
14
+ * - augments error stack traces with the procedure definition site
15
+ * - aborts the stream controller on completion
16
+ */
17
+ async function* yieldingWrapper(
18
+ userIterator: AsyncIterator<any, any, unknown>,
19
+ validateYields: boolean,
20
+ validations: { yield?: (value: any) => { errors?: any[] } },
21
+ abortController: AbortController,
22
+ definitionInfo: ReturnType<typeof captureDefinitionInfo>,
23
+ name: string,
24
+ ): AsyncGenerator<any, any, unknown> {
25
+ try {
26
+ let result = await userIterator.next()
27
+ while (!result.done) {
28
+ const value = result.value
29
+ if (validateYields && validations.yield) {
30
+ const { errors } = validations.yield(value)
31
+ if (errors) throw new ProcedureYieldValidationError(name, `Yield validation error for ${name}`, errors, definitionInfo)
32
+ }
33
+ yield value
34
+ result = await userIterator.next()
35
+ }
36
+ return result.value
37
+ } catch (error: any) {
38
+ if (definitionInfo.definedAt && error && typeof error.stack === 'string') {
39
+ const { file, line, column } = definitionInfo.definedAt
40
+ error.stack = `${error.stack}\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
41
+ }
42
+ throw error
43
+ } finally {
44
+ try { await userIterator.return?.(undefined) } catch { /* swallow */ }
45
+ abortController.abort('stream-completed')
46
+ }
47
+ }
48
+
49
+ export function makeCreateHttpStream<TContext>(
50
+ procedures: Map<string, TProcedureRegistration<TContext, any> | TStreamProcedureRegistration<TContext, any> | THttpProcedureRegistration<TContext> | THttpStreamProcedureRegistration<TContext>>,
51
+ builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
52
+ ) {
53
+ return function CreateHttpStream<
54
+ TName extends string,
55
+ TReq extends Record<string, unknown> | undefined,
56
+ TYieldType,
57
+ TReturnType = void,
58
+ TResHeaders = undefined,
59
+ TErrorKey extends string = string,
60
+ >(
61
+ name: TName,
62
+ config: {
63
+ path: string
64
+ method: HttpMethod
65
+ scope?: string
66
+ errors?: TErrorKey[]
67
+ description?: string
68
+ schema: {
69
+ req?: TReq
70
+ yield?: TYieldType
71
+ returnType?: TReturnType
72
+ res?: TResHeaders extends undefined ? undefined : { headers: TResHeaders }
73
+ }
74
+ validateYields?: boolean
75
+ },
76
+ handler: TResHeaders extends undefined
77
+ ? (
78
+ ctx: Prettify<TContext & TStreamContext>,
79
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
80
+ ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
81
+ : (
82
+ ctx: Prettify<TContext & TStreamContext>,
83
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
84
+ ) => Promise<{ headers: TSchemaLib<TResHeaders>; stream: AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown> }>,
85
+ ) {
86
+ const definitionInfo = captureDefinitionInfo()
87
+
88
+ if (procedures.has(name)) throw new Error(`Procedure with name ${name} is already registered`)
89
+ if ((config.schema as any)?.params) {
90
+ throw new ProcedureRegistrationError(name, `Use schema.req.body (or schema.req.query) instead of schema.params on CreateHttpStream. Procedure: "${name}".`, definitionInfo)
91
+ }
92
+
93
+ const { jsonSchema, validations } = computeSchema(name, {
94
+ req: config.schema.req as Record<string, unknown> | undefined,
95
+ yieldType: config.schema.yield,
96
+ returnType: config.schema.returnType,
97
+ }, definitionInfo)
98
+
99
+ const pathParamsSchema = (jsonSchema.req as Record<string, unknown> | undefined)?.pathParams
100
+ checkPathParamConsistency(name, config.path, pathParamsSchema as Record<string, unknown> | undefined, definitionInfo)
101
+
102
+ const errorFactory = (message: string, meta?: object) => new ProcedureError(name, message, meta, definitionInfo)
103
+ const validateYields = config.validateYields ?? false
104
+
105
+ const wrappedHandler = async (ctx: TContext, req: any): Promise<{ stream: AsyncGenerator<any, any, unknown>; initialHeaders?: Record<string, string> }> => {
106
+ const abortController = new AbortController()
107
+ const skipValidation = (ctx as { isPrevalidated?: boolean }).isPrevalidated || builder?.config?.noRuntimeValidation
108
+
109
+ if (validations?.req && !skipValidation) {
110
+ for (const [channel, validator] of Object.entries(validations.req)) {
111
+ const channelValue = (req as Record<string, unknown>)?.[channel]
112
+ const { errors } = validator(channelValue)
113
+ if (errors) {
114
+ throw new ProcedureValidationError(name, `Validation error for ${name} in req.${channel}`, errors, definitionInfo)
115
+ }
116
+ }
117
+ }
118
+
119
+ const incomingSignal = (ctx as { signal?: AbortSignal }).signal
120
+ const signal = incomingSignal ? AbortSignal.any([incomingSignal, abortController.signal]) : abortController.signal
121
+ const streamCtx: TStreamContext = { error: errorFactory, signal }
122
+
123
+ const handlerResult: any = (handler as any)({ ...ctx, ...streamCtx }, req)
124
+
125
+ if (typeof handlerResult?.[Symbol.asyncIterator] === 'function') {
126
+ // Plain async generator from user
127
+ const userIterator = (handlerResult as AsyncGenerator<any, any, unknown>)[Symbol.asyncIterator]()
128
+ return { stream: yieldingWrapper(userIterator, validateYields, validations, abortController, definitionInfo, name) }
129
+ } else if (typeof handlerResult?.then === 'function') {
130
+ // Async preamble: Promise<{ headers, stream }>
131
+ const resolved = await handlerResult
132
+ if (!resolved || typeof resolved !== 'object' || !('stream' in resolved)) {
133
+ throw new ProcedureError(name, `CreateHttpStream handler returned a Promise that did not resolve to { headers, stream }. Got: ${JSON.stringify(resolved)}`, undefined, definitionInfo)
134
+ }
135
+ const userIterator = (resolved.stream as AsyncGenerator<any, any, unknown>)[Symbol.asyncIterator]()
136
+ return {
137
+ stream: yieldingWrapper(userIterator, validateYields, validations, abortController, definitionInfo, name),
138
+ initialHeaders: resolved.headers,
139
+ }
140
+ } else {
141
+ throw new ProcedureError(name, `CreateHttpStream handler must return an AsyncGenerator or Promise<{ headers, stream }>.`, undefined, definitionInfo)
142
+ }
143
+ }
144
+
145
+ const registeredProcedure: THttpStreamProcedureRegistration<TContext> = {
146
+ name,
147
+ kind: 'http-stream',
148
+ config: {
149
+ path: config.path,
150
+ method: config.method,
151
+ scope: config.scope,
152
+ errors: config.errors as string[] | undefined,
153
+ description: config.description,
154
+ schema: jsonSchema as any,
155
+ validation: { req: validations.req, yield: validations.yield },
156
+ validateYields,
157
+ },
158
+ handler: wrappedHandler as any,
159
+ }
160
+
161
+ procedures.set(name, registeredProcedure as any)
162
+ builder?.onCreate?.(registeredProcedure)
163
+
164
+ const info = { name, kind: 'http-stream' as const, ...registeredProcedure.config }
165
+
166
+ // Explicit return type — preserves TReq/TYieldType/TReturnType through the public callable.
167
+ // Do NOT use `typeof registeredProcedure.handler` — that erases generics to `any`.
168
+ return {
169
+ [name]: registeredProcedure.handler,
170
+ procedure: registeredProcedure.handler,
171
+ info,
172
+ } as {
173
+ [K in TName]: (
174
+ ctx: Prettify<TContext>,
175
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
176
+ ) => Promise<{
177
+ stream: AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
178
+ initialHeaders?: Record<string, string>
179
+ }>
180
+ } & {
181
+ procedure: (
182
+ ctx: Prettify<TContext>,
183
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
184
+ ) => Promise<{
185
+ stream: AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
186
+ initialHeaders?: Record<string, string>
187
+ }>
188
+ info: typeof info
189
+ }
190
+ }
191
+ }