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