ts-procedures 7.3.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 +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 +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 +124 -124
  153. package/build/index.js +10 -221
  154. package/build/index.js.map +1 -1
  155. package/build/index.test.js +20 -919
  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 +15 -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 +22 -1249
  248. package/src/index.ts +49 -485
  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
@@ -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
  describe('Procedures', () => {
8
7
  test('Procedures', () => {
9
8
  const result = Procedures({
@@ -13,188 +12,6 @@ describe('Procedures', () => {
13
12
  });
14
13
  expect(result).toHaveProperty('Create');
15
14
  });
16
- test('Procedures generic context & extended config', () => {
17
- const { Create } = Procedures();
18
- const { info } = Create('TestProcedure', {
19
- // should not throw type errors
20
- customProp: 'customProp',
21
- }, async (ctx) => {
22
- // should not throw type errors
23
- return ctx.authToken;
24
- });
25
- expect(info.customProp).toEqual('customProp');
26
- expect(info.optionalProp).toEqual(undefined);
27
- });
28
- test('Create Single Procedures', () => {
29
- const { procedure: procedure1, info: info1 } = Procedures().Create('test1', {}, async () => {
30
- return '1';
31
- });
32
- const { procedure: procedure2, info: info2 } = Procedures().Create('test2', {}, async () => {
33
- return '2';
34
- });
35
- expect(procedure1).toBeDefined();
36
- expect(info1).toBeDefined();
37
- expect(procedure2).toBeDefined();
38
- expect(info2).toBeDefined();
39
- });
40
- test('Procedures - Create call', () => new Promise((done) => {
41
- let mockHttpCall;
42
- const { Create } = Procedures({
43
- onCreate: ({ handler }) => {
44
- mockHttpCall = handler;
45
- },
46
- });
47
- Create('Handler', {
48
- schema: {
49
- params: v.object({ name: v.string() }),
50
- returnType: v.string(),
51
- },
52
- }, async (ctx, params) => {
53
- expect(params).toEqual({ name: 'name' });
54
- done();
55
- return 'name';
56
- });
57
- mockHttpCall({}, { name: 'name' });
58
- }));
59
- test('Procedures - Create call w/ Typebox', () => new Promise((done) => {
60
- let mockHttpCall;
61
- const { Create } = Procedures({
62
- onCreate: ({ handler, config, name }) => {
63
- mockHttpCall = handler;
64
- },
65
- });
66
- Create('Handler', {
67
- schema: {
68
- params: Type.Object({ name: Type.Optional(Type.String()) }),
69
- returnType: Type.String(),
70
- },
71
- }, async (ctx, params) => {
72
- expect(params).toEqual({ name: 'name' });
73
- done();
74
- return 'name';
75
- });
76
- mockHttpCall({}, { name: 'name' });
77
- }));
78
- test('Procedures - Create returns a handler to call/test the Procedure registration', async () => {
79
- let ProcedureRegisteredCbHandler;
80
- const { Create } = Procedures({
81
- onCreate: (Procedure) => {
82
- ProcedureRegisteredCbHandler = Procedure.handler;
83
- },
84
- });
85
- const { NamedExportHandler, procedure, info } = Create('NamedExportHandler', {
86
- description: 'Handler description',
87
- schema: {
88
- params: v.object({ number: v.number() }),
89
- },
90
- }, async (ctx, params) => {
91
- return params.number;
92
- });
93
- expect(NamedExportHandler).toBeDefined();
94
- expect(procedure).toBeDefined();
95
- expect(ProcedureRegisteredCbHandler).toEqual(NamedExportHandler);
96
- expect(ProcedureRegisteredCbHandler).toEqual(procedure);
97
- const result = NamedExportHandler({}, { number: 1 });
98
- expect(result).toBeDefined();
99
- expect(result).toBeInstanceOf(Promise);
100
- await expect(result).resolves.toEqual(1);
101
- expect(info).toBeDefined();
102
- expect(info).toBeInstanceOf(Object);
103
- expect(info.schema).toHaveProperty('params');
104
- expect(info.schema.params).toEqual({
105
- type: 'object',
106
- properties: { number: { type: 'number' } },
107
- });
108
- expect(info).toHaveProperty('description');
109
- expect(info.description).toEqual('Handler description');
110
- });
111
- test('Procedures - Create params validation w/ no params provided', () => new Promise((done) => {
112
- let mockHttpCall;
113
- const { Create } = Procedures({
114
- onCreate: ({ handler, config, name }) => {
115
- mockHttpCall = (callParams) => {
116
- if (config.validation?.params) {
117
- const { errors } = config.validation.params(callParams);
118
- if (errors && 'message' in errors[0]) {
119
- expect(errors[0].message).toEqual('must be object');
120
- done();
121
- return;
122
- }
123
- }
124
- handler(callParams, {});
125
- };
126
- },
127
- });
128
- Create('test', {
129
- schema: {
130
- params: v.object({}),
131
- },
132
- }, async () => {
133
- done();
134
- });
135
- mockHttpCall();
136
- }));
137
- test('Procedures - Create params validation w/ missing params', async () => new Promise((done) => {
138
- let mockHttpCall;
139
- const { Create } = Procedures({
140
- onCreate: async ({ handler, config, name }) => {
141
- mockHttpCall = async (callParams) => {
142
- if (config.validation?.params) {
143
- const { errors } = config.validation.params(callParams);
144
- expect(errors).toBeDefined();
145
- expect(errors?.length).toEqual(2);
146
- }
147
- try {
148
- await handler(callParams, {});
149
- }
150
- catch (e) {
151
- expect(e).instanceof(ProcedureValidationError);
152
- expect(e.errors.length).toEqual(2);
153
- done();
154
- }
155
- };
156
- },
157
- });
158
- Create('test', {
159
- schema: {
160
- params: v.object({
161
- name: v.string().required(),
162
- id: v.number().required(),
163
- email: v.string(),
164
- }),
165
- },
166
- }, async () => {
167
- return;
168
- });
169
- mockHttpCall({});
170
- }));
171
- test('Procedures - Create call provides ctx to handler', () => new Promise((done) => {
172
- let mockHttpCall;
173
- const { Create } = Procedures({
174
- onCreate: ({ handler }) => {
175
- mockHttpCall = () => handler({ testCtx: 'testCtx' });
176
- },
177
- });
178
- Create('test', {}, async (ctx, params) => {
179
- expect(ctx.testCtx).toEqual('testCtx');
180
- done();
181
- });
182
- mockHttpCall();
183
- }));
184
- test('Procedure handler can throw local ctx error and is caught', async () => {
185
- const { Create } = Procedures();
186
- const { TestProcedureHandlerError } = Create('TestProcedureHandlerError', {}, async (ctx) => {
187
- throw ctx.error('Local context error');
188
- });
189
- try {
190
- await TestProcedureHandlerError({}, {});
191
- }
192
- catch (e) {
193
- expect(e).toBeInstanceOf(ProcedureError);
194
- expect(e.message).toEqual('Local context error');
195
- expect(e.procedureName).toEqual('TestProcedureHandlerError');
196
- }
197
- });
198
15
  test('Procedures - getRegisteredProcedures', () => {
199
16
  const { Create, getProcedures } = Procedures({
200
17
  onCreate: () => {
@@ -227,24 +44,6 @@ describe('Procedures', () => {
227
44
  },
228
45
  });
229
46
  });
230
- test('Procedures - context() throws', async () => {
231
- const { Create } = Procedures();
232
- function validateAuthToken(token) {
233
- return token === 'valid-token';
234
- }
235
- const { CheckIsAuthenticated } = Create('CheckIsAuthenticated', {
236
- schema: {
237
- returnType: v.string(),
238
- },
239
- }, async (ctx) => {
240
- if (!validateAuthToken(ctx.authToken)) {
241
- throw ctx.error('Invalid auth token');
242
- }
243
- return 'User authentication is valid';
244
- });
245
- await expect(CheckIsAuthenticated({ authToken: 'valid-token' }, {})).resolves.toEqual('User authentication is valid');
246
- await expect(CheckIsAuthenticated({ authToken: 'not-valid-token' }, {})).rejects.toThrowError(ProcedureError);
247
- });
248
47
  test('Procedures - duplicate registration throws before schema computation', () => {
249
48
  const { Create } = Procedures();
250
49
  Create('DuplicateTest', {}, async () => 'first');
@@ -252,22 +51,6 @@ describe('Procedures', () => {
252
51
  Create('DuplicateTest', {}, async () => 'second');
253
52
  }).toThrow('Procedure with name DuplicateTest is already registered');
254
53
  });
255
- test('Procedures - wrapped errors preserve cause', async () => {
256
- const { Create } = Procedures();
257
- const originalError = new Error('Database connection failed');
258
- originalError.code = 'ECONNREFUSED';
259
- const { TestCause } = Create('TestCause', {}, async () => {
260
- throw originalError;
261
- });
262
- try {
263
- await TestCause({}, {});
264
- }
265
- catch (e) {
266
- expect(e).toBeInstanceOf(ProcedureError);
267
- expect(e.cause).toBe(originalError);
268
- expect(e.cause.code).toBe('ECONNREFUSED');
269
- }
270
- });
271
54
  test('Procedures - getProcedure returns specific procedure', () => {
272
55
  const { Create, getProcedure } = Procedures();
273
56
  Create('FindMe', {}, async () => 'found');
@@ -295,307 +78,7 @@ describe('Procedures', () => {
295
78
  clear();
296
79
  expect(getProcedures().length).toBe(0);
297
80
  });
298
- test('Procedures - ctx.error still works after optimization', async () => {
299
- const { Create } = Procedures();
300
- const { ErrorTest } = Create('ErrorTest', {}, async (ctx) => {
301
- throw ctx.error('Custom error message', { code: 'ERR_001' });
302
- });
303
- try {
304
- await ErrorTest({}, {});
305
- }
306
- catch (e) {
307
- expect(e).toBeInstanceOf(ProcedureError);
308
- expect(e.message).toBe('Custom error message');
309
- expect(e.procedureName).toBe('ErrorTest');
310
- expect(e.meta).toEqual({ code: 'ERR_001' });
311
- }
312
- });
313
- test('Create passes through external signal from context', async () => {
314
- const { Create } = Procedures();
315
- const externalAc = new AbortController();
316
- let capturedSignal = null;
317
- const { WithSignal } = Create('WithSignal', {}, async (ctx) => {
318
- capturedSignal = ctx.signal;
319
- return 'done';
320
- });
321
- await WithSignal({ signal: externalAc.signal }, {});
322
- expect(capturedSignal).toBe(externalAc.signal);
323
- expect(capturedSignal.aborted).toBe(false);
324
- });
325
- test('Create external signal reflects abort from caller', async () => {
326
- const { Create } = Procedures();
327
- const externalAc = new AbortController();
328
- let capturedSignal = null;
329
- const { AbortedSignal } = Create('AbortedSignal', {}, async (ctx) => {
330
- capturedSignal = ctx.signal;
331
- expect(ctx.signal.aborted).toBe(true);
332
- return 'done';
333
- });
334
- externalAc.abort();
335
- await AbortedSignal({ signal: externalAc.signal }, {});
336
- expect(capturedSignal.aborted).toBe(true);
337
- });
338
- test('Create external signal cancels in-flight async work', async () => {
339
- const { Create } = Procedures();
340
- const externalAc = new AbortController();
341
- let wasAbortedDuringWork = false;
342
- const ready = Promise.withResolvers();
343
- const { LongWork } = Create('LongWork', {}, async (ctx) => {
344
- ready.resolve();
345
- await new Promise((resolve) => {
346
- ctx.signal.addEventListener('abort', () => {
347
- wasAbortedDuringWork = true;
348
- resolve();
349
- });
350
- });
351
- return 'done';
352
- });
353
- const p = LongWork({ signal: externalAc.signal }, {});
354
- await ready.promise;
355
- externalAc.abort();
356
- await p;
357
- expect(wasAbortedDuringWork).toBe(true);
358
- });
359
- });
360
- describe('Procedures - Definition Location in Errors', () => {
361
- test('ProcedureValidationError includes definition location', async () => {
362
- const { Create } = Procedures();
363
- const { TestValidation } = Create('TestValidation', {
364
- schema: {
365
- params: v.object({ name: v.string().required() }),
366
- },
367
- }, async (ctx, params) => {
368
- return params.name;
369
- });
370
- try {
371
- // @ts-expect-error - intentionally passing invalid params
372
- await TestValidation({}, {}); // Missing required 'name' param
373
- }
374
- catch (e) {
375
- expect(e).toBeInstanceOf(ProcedureValidationError);
376
- expect(e.definedAt).toBeDefined();
377
- expect(e.definedAt.file).toContain('index.test.ts');
378
- expect(e.definedAt.line).toBeGreaterThan(0);
379
- expect(e.definedAt.column).toBeGreaterThan(0);
380
- expect(e.stack).toContain('--- Procedure "TestValidation" defined at ---');
381
- }
382
- });
383
- test('ctx.error() includes definition location', async () => {
384
- const { Create } = Procedures();
385
- const { TestCtxError } = Create('TestCtxError', {}, async (ctx) => {
386
- throw ctx.error('Custom error');
387
- });
388
- try {
389
- await TestCtxError({}, {});
390
- }
391
- catch (e) {
392
- expect(e).toBeInstanceOf(ProcedureError);
393
- expect(e.definedAt).toBeDefined();
394
- expect(e.definedAt.file).toContain('index.test.ts');
395
- expect(e.getDefinitionLocation()).toBeDefined();
396
- expect(e.stack).toContain('--- Procedure "TestCtxError" defined at ---');
397
- }
398
- });
399
- test('wrapped errors include definition location', async () => {
400
- const { Create } = Procedures();
401
- const { TestWrappedError } = Create('TestWrappedError', {}, async () => {
402
- throw new Error('Original error');
403
- });
404
- try {
405
- await TestWrappedError({}, {});
406
- }
407
- catch (e) {
408
- expect(e).toBeInstanceOf(ProcedureError);
409
- expect(e.definedAt).toBeDefined();
410
- expect(e.definedAt.file).toContain('index.test.ts');
411
- expect(e.message).toContain('Error in handler for TestWrappedError');
412
- expect(e.cause).toBeInstanceOf(Error);
413
- expect(e.cause.message).toBe('Original error');
414
- }
415
- });
416
- test('getDefinitionLocation returns formatted string', async () => {
417
- const { Create } = Procedures();
418
- const { TestGetLocation } = Create('TestGetLocation', {
419
- schema: {
420
- params: v.object({ id: v.number().required() }),
421
- },
422
- }, async (ctx, params) => {
423
- return params.id;
424
- });
425
- try {
426
- // @ts-expect-error - intentionally passing invalid params
427
- await TestGetLocation({}, {}); // Missing required 'id' param
428
- }
429
- catch (e) {
430
- const location = e.getDefinitionLocation();
431
- expect(location).toBeDefined();
432
- expect(location).toMatch(/index\.test\.ts:\d+:\d+/);
433
- }
434
- });
435
- test('error stack shows procedure definition location at the end', async () => {
436
- const { Create } = Procedures();
437
- const { TestStackFormat } = Create('TestStackFormat', {
438
- schema: {
439
- params: v.object({ value: v.string().required() }),
440
- },
441
- }, async (ctx, params) => {
442
- return params.value;
443
- });
444
- try {
445
- // @ts-expect-error - intentionally passing invalid params
446
- await TestStackFormat({}, {});
447
- }
448
- catch (e) {
449
- // Verify it's a validation error
450
- expect(e.name).toBe('ProcedureValidationError');
451
- expect(e).toBeInstanceOf(ProcedureValidationError);
452
- // Stack should contain the error message and definition location
453
- expect(e.stack).toContain('Validation error for TestStackFormat');
454
- expect(e.stack).toContain('--- Procedure "TestStackFormat" defined at ---');
455
- // The definition section should be at the end of the stack
456
- const stackLines = e.stack.split('\n');
457
- const definitionIndex = stackLines.findIndex((line) => line.includes('--- Procedure "TestStackFormat" defined at ---'));
458
- expect(definitionIndex).toBeGreaterThan(0);
459
- }
460
- });
461
- });
462
- describe('Streaming Procedures - CreateStream', () => {
463
- test('CreateStream creates a streaming procedure', async () => {
464
- const { CreateStream } = Procedures();
465
- const { StreamTest, procedure, info } = CreateStream('StreamTest', {
466
- description: 'Test streaming procedure',
467
- schema: {
468
- params: v.object({ count: v.number().required() }),
469
- yieldType: v.object({ value: v.number().required() }),
470
- },
471
- }, async function* (ctx, params) {
472
- for (let i = 0; i < params.count; i++) {
473
- yield { value: i };
474
- }
475
- });
476
- expect(StreamTest).toBeDefined();
477
- expect(procedure).toBeDefined();
478
- expect(info.isStream).toBe(true);
479
- expect(info.name).toBe('StreamTest');
480
- expect(info.description).toBe('Test streaming procedure');
481
- expect(info.schema.params).toEqual({
482
- type: 'object',
483
- properties: { count: { type: 'number' } },
484
- required: ['count'],
485
- });
486
- expect(info.schema.yieldType).toEqual({
487
- type: 'object',
488
- properties: { value: { type: 'number' } },
489
- required: ['value'],
490
- });
491
- // Collect yielded values
492
- const values = [];
493
- for await (const val of StreamTest({}, { count: 3 })) {
494
- values.push(val);
495
- }
496
- expect(values).toEqual([{ value: 0 }, { value: 1 }, { value: 2 }]);
497
- });
498
- test('CreateStream validates params', async () => {
499
- const { CreateStream } = Procedures();
500
- const { StreamWithParams } = CreateStream('StreamWithParams', {
501
- schema: {
502
- params: v.object({ name: v.string().required() }),
503
- yieldType: v.string(),
504
- },
505
- }, async function* (ctx, params) {
506
- yield params.name;
507
- });
508
- // Missing required param should throw ProcedureValidationError
509
- try {
510
- // @ts-expect-error - intentionally passing invalid params
511
- for await (const _val of StreamWithParams({}, {})) {
512
- // Should not reach here
513
- }
514
- expect.fail('Should have thrown');
515
- }
516
- catch (e) {
517
- expect(e).toBeInstanceOf(ProcedureValidationError);
518
- expect(e.message).toContain('Validation error for StreamWithParams');
519
- }
520
- });
521
- test('CreateStream with validateYields validates each yielded value', async () => {
522
- const { CreateStream } = Procedures();
523
- const { ProcedureYieldValidationError } = await import('./errors.js');
524
- const { StreamValidateYields } = CreateStream('StreamValidateYields', {
525
- schema: {
526
- yieldType: v.object({ id: v.number().required() }),
527
- },
528
- validateYields: true,
529
- }, async function* () {
530
- yield { id: 1 }; // Valid
531
- yield { id: 'not-a-number' }; // Invalid - should throw
532
- });
533
- const values = [];
534
- try {
535
- for await (const val of StreamValidateYields({}, {})) {
536
- values.push(val);
537
- }
538
- expect.fail('Should have thrown on invalid yield');
539
- }
540
- catch (e) {
541
- expect(e).toBeInstanceOf(ProcedureYieldValidationError);
542
- expect(e.message).toContain('Yield validation error for StreamValidateYields');
543
- expect(values).toEqual([{ id: 1 }]); // First value was valid
544
- }
545
- });
546
- test('CreateStream without validateYields does not validate yields', async () => {
547
- const { CreateStream } = Procedures();
548
- const { StreamNoValidate } = CreateStream('StreamNoValidate', {
549
- schema: {
550
- yieldType: v.object({ id: v.number().required() }),
551
- },
552
- // validateYields defaults to false
553
- }, async function* () {
554
- yield { id: 1 };
555
- yield { id: 'not-a-number' }; // Invalid but won't throw
556
- });
557
- const values = [];
558
- for await (const val of StreamNoValidate({}, {})) {
559
- values.push(val);
560
- }
561
- expect(values).toEqual([{ id: 1 }, { id: 'not-a-number' }]);
562
- });
563
- test('CreateStream ctx.error throws ProcedureError', async () => {
564
- const { CreateStream } = Procedures();
565
- const { StreamError } = CreateStream('StreamError', {}, async function* (ctx) {
566
- yield 'first';
567
- throw ctx.error('Custom stream error', { code: 'STREAM_ERR' });
568
- });
569
- const values = [];
570
- try {
571
- for await (const val of StreamError({}, {})) {
572
- values.push(val);
573
- }
574
- expect.fail('Should have thrown');
575
- }
576
- catch (e) {
577
- expect(e).toBeInstanceOf(ProcedureError);
578
- expect(e.message).toBe('Custom stream error');
579
- expect(e.procedureName).toBe('StreamError');
580
- expect(e.meta).toEqual({ code: 'STREAM_ERR' });
581
- expect(values).toEqual(['first']);
582
- }
583
- });
584
- test('CreateStream provides ctx.signal for abort handling', async () => {
585
- const { CreateStream } = Procedures();
586
- let signalReceived = false;
587
- const { StreamWithSignal } = CreateStream('StreamWithSignal', {}, async function* (ctx) {
588
- expect(ctx.signal).toBeDefined();
589
- expect(ctx.signal).toBeInstanceOf(AbortSignal);
590
- signalReceived = true;
591
- yield 'value';
592
- });
593
- for await (const _val of StreamWithSignal({}, {})) {
594
- // consume
595
- }
596
- expect(signalReceived).toBe(true);
597
- });
598
- test('CreateStream appears in getProcedures with isStream flag', () => {
81
+ test('CreateStream appears in getProcedures with kind: rpc-stream', () => {
599
82
  const { Create, CreateStream, getProcedures } = Procedures();
600
83
  Create('RegularProc', {}, async () => 'regular');
601
84
  CreateStream('StreamProc', {}, async function* () {
@@ -605,409 +88,11 @@ describe('Streaming Procedures - CreateStream', () => {
605
88
  expect(procs.length).toBe(2);
606
89
  const regular = procs.find((p) => p.name === 'RegularProc');
607
90
  const stream = procs.find((p) => p.name === 'StreamProc');
608
- expect(regular?.isStream).toBeUndefined();
609
- expect(stream?.isStream).toBe(true);
610
- });
611
- test('CreateStream onCreate callback receives isStream flag', () => new Promise((done) => {
612
- let receivedProc;
613
- const { CreateStream } = Procedures({
614
- onCreate: (proc) => {
615
- receivedProc = proc;
616
- },
617
- });
618
- CreateStream('OnCreateStream', {}, async function* () {
619
- yield 'test';
620
- });
621
- expect(receivedProc).toBeDefined();
622
- expect(receivedProc.isStream).toBe(true);
623
- expect(receivedProc.name).toBe('OnCreateStream');
624
- done();
625
- }));
626
- test('CreateStream duplicate registration throws', () => {
627
- const { CreateStream } = Procedures();
628
- CreateStream('DuplicateStream', {}, async function* () {
629
- yield 'first';
630
- });
631
- expect(() => {
632
- CreateStream('DuplicateStream', {}, async function* () {
633
- yield 'second';
634
- });
635
- }).toThrow('Procedure with name DuplicateStream is already registered');
636
- });
637
- test('CreateStream error includes definition location', async () => {
638
- const { CreateStream } = Procedures();
639
- const { StreamErrorLocation } = CreateStream('StreamErrorLocation', {
640
- schema: {
641
- params: v.object({ id: v.number().required() }),
642
- },
643
- }, async function* () {
644
- yield 'test';
645
- });
646
- try {
647
- // @ts-expect-error - intentionally passing invalid params
648
- for await (const _val of StreamErrorLocation({}, {})) {
649
- // consume
650
- }
651
- expect.fail('Should have thrown');
652
- }
653
- catch (e) {
654
- expect(e.definedAt).toBeDefined();
655
- expect(e.definedAt.file).toContain('index.test.ts');
656
- expect(e.stack).toContain('--- Procedure "StreamErrorLocation" defined at ---');
657
- }
658
- });
659
- test('CreateStream with Typebox schema', async () => {
660
- const { CreateStream } = Procedures();
661
- const { TypeboxStream } = CreateStream('TypeboxStream', {
662
- schema: {
663
- params: Type.Object({ limit: Type.Number() }),
664
- yieldType: Type.Object({ data: Type.String() }),
665
- },
666
- }, async function* (ctx, params) {
667
- for (let i = 0; i < params.limit; i++) {
668
- yield { data: `item-${i}` };
669
- }
670
- });
671
- const values = [];
672
- for await (const val of TypeboxStream({}, { limit: 2 })) {
673
- values.push(val);
674
- }
675
- expect(values).toEqual([{ data: 'item-0' }, { data: 'item-1' }]);
676
- });
677
- test('CreateStream with context type', async () => {
678
- const { CreateStream } = Procedures();
679
- const { ContextStream } = CreateStream('ContextStream', {}, async function* (ctx) {
680
- // ctx should have both userId and error
681
- expect(ctx.userId).toBe('user-123');
682
- expect(ctx.error).toBeDefined();
683
- expect(ctx.signal).toBeDefined();
684
- yield ctx.userId;
685
- });
686
- const values = [];
687
- for await (const val of ContextStream({ userId: 'user-123' }, {})) {
688
- values.push(val);
689
- }
690
- expect(values).toEqual(['user-123']);
691
- });
692
- test('CreateStream rethrows the original error preserving class identity', async () => {
693
- // The streaming wrapper must NOT box user errors inside ProcedureError —
694
- // doing so would defeat route-declared typed-error dispatch on the client
695
- // (the HTTP builder's taxonomy would see `ProcedureError` instead of the
696
- // user's class). Stack annotation is added in place; class identity and
697
- // custom properties are preserved.
698
- class MyDomainError extends Error {
699
- name = 'MyDomainError';
700
- code = 'STREAM_FAIL';
701
- }
702
- const { CreateStream } = Procedures();
703
- const originalError = new MyDomainError('Stream underlying error');
704
- const { StreamCause } = CreateStream('StreamCause', {},
705
- // eslint-disable-next-line require-yield
706
- async function* () {
707
- throw originalError;
708
- });
709
- try {
710
- for await (const _val of StreamCause({}, {})) {
711
- // consume
712
- }
713
- expect.fail('Should have thrown');
714
- }
715
- catch (e) {
716
- expect(e).toBe(originalError);
717
- expect(e).toBeInstanceOf(MyDomainError);
718
- expect(e.code).toBe('STREAM_FAIL');
719
- }
720
- });
721
- test('CreateStream propagates .return() to the user generator', async () => {
722
- // Consumers that close a stream early (via `iterator.return()` or breaking
723
- // out of for-await) must trigger the user generator's `finally` block so
724
- // cleanup (db handles, subscriptions, signal-driven teardown) runs.
725
- const { CreateStream } = Procedures();
726
- let finallyRan = false;
727
- const { EarlyClose } = CreateStream('EarlyClose', {}, async function* () {
728
- try {
729
- yield 1;
730
- yield 2;
731
- yield 3;
732
- }
733
- finally {
734
- finallyRan = true;
735
- }
736
- });
737
- const iter = EarlyClose({}, {});
738
- const first = await iter.next();
739
- expect(first.value).toBe(1);
740
- await iter.return(undefined);
741
- expect(finallyRan).toBe(true);
742
- });
743
- test('CreateStream with extended config', () => {
744
- const { CreateStream } = Procedures();
745
- const { info } = CreateStream('ExtendedStream', {
746
- scope: 'api',
747
- version: 1,
748
- description: 'Extended config stream',
749
- }, async function* () {
750
- yield 'data';
751
- });
752
- expect(info.scope).toBe('api');
753
- expect(info.version).toBe(1);
754
- expect(info.description).toBe('Extended config stream');
755
- expect(info.isStream).toBe(true);
756
- });
757
- test('CreateStream signal.aborted becomes true after generator completes', async () => {
758
- const { CreateStream } = Procedures();
759
- let capturedSignal = null;
760
- const { AbortStream } = CreateStream('AbortStream', {}, async function* (ctx) {
761
- capturedSignal = ctx.signal;
762
- yield 'value';
763
- });
764
- // Consume the generator
765
- for await (const _val of AbortStream({}, {})) {
766
- // consume
767
- }
768
- // After generator completes, signal should be aborted
769
- expect(capturedSignal).not.toBeNull();
770
- expect(capturedSignal.aborted).toBe(true);
771
- });
772
- test('CreateStream signal.reason is stream-completed after normal completion', async () => {
773
- const { CreateStream } = Procedures();
774
- let capturedSignal = null;
775
- const { ReasonStream } = CreateStream('ReasonStream', {}, async function* (ctx) {
776
- capturedSignal = ctx.signal;
777
- yield 'value';
778
- });
779
- for await (const _val of ReasonStream({}, {})) {
780
- // consume
781
- }
782
- expect(capturedSignal.reason).toBe('stream-completed');
783
- });
784
- test('CreateStream combines external signal with internal signal', async () => {
785
- const { CreateStream } = Procedures();
786
- const externalAc = new AbortController();
787
- let capturedSignal = null;
788
- const { CombinedStream } = CreateStream('CombinedStream', {}, async function* (ctx) {
789
- capturedSignal = ctx.signal;
790
- // Combined signal is a new object, not the raw external signal
791
- expect(ctx.signal).not.toBe(externalAc.signal);
792
- yield 'value';
793
- });
794
- // Abort external before consuming — combined signal should reflect it
795
- externalAc.abort('client-disconnected');
796
- for await (const _val of CombinedStream({ signal: externalAc.signal }, {})) {
797
- // consume
798
- }
799
- expect(capturedSignal.aborted).toBe(true);
800
- // Reason comes from external abort, not internal 'stream-completed'
801
- expect(capturedSignal.reason).toBe('client-disconnected');
802
- });
803
- });
804
- describe('isPrevalidated context property', () => {
805
- test('Create skips validation when ctx.isPrevalidated is true', async () => {
806
- const { Create } = Procedures();
807
- const { SkipValidation } = Create('SkipValidation', {
808
- schema: {
809
- params: v.object({ name: v.string().required() }),
810
- },
811
- }, async (ctx, params) => {
812
- return params;
813
- });
814
- // Without isPrevalidated, missing required param would throw
815
- // With isPrevalidated: true, validation is skipped
816
- const result = await SkipValidation({ isPrevalidated: true }, {});
817
- expect(result).toEqual({});
818
- });
819
- test('Create validates when ctx.isPrevalidated is false', async () => {
820
- const { Create } = Procedures();
821
- const { ValidateParams } = Create('ValidateParams', {
822
- schema: {
823
- params: v.object({ name: v.string().required() }),
824
- },
825
- }, async (ctx, params) => {
826
- return params;
827
- });
828
- // With isPrevalidated: false, validation should still run
829
- try {
830
- await ValidateParams({ isPrevalidated: false }, {});
831
- expect.fail('Should have thrown validation error');
832
- }
833
- catch (e) {
834
- expect(e).toBeInstanceOf(ProcedureValidationError);
835
- }
836
- });
837
- test('Create validates when ctx.isPrevalidated is undefined', async () => {
838
- const { Create } = Procedures();
839
- const { ValidateUndefined } = Create('ValidateUndefined', {
840
- schema: {
841
- params: v.object({ id: v.number().required() }),
842
- },
843
- }, async (ctx, params) => {
844
- return params;
845
- });
846
- // Without isPrevalidated property, validation should run
847
- try {
848
- await ValidateUndefined({}, {});
849
- expect.fail('Should have thrown validation error');
850
- }
851
- catch (e) {
852
- expect(e).toBeInstanceOf(ProcedureValidationError);
853
- }
854
- });
855
- test('CreateStream skips validation when ctx.isPrevalidated is true', async () => {
856
- const { CreateStream } = Procedures();
857
- const { StreamSkipValidation } = CreateStream('StreamSkipValidation', {
858
- schema: {
859
- params: v.object({ count: v.number().required() }),
860
- },
861
- }, async function* (ctx, params) {
862
- yield { received: params };
863
- });
864
- // With isPrevalidated: true, validation is skipped even with invalid params
865
- const values = [];
866
- for await (const val of StreamSkipValidation({ isPrevalidated: true }, {})) {
867
- values.push(val);
868
- }
869
- expect(values).toEqual([{ received: {} }]);
870
- });
871
- test('CreateStream validates when ctx.isPrevalidated is false', async () => {
872
- const { CreateStream } = Procedures();
873
- const { StreamValidate } = CreateStream('StreamValidate', {
874
- schema: {
875
- params: v.object({ count: v.number().required() }),
876
- },
877
- }, async function* (ctx, params) {
878
- yield { received: params };
879
- });
880
- // With isPrevalidated: false, validation should run
881
- try {
882
- for await (const _val of StreamValidate({ isPrevalidated: false }, {})) {
883
- // consume
884
- }
885
- expect.fail('Should have thrown validation error');
886
- }
887
- catch (e) {
888
- expect(e).toBeInstanceOf(ProcedureValidationError);
889
- }
890
- });
891
- test('CreateStream validates when ctx.isPrevalidated is undefined', async () => {
892
- const { CreateStream } = Procedures();
893
- const { StreamValidateUndefined } = CreateStream('StreamValidateUndefined', {
894
- schema: {
895
- params: v.object({ value: v.string().required() }),
896
- },
897
- }, async function* (ctx, params) {
898
- yield params;
899
- });
900
- // Without isPrevalidated property, validation should run
901
- try {
902
- for await (const _val of StreamValidateUndefined({}, {})) {
903
- // consume
904
- }
905
- expect.fail('Should have thrown validation error');
906
- }
907
- catch (e) {
908
- expect(e).toBeInstanceOf(ProcedureValidationError);
909
- }
910
- });
911
- test('isPrevalidated is not exposed in handler context type', async () => {
912
- const { Create } = Procedures();
913
- Create('CheckCtxType', {}, async (ctx) => {
914
- // @ts-expect-error - isPrevalidated should not be on handler context type
915
- void ctx.isPrevalidated;
916
- return 'done';
917
- });
91
+ expect(regular?.kind).toBe('rpc');
92
+ expect(stream?.kind).toBe('rpc-stream');
918
93
  });
919
94
  });
920
95
  describe('builder.config.noRuntimeValidation', () => {
921
- test('Create skips params validation when noRuntimeValidation is true', async () => {
922
- const { Create } = Procedures({ config: { noRuntimeValidation: true } });
923
- const { SkipParams } = Create('SkipParams', {
924
- schema: {
925
- params: Type.Object({ name: Type.String() }),
926
- },
927
- }, async (_ctx, params) => params);
928
- // Missing required `name` would normally throw ProcedureValidationError
929
- const result = await SkipParams({}, {});
930
- expect(result).toEqual({});
931
- });
932
- test('Create skips input channel validation when noRuntimeValidation is true', async () => {
933
- const { Create } = Procedures({ config: { noRuntimeValidation: true } });
934
- const { SkipInput } = Create('SkipInput', {
935
- schema: {
936
- input: {
937
- pathParams: Type.Object({ id: Type.String() }),
938
- body: Type.Object({ name: Type.String() }),
939
- },
940
- },
941
- }, async (_ctx, params) => params);
942
- // Both channels missing required fields — normally throws on the first one
943
- const result = await SkipInput({}, { pathParams: {}, body: {} });
944
- expect(result).toEqual({ pathParams: {}, body: {} });
945
- });
946
- test('Create still validates when noRuntimeValidation is omitted', async () => {
947
- const { Create } = Procedures({ config: {} });
948
- const { ValidateDefault } = Create('ValidateDefault', {
949
- schema: {
950
- params: Type.Object({ name: Type.String() }),
951
- },
952
- }, async (_ctx, params) => params);
953
- await expect(ValidateDefault({}, {})).rejects.toBeInstanceOf(ProcedureValidationError);
954
- });
955
- test('Create validates when builder is omitted entirely', async () => {
956
- const { Create } = Procedures();
957
- const { ValidateNoBuilder } = Create('ValidateNoBuilder', {
958
- schema: {
959
- params: Type.Object({ name: Type.String() }),
960
- },
961
- }, async (_ctx, params) => params);
962
- await expect(ValidateNoBuilder({}, {})).rejects.toBeInstanceOf(ProcedureValidationError);
963
- });
964
- test('CreateStream skips params validation when noRuntimeValidation is true', async () => {
965
- const { CreateStream } = Procedures({ config: { noRuntimeValidation: true } });
966
- const { StreamSkipParams } = CreateStream('StreamSkipParams', {
967
- schema: {
968
- params: Type.Object({ count: Type.Number() }),
969
- },
970
- }, async function* (_ctx, params) {
971
- yield { received: params };
972
- });
973
- const values = [];
974
- for await (const val of StreamSkipParams({}, {})) {
975
- values.push(val);
976
- }
977
- expect(values).toEqual([{ received: {} }]);
978
- });
979
- test('CreateStream skips input channel validation when noRuntimeValidation is true', async () => {
980
- const { CreateStream } = Procedures({ config: { noRuntimeValidation: true } });
981
- const { StreamSkipInput } = CreateStream('StreamSkipInput', {
982
- schema: {
983
- input: {
984
- pathParams: Type.Object({ id: Type.String() }),
985
- },
986
- },
987
- }, async function* (_ctx, params) {
988
- yield params;
989
- });
990
- const values = [];
991
- for await (const val of StreamSkipInput({}, { pathParams: {} })) {
992
- values.push(val);
993
- }
994
- expect(values).toEqual([{ pathParams: {} }]);
995
- });
996
- test('CreateStream still validates when noRuntimeValidation is omitted', async () => {
997
- const { CreateStream } = Procedures({ config: {} });
998
- const { StreamValidateDefault } = CreateStream('StreamValidateDefault', {
999
- schema: {
1000
- params: Type.Object({ count: Type.Number() }),
1001
- },
1002
- }, async function* (_ctx, params) {
1003
- yield params;
1004
- });
1005
- await expect(async () => {
1006
- for await (const _v of StreamValidateDefault({}, {})) {
1007
- // consume
1008
- }
1009
- }).rejects.toBeInstanceOf(ProcedureValidationError);
1010
- });
1011
96
  test('noRuntimeValidation does not interfere with onCreate callback', async () => {
1012
97
  const seen = [];
1013
98
  const { Create } = Procedures({
@@ -1024,4 +109,20 @@ describe('builder.config.noRuntimeValidation', () => {
1024
109
  void Procedures({ config: { noRuntimeValidation: false } });
1025
110
  });
1026
111
  });
112
+ describe('Procedure registration kind discriminant', () => {
113
+ it('Create produces kind: rpc', () => {
114
+ const procs = Procedures();
115
+ procs.Create('Foo', { schema: { params: Type.Object({}) } }, async () => undefined);
116
+ const reg = procs.getProcedure('Foo');
117
+ expect(reg?.kind).toBe('rpc');
118
+ });
119
+ it('CreateStream produces kind: rpc-stream', () => {
120
+ const procs = Procedures();
121
+ procs.CreateStream('Bar', {
122
+ schema: { params: Type.Object({}), yieldType: Type.Number() },
123
+ }, async function* () { });
124
+ const reg = procs.getProcedure('Bar');
125
+ expect(reg?.kind).toBe('rpc-stream');
126
+ });
127
+ });
1027
128
  //# sourceMappingURL=index.test.js.map