ts-procedures 7.3.0 → 8.1.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 (332) 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 +104 -53
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +205 -232
  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 +34 -48
  12. package/agent_config/cursor/cursorrules +34 -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 +418 -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 +351 -55
  39. package/build/codegen/emit-scope.js.map +1 -1
  40. package/build/codegen/emit-scope.test.js +540 -110
  41. package/build/codegen/emit-scope.test.js.map +1 -1
  42. package/build/codegen/emit-types.d.ts +6 -2
  43. package/build/codegen/emit-types.js +81 -20
  44. package/build/codegen/emit-types.js.map +1 -1
  45. package/build/codegen/emit-types.test.js +70 -1
  46. package/build/codegen/emit-types.test.js.map +1 -1
  47. package/build/codegen/pipeline.test.js +7 -7
  48. package/build/codegen/pipeline.test.js.map +1 -1
  49. package/build/codegen/resolve-envelope.js +1 -1
  50. package/build/codegen/resolve-envelope.js.map +1 -1
  51. package/build/codegen/resolve-envelope.test.js +5 -5
  52. package/build/codegen/resolve-envelope.test.js.map +1 -1
  53. package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
  54. package/build/codegen/targets/_shared/route-slots.js +49 -8
  55. package/build/codegen/targets/_shared/route-slots.js.map +1 -1
  56. package/build/codegen/targets/_shared/route-slots.test.js +99 -26
  57. package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
  58. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
  59. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
  60. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
  61. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
  62. package/build/codegen/targets/kotlin/integration.test.js +6 -0
  63. package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
  64. package/build/codegen/targets/swift/access-level.test.js +8 -11
  65. package/build/codegen/targets/swift/access-level.test.js.map +1 -1
  66. package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
  67. package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
  68. package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
  69. package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
  70. package/build/codegen/targets/swift/integration.test.js +6 -0
  71. package/build/codegen/targets/swift/integration.test.js.map +1 -1
  72. package/build/create-http-stream.d.ts +58 -0
  73. package/build/create-http-stream.js +122 -0
  74. package/build/create-http-stream.js.map +1 -0
  75. package/build/create-http-stream.test.js +88 -0
  76. package/build/create-http-stream.test.js.map +1 -0
  77. package/build/create-http.d.ts +49 -0
  78. package/build/create-http.js +108 -0
  79. package/build/create-http.js.map +1 -0
  80. package/build/create-http.test.js +137 -0
  81. package/build/create-http.test.js.map +1 -0
  82. package/build/create-stream.d.ts +35 -0
  83. package/build/create-stream.js +123 -0
  84. package/build/create-stream.js.map +1 -0
  85. package/build/create-stream.test.js +428 -0
  86. package/build/create-stream.test.js.map +1 -0
  87. package/build/create.d.ts +28 -0
  88. package/build/create.js +82 -0
  89. package/build/create.js.map +1 -0
  90. package/build/create.test.js +483 -0
  91. package/build/create.test.js.map +1 -0
  92. package/build/exports.d.ts +2 -0
  93. package/build/implementations/http/astro/index.test.js +20 -12
  94. package/build/implementations/http/astro/index.test.js.map +1 -1
  95. package/build/implementations/http/doc-registry.js +1 -1
  96. package/build/implementations/http/doc-registry.js.map +1 -1
  97. package/build/implementations/http/doc-registry.test.js +36 -5
  98. package/build/implementations/http/doc-registry.test.js.map +1 -1
  99. package/build/implementations/http/error-dispatch.d.ts +76 -0
  100. package/build/implementations/http/error-dispatch.js +77 -0
  101. package/build/implementations/http/error-dispatch.js.map +1 -0
  102. package/build/implementations/http/error-dispatch.test.js +254 -0
  103. package/build/implementations/http/error-dispatch.test.js.map +1 -0
  104. package/build/implementations/http/error-taxonomy.d.ts +5 -5
  105. package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
  106. package/build/implementations/http/hono/docs/http-doc.js +42 -0
  107. package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
  108. package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
  109. package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
  110. package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
  111. package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
  112. package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
  113. package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
  114. package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
  115. package/build/implementations/http/hono/docs/stream-doc.js +42 -0
  116. package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
  117. package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
  118. package/build/implementations/http/hono/handlers/http-stream.js +123 -0
  119. package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
  120. package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
  121. package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
  122. package/build/implementations/http/hono/handlers/http.d.ts +10 -0
  123. package/build/implementations/http/hono/handlers/http.js +115 -0
  124. package/build/implementations/http/hono/handlers/http.js.map +1 -0
  125. package/build/implementations/http/hono/handlers/http.test.js +118 -0
  126. package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
  127. package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
  128. package/build/implementations/http/hono/handlers/rpc.js +32 -0
  129. package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
  130. package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
  131. package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
  132. package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
  133. package/build/implementations/http/hono/handlers/stream.js +147 -0
  134. package/build/implementations/http/hono/handlers/stream.js.map +1 -0
  135. package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
  136. package/build/implementations/http/hono/handlers/stream.test.js +177 -0
  137. package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
  138. package/build/implementations/http/hono/index.d.ts +57 -0
  139. package/build/implementations/http/hono/index.js +149 -0
  140. package/build/implementations/http/hono/index.js.map +1 -0
  141. package/build/implementations/http/hono/index.test.d.ts +1 -0
  142. package/build/implementations/http/hono/index.test.js +274 -0
  143. package/build/implementations/http/hono/index.test.js.map +1 -0
  144. package/build/implementations/http/hono/path.d.ts +17 -0
  145. package/build/implementations/http/hono/path.js +39 -0
  146. package/build/implementations/http/hono/path.js.map +1 -0
  147. package/build/implementations/http/hono/path.test.d.ts +1 -0
  148. package/build/implementations/http/hono/path.test.js +83 -0
  149. package/build/implementations/http/hono/path.test.js.map +1 -0
  150. package/build/implementations/http/hono/types.d.ts +51 -0
  151. package/build/implementations/http/hono/types.js.map +1 -0
  152. package/build/implementations/http/on-request-error.test.js +6 -96
  153. package/build/implementations/http/on-request-error.test.js.map +1 -1
  154. package/build/implementations/http/route-errors.test.js +11 -59
  155. package/build/implementations/http/route-errors.test.js.map +1 -1
  156. package/build/implementations/types.d.ts +43 -9
  157. package/build/index.d.ts +124 -124
  158. package/build/index.js +10 -221
  159. package/build/index.js.map +1 -1
  160. package/build/index.test.js +20 -919
  161. package/build/index.test.js.map +1 -1
  162. package/build/migration.test.d.ts +1 -0
  163. package/build/migration.test.js +34 -0
  164. package/build/migration.test.js.map +1 -0
  165. package/build/schema/compute-schema.d.ts +11 -3
  166. package/build/schema/compute-schema.js +13 -7
  167. package/build/schema/compute-schema.js.map +1 -1
  168. package/build/schema/parser.d.ts +11 -3
  169. package/build/schema/parser.js +49 -9
  170. package/build/schema/parser.js.map +1 -1
  171. package/build/stack-utils.js +8 -0
  172. package/build/stack-utils.js.map +1 -1
  173. package/build/types.d.ts +142 -0
  174. package/build/types.js.map +1 -0
  175. package/docs/astro-adapter.md +5 -5
  176. package/docs/core.md +15 -17
  177. package/docs/http-integrations.md +83 -170
  178. package/docs/streaming.md +3 -60
  179. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
  180. package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
  181. package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
  182. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
  183. package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
  184. package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
  185. package/package.json +4 -22
  186. package/src/client/call.test.ts +26 -0
  187. package/src/client/call.ts +4 -1
  188. package/src/client/fetch-adapter.test.ts +14 -1
  189. package/src/client/fetch-adapter.ts +3 -1
  190. package/src/client/index.test.ts +7 -7
  191. package/src/client/request-builder.ts +2 -2
  192. package/src/client/stream.test.ts +39 -7
  193. package/src/client/stream.ts +16 -2
  194. package/src/client/typed-error-dispatch.test.ts +7 -97
  195. package/src/client/types.ts +21 -3
  196. package/src/codegen/__fixtures__/users-envelope.json +119 -38
  197. package/src/codegen/e2e.test.ts +452 -24
  198. package/src/codegen/emit-errors.integration.test.ts +1 -1
  199. package/src/codegen/emit-scope.test.ts +581 -110
  200. package/src/codegen/emit-scope.ts +390 -61
  201. package/src/codegen/emit-types.test.ts +73 -1
  202. package/src/codegen/emit-types.ts +82 -21
  203. package/src/codegen/pipeline.test.ts +7 -7
  204. package/src/codegen/resolve-envelope.test.ts +5 -5
  205. package/src/codegen/resolve-envelope.ts +1 -1
  206. package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
  207. package/src/codegen/targets/_shared/route-slots.ts +48 -11
  208. package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
  209. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
  210. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
  211. package/src/codegen/targets/kotlin/integration.test.ts +19 -0
  212. package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
  213. package/src/codegen/targets/swift/access-level.test.ts +8 -11
  214. package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
  215. package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
  216. package/src/codegen/targets/swift/integration.test.ts +17 -0
  217. package/src/create-http-stream.test.ts +97 -0
  218. package/src/create-http-stream.ts +191 -0
  219. package/src/create-http.test.ts +163 -0
  220. package/src/create-http.ts +211 -0
  221. package/src/create-stream.test.ts +565 -0
  222. package/src/create-stream.ts +228 -0
  223. package/src/create.test.ts +658 -0
  224. package/src/create.ts +172 -0
  225. package/src/exports.ts +2 -0
  226. package/src/implementations/http/README.md +135 -95
  227. package/src/implementations/http/astro/README.md +4 -5
  228. package/src/implementations/http/astro/index.test.ts +25 -18
  229. package/src/implementations/http/doc-registry.test.ts +42 -5
  230. package/src/implementations/http/doc-registry.ts +1 -1
  231. package/src/implementations/http/error-dispatch.test.ts +283 -0
  232. package/src/implementations/http/error-dispatch.ts +176 -0
  233. package/src/implementations/http/error-taxonomy.ts +5 -5
  234. package/src/implementations/http/hono/docs/http-doc.ts +43 -0
  235. package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
  236. package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
  237. package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
  238. package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
  239. package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
  240. package/src/implementations/http/hono/handlers/http.test.ts +130 -0
  241. package/src/implementations/http/hono/handlers/http.ts +147 -0
  242. package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
  243. package/src/implementations/http/hono/handlers/rpc.ts +54 -0
  244. package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
  245. package/src/implementations/http/hono/handlers/stream.ts +208 -0
  246. package/src/implementations/http/hono/index.test.ts +329 -0
  247. package/src/implementations/http/hono/index.ts +204 -0
  248. package/src/implementations/http/hono/path.test.ts +96 -0
  249. package/src/implementations/http/hono/path.ts +59 -0
  250. package/src/implementations/http/hono/types.ts +93 -0
  251. package/src/implementations/http/on-request-error.test.ts +10 -116
  252. package/src/implementations/http/route-errors.test.ts +11 -77
  253. package/src/implementations/types.ts +44 -9
  254. package/src/index.test.ts +22 -1249
  255. package/src/index.ts +49 -485
  256. package/src/migration.test.ts +48 -0
  257. package/src/schema/compute-schema.ts +26 -12
  258. package/src/schema/parser.ts +62 -12
  259. package/src/stack-utils.ts +8 -0
  260. package/src/types.ts +133 -0
  261. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
  262. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
  263. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
  264. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
  265. package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
  266. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
  267. package/build/implementations/http/express-rpc/index.d.ts +0 -125
  268. package/build/implementations/http/express-rpc/index.js +0 -216
  269. package/build/implementations/http/express-rpc/index.js.map +0 -1
  270. package/build/implementations/http/express-rpc/index.test.js +0 -684
  271. package/build/implementations/http/express-rpc/index.test.js.map +0 -1
  272. package/build/implementations/http/express-rpc/types.d.ts +0 -11
  273. package/build/implementations/http/express-rpc/types.js.map +0 -1
  274. package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
  275. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
  276. package/build/implementations/http/hono-api/index.d.ts +0 -151
  277. package/build/implementations/http/hono-api/index.js +0 -344
  278. package/build/implementations/http/hono-api/index.js.map +0 -1
  279. package/build/implementations/http/hono-api/index.test.js +0 -992
  280. package/build/implementations/http/hono-api/index.test.js.map +0 -1
  281. package/build/implementations/http/hono-api/types.d.ts +0 -13
  282. package/build/implementations/http/hono-api/types.js.map +0 -1
  283. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
  284. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
  285. package/build/implementations/http/hono-rpc/index.d.ts +0 -130
  286. package/build/implementations/http/hono-rpc/index.js +0 -209
  287. package/build/implementations/http/hono-rpc/index.js.map +0 -1
  288. package/build/implementations/http/hono-rpc/index.test.js +0 -828
  289. package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
  290. package/build/implementations/http/hono-rpc/types.d.ts +0 -11
  291. package/build/implementations/http/hono-rpc/types.js +0 -2
  292. package/build/implementations/http/hono-rpc/types.js.map +0 -1
  293. package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
  294. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
  295. package/build/implementations/http/hono-stream/index.d.ts +0 -171
  296. package/build/implementations/http/hono-stream/index.js +0 -415
  297. package/build/implementations/http/hono-stream/index.js.map +0 -1
  298. package/build/implementations/http/hono-stream/index.test.js +0 -1383
  299. package/build/implementations/http/hono-stream/index.test.js.map +0 -1
  300. package/build/implementations/http/hono-stream/types.d.ts +0 -15
  301. package/build/implementations/http/hono-stream/types.js +0 -2
  302. package/build/implementations/http/hono-stream/types.js.map +0 -1
  303. package/src/implementations/http/express-rpc/README.md +0 -280
  304. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
  305. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  306. package/src/implementations/http/express-rpc/index.ts +0 -327
  307. package/src/implementations/http/express-rpc/types.ts +0 -16
  308. package/src/implementations/http/hono-api/README.md +0 -284
  309. package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
  310. package/src/implementations/http/hono-api/index.test.ts +0 -1341
  311. package/src/implementations/http/hono-api/index.ts +0 -519
  312. package/src/implementations/http/hono-api/types.ts +0 -16
  313. package/src/implementations/http/hono-rpc/README.md +0 -357
  314. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
  315. package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
  316. package/src/implementations/http/hono-rpc/index.ts +0 -320
  317. package/src/implementations/http/hono-rpc/types.ts +0 -16
  318. package/src/implementations/http/hono-stream/README.md +0 -559
  319. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
  320. package/src/implementations/http/hono-stream/index.test.ts +0 -1804
  321. package/src/implementations/http/hono-stream/index.ts +0 -622
  322. package/src/implementations/http/hono-stream/types.ts +0 -20
  323. /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
  324. /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
  325. /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
  326. /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
  327. /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
  328. /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
  329. /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
  330. /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
  331. /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
  332. /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
@@ -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
+ }
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect, expectTypeOf } from 'vitest'
2
+ import { Procedures } from './index.js'
3
+ import { ProcedureRegistrationError, ProcedureValidationError } from './errors.js'
4
+ import { Type } from 'typebox'
5
+
6
+ describe('CreateHttp registration', () => {
7
+ it('registers with kind: http', () => {
8
+ const procs = Procedures()
9
+ procs.CreateHttp('GetUser', {
10
+ path: '/users/:id',
11
+ method: 'get',
12
+ schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
13
+ }, async (_, { pathParams }) => ({ id: pathParams.id }) as any)
14
+
15
+ const reg = procs.getProcedure('GetUser')
16
+ expect(reg?.kind).toBe('http')
17
+ })
18
+
19
+ it('rejects schema.params with v8 message', () => {
20
+ const procs = Procedures()
21
+ expect(() =>
22
+ procs.CreateHttp('Bad', {
23
+ path: '/x', method: 'get',
24
+ schema: { params: Type.Object({}) } as any,
25
+ }, async () => undefined)
26
+ ).toThrow(/Use schema.req.body \(or schema.req.query\) instead of schema.params/)
27
+ })
28
+
29
+ it('rejects path with params but no pathParams schema', () => {
30
+ const procs = Procedures()
31
+ expect(() =>
32
+ procs.CreateHttp('Bad', {
33
+ path: '/users/:id', method: 'get',
34
+ schema: { req: {} },
35
+ }, async () => undefined)
36
+ ).toThrow(/path parameters \[id\] but schema.req.pathParams is not defined/)
37
+ })
38
+
39
+ it('rejects pathParams schema with no path params', () => {
40
+ const procs = Procedures()
41
+ expect(() =>
42
+ procs.CreateHttp('Bad', {
43
+ path: '/users', method: 'get',
44
+ schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
45
+ }, async () => undefined)
46
+ ).toThrow(/has no path parameters/)
47
+ })
48
+
49
+ it('rejects pathParams key mismatch', () => {
50
+ const procs = Procedures()
51
+ expect(() =>
52
+ procs.CreateHttp('Bad', {
53
+ path: '/users/:id', method: 'get',
54
+ schema: { req: { pathParams: Type.Object({ wrongKey: Type.String() }) } },
55
+ }, async () => undefined)
56
+ ).toThrow(/Path param mismatch/)
57
+ })
58
+ })
59
+
60
+ describe('CreateHttp res.body', () => {
61
+ it('handler returns body bare when only res.body declared', async () => {
62
+ const procs = Procedures()
63
+ const { GetUser } = procs.CreateHttp('GetUser', {
64
+ path: '/users/:id', method: 'get',
65
+ schema: {
66
+ req: { pathParams: Type.Object({ id: Type.String() }) },
67
+ res: { body: Type.Object({ id: Type.String(), name: Type.String() }) },
68
+ },
69
+ }, async (_, { pathParams }) => ({ id: pathParams.id, name: 'Alice' }))
70
+
71
+ const result = await GetUser({} as any, { pathParams: { id: '1' } })
72
+ expect(result).toEqual({ id: '1', name: 'Alice' })
73
+ })
74
+ })
75
+
76
+ describe('CreateHttp conditional return shape', () => {
77
+ it('res: { body, headers } → returns { body, headers }', async () => {
78
+ const procs = Procedures()
79
+ const { GetUser } = procs.CreateHttp('GetUser', {
80
+ path: '/users/:id', method: 'get',
81
+ schema: {
82
+ req: { pathParams: Type.Object({ id: Type.String() }) },
83
+ res: {
84
+ body: Type.Object({ id: Type.String() }),
85
+ headers: Type.Object({ 'x-rate-limit': Type.String() }),
86
+ },
87
+ },
88
+ }, async (_, { pathParams }) => ({
89
+ body: { id: pathParams.id },
90
+ headers: { 'x-rate-limit': '99' },
91
+ }))
92
+
93
+ const result = await GetUser({} as any, { pathParams: { id: '1' } })
94
+ expect(result).toEqual({ body: { id: '1' }, headers: { 'x-rate-limit': '99' } })
95
+ })
96
+
97
+ it('res: { headers } only → returns { headers }', async () => {
98
+ const procs = Procedures()
99
+ const { Healthcheck } = procs.CreateHttp('Healthcheck', {
100
+ path: '/health', method: 'get',
101
+ schema: {
102
+ res: { headers: Type.Object({ 'x-version': Type.String() }) },
103
+ },
104
+ }, async () => ({ headers: { 'x-version': '8.0.0' } }))
105
+
106
+ const result = await Healthcheck({} as any, undefined)
107
+ expect(result).toEqual({ headers: { 'x-version': '8.0.0' } })
108
+ })
109
+
110
+ it('no res → returns void', async () => {
111
+ const procs = Procedures()
112
+ const { Ping } = procs.CreateHttp('Ping', {
113
+ path: '/ping', method: 'post',
114
+ schema: { req: { body: Type.Object({}) } },
115
+ }, async () => undefined)
116
+
117
+ const result = await Ping({} as any, { body: {} })
118
+ expect(result).toBeUndefined()
119
+ })
120
+ })
121
+
122
+ describe('CreateHttp HttpReturn type inference', () => {
123
+ it('infers correct return shapes', () => {
124
+ const procs = Procedures()
125
+ const { GetUser } = procs.CreateHttp('GetUser', {
126
+ path: '/users/:id', method: 'get',
127
+ schema: {
128
+ req: { pathParams: Type.Object({ id: Type.String() }) },
129
+ res: {
130
+ body: Type.Object({ id: Type.String() }),
131
+ headers: Type.Object({ 'x-rate-limit': Type.String() }),
132
+ },
133
+ },
134
+ }, async () => ({ body: { id: '1' }, headers: { 'x-rate-limit': '99' } }))
135
+
136
+ type ReturnT = Awaited<ReturnType<typeof GetUser>>
137
+ expectTypeOf<ReturnT>().toEqualTypeOf<{ body: { id: string }; headers: { 'x-rate-limit': string } }>()
138
+ })
139
+ })
140
+
141
+ describe('CreateHttp req channel validation', () => {
142
+ it('validates each channel independently with channel-tagged error', async () => {
143
+ const procs = Procedures()
144
+ const { GetUser } = procs.CreateHttp('GetUser', {
145
+ path: '/users/:id', method: 'get',
146
+ schema: {
147
+ req: {
148
+ pathParams: Type.Object({ id: Type.String() }),
149
+ query: Type.Object({ limit: Type.Number() }),
150
+ },
151
+ },
152
+ }, async (_, { pathParams, query }) => ({ id: pathParams.id, limit: query.limit }) as any)
153
+
154
+ await expect(GetUser({} as any, {
155
+ pathParams: { id: 'abc' },
156
+ query: { limit: 'not-a-number' as any },
157
+ })).rejects.toThrow(ProcedureValidationError)
158
+ await expect(GetUser({} as any, {
159
+ pathParams: { id: 'abc' },
160
+ query: { limit: 'not-a-number' as any },
161
+ })).rejects.toThrow(/req\.query/)
162
+ })
163
+ })
@@ -0,0 +1,211 @@
1
+ import { ProcedureError, ProcedureRegistrationError, ProcedureValidationError } 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,
7
+ TBuilderConfig,
8
+ TCreateHttpConfig,
9
+ TLocalContext,
10
+ THttpProcedureRegistration,
11
+ THttpStreamProcedureRegistration,
12
+ TProcedureRegistration,
13
+ TStreamProcedureRegistration,
14
+ } from './types.js'
15
+
16
+ type Infer<T> = TSchemaLib<T>
17
+
18
+ export type HttpReturn<TRes> =
19
+ TRes extends { body: infer B; headers: infer H } ? { body: Infer<B>; headers: Infer<H> }
20
+ : TRes extends { headers: infer H } ? { headers: Infer<H> }
21
+ : TRes extends { body: infer B } ? Infer<B>
22
+ : void
23
+
24
+ const PATH_PARAM_RE = /:([a-zA-Z_][a-zA-Z0-9_]*)/g
25
+
26
+ function extractPathParamNames(path: string): string[] {
27
+ const matches = path.match(PATH_PARAM_RE)
28
+ return matches ? matches.map((m) => m.slice(1)) : []
29
+ }
30
+
31
+ export function checkPathParamConsistency(
32
+ procedureName: string,
33
+ path: string,
34
+ pathParamsSchema: Record<string, unknown> | undefined,
35
+ definitionInfo: ReturnType<typeof captureDefinitionInfo>,
36
+ ) {
37
+ const pathParamNames = extractPathParamNames(path)
38
+ const hasPathParams = pathParamNames.length > 0
39
+ const hasSchema = pathParamsSchema !== undefined
40
+
41
+ if (hasPathParams && !hasSchema) {
42
+ throw new ProcedureRegistrationError(
43
+ procedureName,
44
+ `Path "${path}" has path parameters [${pathParamNames.join(', ')}] but schema.req.pathParams is not defined.`,
45
+ definitionInfo,
46
+ )
47
+ }
48
+ if (!hasPathParams && hasSchema) {
49
+ throw new ProcedureRegistrationError(
50
+ procedureName,
51
+ `schema.req.pathParams is defined but path "${path}" has no path parameters.`,
52
+ definitionInfo,
53
+ )
54
+ }
55
+ if (hasPathParams && hasSchema) {
56
+ const schemaProperties = (pathParamsSchema as { properties?: Record<string, unknown> }).properties
57
+ if (schemaProperties) {
58
+ const schemaKeys = Object.keys(schemaProperties)
59
+ const missing = pathParamNames.filter((p) => !schemaKeys.includes(p))
60
+ const extra = schemaKeys.filter((k) => !pathParamNames.includes(k))
61
+ if (missing.length > 0 || extra.length > 0) {
62
+ const parts: string[] = []
63
+ if (missing.length > 0) parts.push(`path has [${missing.join(', ')}] missing from schema`)
64
+ if (extra.length > 0) parts.push(`schema has [${extra.join(', ')}] not in path`)
65
+ throw new ProcedureRegistrationError(
66
+ procedureName,
67
+ `Path param mismatch for "${procedureName}": ${parts.join('; ')}.`,
68
+ definitionInfo,
69
+ )
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ export function makeCreateHttp<TContext>(
76
+ procedures: Map<
77
+ string,
78
+ | TProcedureRegistration<TContext, any>
79
+ | TStreamProcedureRegistration<TContext, any>
80
+ | THttpProcedureRegistration<TContext>
81
+ | THttpStreamProcedureRegistration<TContext>
82
+ >,
83
+ builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
84
+ ) {
85
+ return function CreateHttp<
86
+ TName extends string,
87
+ TReq extends Record<string, unknown> | undefined,
88
+ TRes extends { body?: unknown; headers?: unknown } | undefined = undefined,
89
+ TErrorKey extends string = string,
90
+ >(
91
+ name: TName,
92
+ config: TCreateHttpConfig<TReq, TRes, TErrorKey>,
93
+ handler: (
94
+ ctx: Prettify<TContext & TLocalContext>,
95
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
96
+ ) => Promise<HttpReturn<TRes>>,
97
+ ) {
98
+ const definitionInfo = captureDefinitionInfo()
99
+
100
+ if (procedures.has(name)) {
101
+ throw new Error(`Procedure with name ${name} is already registered`)
102
+ }
103
+
104
+ if ((config.schema as any)?.params) {
105
+ throw new ProcedureRegistrationError(
106
+ name,
107
+ `Use schema.req.body (or schema.req.query) instead of schema.params on CreateHttp. Procedure: "${name}".`,
108
+ definitionInfo,
109
+ )
110
+ }
111
+
112
+ const { jsonSchema, validations } = computeSchema(
113
+ name,
114
+ {
115
+ req: config.schema.req as Record<string, unknown> | undefined,
116
+ res: config.schema.res as { body?: unknown; headers?: unknown } | undefined,
117
+ },
118
+ definitionInfo,
119
+ )
120
+
121
+ const pathParamsSchema = (jsonSchema.req as Record<string, unknown> | undefined)?.pathParams
122
+ checkPathParamConsistency(
123
+ name,
124
+ config.path,
125
+ pathParamsSchema as Record<string, unknown> | undefined,
126
+ definitionInfo,
127
+ )
128
+
129
+ const errorFactory = (message: string, meta?: object) =>
130
+ new ProcedureError(name, message, meta, definitionInfo)
131
+
132
+ const registeredProcedure: THttpProcedureRegistration<TContext> = {
133
+ name,
134
+ kind: 'http',
135
+ config: {
136
+ path: config.path,
137
+ method: config.method,
138
+ successStatus: config.successStatus,
139
+ scope: config.scope,
140
+ errors: config.errors as string[] | undefined,
141
+ description: config.description,
142
+ schema: jsonSchema as any,
143
+ validation: { req: validations.req },
144
+ },
145
+ handler: async (ctx: TContext, req: any) => {
146
+ try {
147
+ const skipValidation =
148
+ (ctx as { isPrevalidated?: boolean }).isPrevalidated ||
149
+ builder?.config?.noRuntimeValidation
150
+
151
+ if (validations?.req && !skipValidation) {
152
+ for (const [channel, validator] of Object.entries(validations.req)) {
153
+ const channelValue = (req as Record<string, unknown>)?.[channel]
154
+ const { errors } = validator(channelValue)
155
+ if (errors) {
156
+ throw new ProcedureValidationError(
157
+ name,
158
+ `Validation error for ${name} in req.${channel}`,
159
+ errors,
160
+ definitionInfo,
161
+ )
162
+ }
163
+ }
164
+ }
165
+
166
+ const localCtx: TLocalContext = { error: errorFactory }
167
+ return await handler({ ...ctx, ...localCtx } as any, req)
168
+ } catch (error: any) {
169
+ if (error instanceof ProcedureError) throw error
170
+ const err = new ProcedureError(
171
+ name,
172
+ `Error in handler for ${name} - ${error?.message}`,
173
+ undefined,
174
+ definitionInfo,
175
+ )
176
+ err.cause = error
177
+ if (error.stack && definitionInfo.definedAt) {
178
+ const { file, line, column } = definitionInfo.definedAt
179
+ err.stack =
180
+ error.stack + `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
181
+ } else if (error.stack) {
182
+ err.stack = error.stack
183
+ }
184
+ throw err
185
+ }
186
+ },
187
+ }
188
+
189
+ procedures.set(name, registeredProcedure as any)
190
+ builder?.onCreate?.(registeredProcedure)
191
+
192
+ const info = { name, ...registeredProcedure.config }
193
+
194
+ return {
195
+ [name]: registeredProcedure.handler,
196
+ procedure: registeredProcedure.handler,
197
+ info,
198
+ } as {
199
+ [K in TName]: (
200
+ ctx: Prettify<TContext>,
201
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
202
+ ) => Promise<HttpReturn<TRes>>
203
+ } & {
204
+ procedure: (
205
+ ctx: Prettify<TContext>,
206
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
207
+ ) => Promise<HttpReturn<TRes>>
208
+ info: typeof info
209
+ }
210
+ }
211
+ }