ts-procedures 8.6.0 → 9.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 (627) hide show
  1. package/README.md +166 -101
  2. package/agent_config/claude-code/.claude-plugin/plugin.json +1 -1
  3. package/agent_config/claude-code/agents/ts-procedures-architect.md +11 -10
  4. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +25 -12
  5. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +10 -12
  6. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +141 -45
  7. package/agent_config/claude-code/skills/ts-procedures/checklist.md +7 -6
  8. package/agent_config/claude-code/skills/ts-procedures/patterns.md +45 -6
  9. package/agent_config/claude-code/skills/ts-procedures/templates/client.md +1 -1
  10. package/agent_config/claude-code/skills/ts-procedures/templates/hono.md +1 -1
  11. package/agent_config/copilot/copilot-instructions.md +50 -33
  12. package/agent_config/cursor/cursorrules +50 -33
  13. package/build/adapters/astro/astro-context.js.map +1 -0
  14. package/build/adapters/astro/create-handler.js.map +1 -0
  15. package/build/adapters/astro/index.js.map +1 -0
  16. package/build/{implementations/http → adapters}/astro/index.test.js +1 -1
  17. package/build/adapters/astro/index.test.js.map +1 -0
  18. package/build/adapters/astro/rewrite-request.js.map +1 -0
  19. package/build/adapters/hono/envelope-parity.test.js +98 -0
  20. package/build/adapters/hono/envelope-parity.test.js.map +1 -0
  21. package/build/{implementations/http → adapters}/hono/handlers/http-stream.d.ts +1 -1
  22. package/build/adapters/hono/handlers/http-stream.js +55 -0
  23. package/build/adapters/hono/handlers/http-stream.js.map +1 -0
  24. package/build/{implementations/http → adapters}/hono/handlers/http-stream.test.js +1 -1
  25. package/build/adapters/hono/handlers/http-stream.test.js.map +1 -0
  26. package/build/{implementations/http → adapters}/hono/handlers/http.d.ts +1 -1
  27. package/build/adapters/hono/handlers/http.js +50 -0
  28. package/build/adapters/hono/handlers/http.js.map +1 -0
  29. package/build/{implementations/http → adapters}/hono/handlers/http.test.js +1 -1
  30. package/build/adapters/hono/handlers/http.test.js.map +1 -0
  31. package/build/{implementations/http → adapters}/hono/handlers/rpc.d.ts +2 -2
  32. package/build/adapters/hono/handlers/rpc.js +23 -0
  33. package/build/adapters/hono/handlers/rpc.js.map +1 -0
  34. package/build/{implementations/http → adapters}/hono/handlers/rpc.test.js +1 -1
  35. package/build/adapters/hono/handlers/rpc.test.js.map +1 -0
  36. package/build/adapters/hono/handlers/stream.d.ts +12 -0
  37. package/build/adapters/hono/handlers/stream.js +89 -0
  38. package/build/adapters/hono/handlers/stream.js.map +1 -0
  39. package/build/{implementations/http → adapters}/hono/handlers/stream.test.js +3 -2
  40. package/build/adapters/hono/handlers/stream.test.js.map +1 -0
  41. package/build/{implementations/http → adapters}/hono/index.d.ts +24 -12
  42. package/build/{implementations/http → adapters}/hono/index.js +19 -8
  43. package/build/adapters/hono/index.js.map +1 -0
  44. package/build/{implementations/http → adapters}/hono/index.test.js +2 -4
  45. package/build/adapters/hono/index.test.js.map +1 -0
  46. package/build/{implementations/http → adapters/hono}/on-request-error.test.js +2 -2
  47. package/build/adapters/hono/on-request-error.test.js.map +1 -0
  48. package/build/adapters/hono/request.d.ts +7 -0
  49. package/build/adapters/hono/request.js +22 -0
  50. package/build/adapters/hono/request.js.map +1 -0
  51. package/build/{implementations/http → adapters/hono}/route-errors.test.js +4 -4
  52. package/build/adapters/hono/route-errors.test.js.map +1 -0
  53. package/build/adapters/hono/types.d.ts +55 -0
  54. package/build/adapters/hono/types.js +19 -0
  55. package/build/adapters/hono/types.js.map +1 -0
  56. package/build/client/freeze.test.js +39 -0
  57. package/build/client/freeze.test.js.map +1 -0
  58. package/build/client/typed-error-dispatch.test.js +2 -2
  59. package/build/client/typed-error-dispatch.test.js.map +1 -1
  60. package/build/codegen/__fixtures__/make-envelope.d.ts +1 -1
  61. package/build/codegen/bin/cli.d.ts +5 -0
  62. package/build/codegen/bin/cli.js +139 -182
  63. package/build/codegen/bin/cli.js.map +1 -1
  64. package/build/codegen/bin/cli.test.js +12 -2
  65. package/build/codegen/bin/cli.test.js.map +1 -1
  66. package/build/codegen/bin/flag-specs.d.ts +9 -0
  67. package/build/codegen/bin/flag-specs.js +33 -31
  68. package/build/codegen/bin/flag-specs.js.map +1 -1
  69. package/build/codegen/bin/flag-specs.test.js +14 -1
  70. package/build/codegen/bin/flag-specs.test.js.map +1 -1
  71. package/build/codegen/collect-models.d.ts +1 -1
  72. package/build/codegen/emit/api-route.d.ts +8 -0
  73. package/build/codegen/emit/api-route.js +156 -0
  74. package/build/codegen/emit/api-route.js.map +1 -0
  75. package/build/codegen/emit/context.d.ts +30 -0
  76. package/build/codegen/emit/context.js +2 -0
  77. package/build/codegen/emit/context.js.map +1 -0
  78. package/build/codegen/emit/declarations.d.ts +24 -0
  79. package/build/codegen/emit/declarations.js +48 -0
  80. package/build/codegen/emit/declarations.js.map +1 -0
  81. package/build/codegen/emit/format-types.d.ts +61 -0
  82. package/build/codegen/emit/format-types.js +188 -0
  83. package/build/codegen/emit/format-types.js.map +1 -0
  84. package/build/codegen/emit/http-stream-route.d.ts +7 -0
  85. package/build/codegen/emit/http-stream-route.js +138 -0
  86. package/build/codegen/emit/http-stream-route.js.map +1 -0
  87. package/build/codegen/emit/route-shared.d.ts +35 -0
  88. package/build/codegen/emit/route-shared.js +88 -0
  89. package/build/codegen/emit/route-shared.js.map +1 -0
  90. package/build/codegen/emit/rpc-route.d.ts +7 -0
  91. package/build/codegen/emit/rpc-route.js +37 -0
  92. package/build/codegen/emit/rpc-route.js.map +1 -0
  93. package/build/codegen/emit/scope-file.d.ts +39 -0
  94. package/build/codegen/emit/scope-file.js +166 -0
  95. package/build/codegen/emit/scope-file.js.map +1 -0
  96. package/build/codegen/emit/stream-route.d.ts +7 -0
  97. package/build/codegen/emit/stream-route.js +62 -0
  98. package/build/codegen/emit/stream-route.js.map +1 -0
  99. package/build/codegen/emit-errors.d.ts +1 -1
  100. package/build/codegen/emit-errors.integration.test.js +1 -1
  101. package/build/codegen/emit-errors.integration.test.js.map +1 -1
  102. package/build/codegen/emit-scope.d.ts +13 -30
  103. package/build/codegen/emit-scope.js +15 -844
  104. package/build/codegen/emit-scope.js.map +1 -1
  105. package/build/codegen/goldens.test.js +69 -0
  106. package/build/codegen/goldens.test.js.map +1 -0
  107. package/build/codegen/group-routes.d.ts +1 -1
  108. package/build/codegen/pipeline.d.ts +1 -1
  109. package/build/codegen/resolve-envelope.d.ts +1 -1
  110. package/build/codegen/targets/_shared/error-schemas.d.ts +1 -1
  111. package/build/codegen/targets/_shared/route-slots.d.ts +1 -1
  112. package/build/codegen/targets/_shared/target-run.d.ts +1 -1
  113. package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +1 -1
  114. package/build/codegen/targets/swift/emit-route-swift.d.ts +1 -1
  115. package/build/core/create-http-stream.d.ts +50 -0
  116. package/build/core/create-http-stream.js +108 -0
  117. package/build/core/create-http-stream.js.map +1 -0
  118. package/build/{create-http-stream.test.js → core/create-http-stream.test.js} +1 -1
  119. package/build/core/create-http-stream.test.js.map +1 -0
  120. package/build/core/create-http.d.ts +51 -0
  121. package/build/core/create-http.js +65 -0
  122. package/build/core/create-http.js.map +1 -0
  123. package/build/{create-http.test.js → core/create-http.test.js} +13 -4
  124. package/build/core/create-http.test.js.map +1 -0
  125. package/build/core/create-stream.d.ts +26 -0
  126. package/build/core/create-stream.js +80 -0
  127. package/build/core/create-stream.js.map +1 -0
  128. package/build/{create-stream.test.js → core/create-stream.test.js} +23 -28
  129. package/build/core/create-stream.test.js.map +1 -0
  130. package/build/core/create.d.ts +22 -0
  131. package/build/core/create.js +71 -0
  132. package/build/core/create.js.map +1 -0
  133. package/build/{create.test.js → core/create.test.js} +25 -46
  134. package/build/core/create.test.js.map +1 -0
  135. package/build/core/definition-site.d.ts +24 -0
  136. package/build/{stack-utils.js → core/definition-site.js} +20 -20
  137. package/build/core/definition-site.js.map +1 -0
  138. package/build/{stack-utils.test.js → core/definition-site.test.js} +12 -3
  139. package/build/core/definition-site.test.js.map +1 -0
  140. package/build/{errors.d.ts → core/errors.d.ts} +19 -8
  141. package/build/{errors.js → core/errors.js} +21 -26
  142. package/build/core/errors.js.map +1 -0
  143. package/build/core/errors.test.js.map +1 -0
  144. package/build/core/factory-options.test.js +82 -0
  145. package/build/core/factory-options.test.js.map +1 -0
  146. package/build/core/http-route.d.ts +13 -0
  147. package/build/core/http-route.js +54 -0
  148. package/build/core/http-route.js.map +1 -0
  149. package/build/core/internal.d.ts +72 -0
  150. package/build/core/internal.js +128 -0
  151. package/build/core/internal.js.map +1 -0
  152. package/build/{migration.test.js → core/migration.test.js} +17 -1
  153. package/build/core/migration.test.js.map +1 -0
  154. package/build/core/procedures.d.ts +143 -0
  155. package/build/core/procedures.js +64 -0
  156. package/build/core/procedures.js.map +1 -0
  157. package/build/{index.test.js → core/procedures.test.js} +14 -11
  158. package/build/core/procedures.test.js.map +1 -0
  159. package/build/core/types.d.ts +182 -0
  160. package/build/{schema → core}/types.js.map +1 -1
  161. package/build/exports.d.ts +31 -11
  162. package/build/exports.js +23 -8
  163. package/build/exports.js.map +1 -1
  164. package/build/schema/adapter.d.ts +35 -0
  165. package/build/schema/adapter.js +13 -0
  166. package/build/schema/adapter.js.map +1 -0
  167. package/build/schema/adapter.test.js +53 -0
  168. package/build/schema/adapter.test.js.map +1 -0
  169. package/build/schema/compile.d.ts +37 -0
  170. package/build/schema/compile.js +38 -0
  171. package/build/schema/compile.js.map +1 -0
  172. package/build/schema/compile.test.js +78 -0
  173. package/build/schema/compile.test.js.map +1 -0
  174. package/build/schema/compute-schema.d.ts +47 -37
  175. package/build/schema/compute-schema.js +86 -29
  176. package/build/schema/compute-schema.js.map +1 -1
  177. package/build/schema/compute-schema.test.js +158 -40
  178. package/build/schema/compute-schema.test.js.map +1 -1
  179. package/build/schema/json-schema.d.ts +17 -0
  180. package/build/schema/json-schema.js +2 -0
  181. package/build/schema/json-schema.js.map +1 -0
  182. package/build/schema/typebox.d.ts +11 -0
  183. package/build/schema/typebox.js +24 -0
  184. package/build/schema/typebox.js.map +1 -0
  185. package/build/schema/typebox.test.js +34 -0
  186. package/build/schema/typebox.test.js.map +1 -0
  187. package/build/server/context.d.ts +8 -0
  188. package/build/server/context.js +7 -0
  189. package/build/server/context.js.map +1 -0
  190. package/build/server/context.test.js +16 -0
  191. package/build/server/context.test.js.map +1 -0
  192. package/build/{doc-envelope.d.ts → server/doc-envelope.d.ts} +1 -1
  193. package/build/server/doc-envelope.js.map +1 -0
  194. package/build/server/doc-envelope.test.d.ts +1 -0
  195. package/build/server/doc-envelope.test.js.map +1 -0
  196. package/build/{implementations/http → server}/doc-registry.d.ts +7 -2
  197. package/build/{implementations/http → server}/doc-registry.js +9 -5
  198. package/build/server/doc-registry.js.map +1 -0
  199. package/build/server/doc-registry.test.d.ts +1 -0
  200. package/build/{implementations/http → server}/doc-registry.test.js +27 -24
  201. package/build/server/doc-registry.test.js.map +1 -0
  202. package/build/server/docs/docs.test.d.ts +1 -0
  203. package/build/server/docs/docs.test.js +237 -0
  204. package/build/server/docs/docs.test.js.map +1 -0
  205. package/build/{implementations/http/hono → server}/docs/http-doc.d.ts +2 -2
  206. package/build/{implementations/http/hono → server}/docs/http-doc.js +1 -1
  207. package/build/server/docs/http-doc.js.map +1 -0
  208. package/build/{implementations/http/hono → server}/docs/http-stream-doc.d.ts +2 -2
  209. package/build/{implementations/http/hono → server}/docs/http-stream-doc.js +1 -1
  210. package/build/server/docs/http-stream-doc.js.map +1 -0
  211. package/build/{implementations/http/hono → server}/docs/rpc-doc.d.ts +2 -2
  212. package/build/{implementations/http/hono → server}/docs/rpc-doc.js +1 -1
  213. package/build/server/docs/rpc-doc.js.map +1 -0
  214. package/build/{implementations/http/hono → server}/docs/stream-doc.d.ts +2 -2
  215. package/build/{implementations/http/hono → server}/docs/stream-doc.js +1 -1
  216. package/build/server/docs/stream-doc.js.map +1 -0
  217. package/build/server/errors/dispatch.d.ts +96 -0
  218. package/build/{implementations/http/error-dispatch.js → server/errors/dispatch.js} +20 -10
  219. package/build/server/errors/dispatch.js.map +1 -0
  220. package/build/server/errors/dispatch.test.d.ts +1 -0
  221. package/build/server/errors/dispatch.test.js +418 -0
  222. package/build/server/errors/dispatch.test.js.map +1 -0
  223. package/build/{implementations/http/error-taxonomy.d.ts → server/errors/taxonomy.d.ts} +8 -17
  224. package/build/{implementations/http/error-taxonomy.js → server/errors/taxonomy.js} +6 -15
  225. package/build/server/errors/taxonomy.js.map +1 -0
  226. package/build/server/errors/taxonomy.test.d.ts +1 -0
  227. package/build/{implementations/http/error-taxonomy.test.js → server/errors/taxonomy.test.js} +45 -39
  228. package/build/server/errors/taxonomy.test.js.map +1 -0
  229. package/build/server/index.d.ts +29 -0
  230. package/build/server/index.js +27 -0
  231. package/build/server/index.js.map +1 -0
  232. package/build/server/no-framework-imports.test.d.ts +1 -0
  233. package/build/server/no-framework-imports.test.js +40 -0
  234. package/build/server/no-framework-imports.test.js.map +1 -0
  235. package/build/{implementations/http/hono/path.d.ts → server/paths.d.ts} +2 -3
  236. package/build/{implementations/http/hono/path.js → server/paths.js} +1 -1
  237. package/build/server/paths.js.map +1 -0
  238. package/build/server/paths.test.d.ts +1 -0
  239. package/build/server/paths.test.js +111 -0
  240. package/build/server/paths.test.js.map +1 -0
  241. package/build/server/request/params.d.ts +29 -0
  242. package/build/server/request/params.js +43 -0
  243. package/build/server/request/params.js.map +1 -0
  244. package/build/server/request/params.test.d.ts +1 -0
  245. package/build/server/request/params.test.js +91 -0
  246. package/build/server/request/params.test.js.map +1 -0
  247. package/build/server/request/query.d.ts +9 -0
  248. package/build/server/request/query.js +22 -0
  249. package/build/server/request/query.js.map +1 -0
  250. package/build/server/request/query.test.d.ts +1 -0
  251. package/build/server/request/query.test.js +60 -0
  252. package/build/server/request/query.test.js.map +1 -0
  253. package/build/server/sse.d.ts +70 -0
  254. package/build/server/sse.js +94 -0
  255. package/build/server/sse.js.map +1 -0
  256. package/build/server/sse.test.d.ts +1 -0
  257. package/build/server/sse.test.js +98 -0
  258. package/build/server/sse.test.js.map +1 -0
  259. package/build/{implementations → server}/types.d.ts +17 -15
  260. package/build/{implementations → server}/types.js.map +1 -1
  261. package/docs/astro-adapter.md +8 -9
  262. package/docs/client-and-codegen.md +4 -4
  263. package/docs/client-error-handling.md +5 -5
  264. package/docs/codegen-kotlin.md +2 -3
  265. package/docs/codegen-swift.md +1 -2
  266. package/docs/core.md +135 -54
  267. package/docs/http-integrations.md +58 -6
  268. package/docs/migration-v8-to-v9.md +192 -0
  269. package/docs/plans/2026-06-09-v9-rewrite.md +130 -0
  270. package/docs/specs/2026-06-09-v9-rewrite-design.md +221 -0
  271. package/docs/streaming.md +12 -0
  272. package/package.json +23 -47
  273. package/src/{implementations/http → adapters}/astro/index.test.ts +2 -2
  274. package/src/adapters/hono/__fixtures__/parity-envelope.json +389 -0
  275. package/src/adapters/hono/envelope-parity.test.ts +126 -0
  276. package/src/{implementations/http → adapters}/hono/handlers/http-stream.test.ts +1 -1
  277. package/src/adapters/hono/handlers/http-stream.ts +73 -0
  278. package/src/{implementations/http → adapters}/hono/handlers/http.test.ts +1 -1
  279. package/src/adapters/hono/handlers/http.ts +70 -0
  280. package/src/{implementations/http → adapters}/hono/handlers/rpc.test.ts +2 -2
  281. package/src/adapters/hono/handlers/rpc.ts +39 -0
  282. package/src/{implementations/http → adapters}/hono/handlers/stream.test.ts +4 -3
  283. package/src/{implementations/http → adapters}/hono/handlers/stream.ts +19 -92
  284. package/src/{implementations/http → adapters}/hono/index.test.ts +14 -16
  285. package/src/{implementations/http → adapters}/hono/index.ts +35 -30
  286. package/src/{implementations/http → adapters/hono}/on-request-error.test.ts +3 -3
  287. package/src/adapters/hono/request.ts +28 -0
  288. package/src/{implementations/http → adapters/hono}/route-errors.test.ts +5 -5
  289. package/src/{implementations/http → adapters}/hono/types.ts +43 -20
  290. package/src/client/freeze.test.ts +41 -0
  291. package/src/client/typed-error-dispatch.test.ts +3 -3
  292. package/src/codegen/__fixtures__/make-envelope.ts +1 -1
  293. package/src/codegen/__fixtures__/models-envelope.json +310 -0
  294. package/src/codegen/__goldens__/MANIFEST.json +85 -0
  295. package/src/codegen/__goldens__/kotlin-default--models/Billing.kt +112 -0
  296. package/src/codegen/__goldens__/kotlin-default--models/BillingReports.kt +26 -0
  297. package/src/codegen/__goldens__/kotlin-default--models/Orders.kt +88 -0
  298. package/src/codegen/__goldens__/kotlin-default--users/Users.kt +189 -0
  299. package/src/codegen/__goldens__/swift-default--models/Billing.swift +97 -0
  300. package/src/codegen/__goldens__/swift-default--models/BillingReports.swift +20 -0
  301. package/src/codegen/__goldens__/swift-default--models/Orders.swift +81 -0
  302. package/src/codegen/__goldens__/swift-default--users/Users.swift +204 -0
  303. package/src/codegen/__goldens__/ts-default--models/_client.ts +1319 -0
  304. package/src/codegen/__goldens__/ts-default--models/_errors.ts +90 -0
  305. package/src/codegen/__goldens__/ts-default--models/_models.ts +10 -0
  306. package/src/codegen/__goldens__/ts-default--models/_types.ts +502 -0
  307. package/src/codegen/__goldens__/ts-default--models/billing-reports.ts +29 -0
  308. package/src/codegen/__goldens__/ts-default--models/billing.ts +67 -0
  309. package/src/codegen/__goldens__/ts-default--models/index.ts +48 -0
  310. package/src/codegen/__goldens__/ts-default--models/orders.ts +80 -0
  311. package/src/codegen/__goldens__/ts-default--users/_client.ts +1319 -0
  312. package/src/codegen/__goldens__/ts-default--users/_errors.ts +90 -0
  313. package/src/codegen/__goldens__/ts-default--users/_types.ts +502 -0
  314. package/src/codegen/__goldens__/ts-default--users/index.ts +38 -0
  315. package/src/codegen/__goldens__/ts-default--users/users.ts +169 -0
  316. package/src/codegen/__goldens__/ts-external-runtime--models/_errors.ts +90 -0
  317. package/src/codegen/__goldens__/ts-external-runtime--models/_models.ts +10 -0
  318. package/src/codegen/__goldens__/ts-external-runtime--models/billing-reports.ts +29 -0
  319. package/src/codegen/__goldens__/ts-external-runtime--models/billing.ts +67 -0
  320. package/src/codegen/__goldens__/ts-external-runtime--models/index.ts +48 -0
  321. package/src/codegen/__goldens__/ts-external-runtime--models/orders.ts +80 -0
  322. package/src/codegen/__goldens__/ts-external-runtime--users/_errors.ts +90 -0
  323. package/src/codegen/__goldens__/ts-external-runtime--users/index.ts +38 -0
  324. package/src/codegen/__goldens__/ts-external-runtime--users/users.ts +169 -0
  325. package/src/codegen/__goldens__/ts-flat--models/_client.ts +1319 -0
  326. package/src/codegen/__goldens__/ts-flat--models/_errors.ts +87 -0
  327. package/src/codegen/__goldens__/ts-flat--models/_models.ts +10 -0
  328. package/src/codegen/__goldens__/ts-flat--models/_types.ts +502 -0
  329. package/src/codegen/__goldens__/ts-flat--models/billing-reports.ts +28 -0
  330. package/src/codegen/__goldens__/ts-flat--models/billing.ts +51 -0
  331. package/src/codegen/__goldens__/ts-flat--models/index.ts +42 -0
  332. package/src/codegen/__goldens__/ts-flat--models/orders.ts +73 -0
  333. package/src/codegen/__goldens__/ts-flat--users/_client.ts +1319 -0
  334. package/src/codegen/__goldens__/ts-flat--users/_errors.ts +87 -0
  335. package/src/codegen/__goldens__/ts-flat--users/_types.ts +502 -0
  336. package/src/codegen/__goldens__/ts-flat--users/index.ts +34 -0
  337. package/src/codegen/__goldens__/ts-flat--users/users.ts +126 -0
  338. package/src/codegen/__goldens__/ts-no-share-models--models/_client.ts +1319 -0
  339. package/src/codegen/__goldens__/ts-no-share-models--models/_errors.ts +90 -0
  340. package/src/codegen/__goldens__/ts-no-share-models--models/_types.ts +502 -0
  341. package/src/codegen/__goldens__/ts-no-share-models--models/billing-reports.ts +29 -0
  342. package/src/codegen/__goldens__/ts-no-share-models--models/billing.ts +111 -0
  343. package/src/codegen/__goldens__/ts-no-share-models--models/index.ts +48 -0
  344. package/src/codegen/__goldens__/ts-no-share-models--models/orders.ts +112 -0
  345. package/src/codegen/__goldens__/ts-no-share-models--users/_client.ts +1319 -0
  346. package/src/codegen/__goldens__/ts-no-share-models--users/_errors.ts +90 -0
  347. package/src/codegen/__goldens__/ts-no-share-models--users/_types.ts +502 -0
  348. package/src/codegen/__goldens__/ts-no-share-models--users/index.ts +38 -0
  349. package/src/codegen/__goldens__/ts-no-share-models--users/users.ts +169 -0
  350. package/src/codegen/__goldens__/ts-shared-models-module--models/_client.ts +1319 -0
  351. package/src/codegen/__goldens__/ts-shared-models-module--models/_errors.ts +90 -0
  352. package/src/codegen/__goldens__/ts-shared-models-module--models/_models.ts +7 -0
  353. package/src/codegen/__goldens__/ts-shared-models-module--models/_types.ts +502 -0
  354. package/src/codegen/__goldens__/ts-shared-models-module--models/billing-reports.ts +29 -0
  355. package/src/codegen/__goldens__/ts-shared-models-module--models/billing.ts +67 -0
  356. package/src/codegen/__goldens__/ts-shared-models-module--models/index.ts +48 -0
  357. package/src/codegen/__goldens__/ts-shared-models-module--models/orders.ts +80 -0
  358. package/src/codegen/bin/cli.test.ts +13 -2
  359. package/src/codegen/bin/cli.ts +181 -144
  360. package/src/codegen/bin/flag-specs.test.ts +16 -1
  361. package/src/codegen/bin/flag-specs.ts +43 -31
  362. package/src/codegen/bundle-size.test.ts +1 -1
  363. package/src/codegen/collect-models.ts +1 -1
  364. package/src/codegen/e2e.test.ts +1 -1
  365. package/src/codegen/emit/api-route.ts +184 -0
  366. package/src/codegen/emit/context.ts +32 -0
  367. package/src/codegen/emit/declarations.ts +49 -0
  368. package/src/codegen/emit/format-types.ts +232 -0
  369. package/src/codegen/emit/http-stream-route.ts +162 -0
  370. package/src/codegen/emit/route-shared.ts +102 -0
  371. package/src/codegen/emit/rpc-route.ts +49 -0
  372. package/src/codegen/emit/scope-file.ts +226 -0
  373. package/src/codegen/emit/stream-route.ts +81 -0
  374. package/src/codegen/emit-errors.integration.test.ts +2 -2
  375. package/src/codegen/emit-errors.test.ts +1 -1
  376. package/src/codegen/emit-errors.ts +1 -1
  377. package/src/codegen/emit-scope.test.ts +2 -2
  378. package/src/codegen/emit-scope.ts +15 -1048
  379. package/src/codegen/goldens.test.ts +89 -0
  380. package/src/codegen/group-routes.test.ts +1 -1
  381. package/src/codegen/group-routes.ts +1 -1
  382. package/src/codegen/pipeline.test.ts +1 -1
  383. package/src/codegen/pipeline.ts +1 -1
  384. package/src/codegen/resolve-envelope.test.ts +1 -1
  385. package/src/codegen/resolve-envelope.ts +1 -1
  386. package/src/codegen/targets/_shared/error-schemas.test.ts +1 -1
  387. package/src/codegen/targets/_shared/error-schemas.ts +1 -1
  388. package/src/codegen/targets/_shared/route-slots.test.ts +1 -1
  389. package/src/codegen/targets/_shared/route-slots.ts +1 -1
  390. package/src/codegen/targets/_shared/target-run.ts +1 -1
  391. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +1 -1
  392. package/src/codegen/targets/kotlin/emit-route-kotlin.ts +1 -1
  393. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +1 -1
  394. package/src/codegen/targets/swift/access-level.test.ts +1 -1
  395. package/src/codegen/targets/swift/emit-route-swift.test.ts +1 -1
  396. package/src/codegen/targets/swift/emit-route-swift.ts +1 -1
  397. package/src/codegen/targets/swift/emit-scope-swift.test.ts +1 -1
  398. package/src/codegen/targets/ts/shared-models.test.ts +1 -1
  399. package/src/{create-http-stream.test.ts → core/create-http-stream.test.ts} +1 -1
  400. package/src/core/create-http-stream.ts +207 -0
  401. package/src/{create-http.test.ts → core/create-http.test.ts} +15 -4
  402. package/src/core/create-http.ts +126 -0
  403. package/src/{create-stream.test.ts → core/create-stream.test.ts} +28 -31
  404. package/src/core/create-stream.ts +142 -0
  405. package/src/{create.test.ts → core/create.test.ts} +25 -57
  406. package/src/core/create.ts +121 -0
  407. package/src/{stack-utils.test.ts → core/definition-site.test.ts} +14 -3
  408. package/src/{stack-utils.ts → core/definition-site.ts} +20 -23
  409. package/src/{errors.test.ts → core/errors.test.ts} +1 -1
  410. package/src/{errors.ts → core/errors.ts} +30 -28
  411. package/src/core/factory-options.test.ts +112 -0
  412. package/src/core/http-route.ts +73 -0
  413. package/src/core/internal.ts +203 -0
  414. package/src/{migration.test.ts → core/migration.test.ts} +23 -1
  415. package/src/{index.test.ts → core/procedures.test.ts} +13 -11
  416. package/src/core/procedures.ts +75 -0
  417. package/src/core/types.ts +195 -0
  418. package/src/exports.ts +60 -11
  419. package/src/schema/adapter.test.ts +58 -0
  420. package/src/schema/adapter.ts +45 -0
  421. package/src/schema/compile.test.ts +95 -0
  422. package/src/schema/compile.ts +64 -0
  423. package/src/schema/compute-schema.test.ts +222 -41
  424. package/src/schema/compute-schema.ts +145 -71
  425. package/src/schema/json-schema.ts +21 -0
  426. package/src/schema/typebox.test.ts +40 -0
  427. package/src/schema/typebox.ts +27 -0
  428. package/src/server/context.test.ts +22 -0
  429. package/src/server/context.ts +18 -0
  430. package/src/{doc-envelope.test.ts → server/doc-envelope.test.ts} +2 -2
  431. package/src/{doc-envelope.ts → server/doc-envelope.ts} +1 -1
  432. package/src/{implementations/http → server}/doc-registry.test.ts +32 -26
  433. package/src/{implementations/http → server}/doc-registry.ts +11 -7
  434. package/src/server/docs/docs.test.ts +287 -0
  435. package/src/{implementations/http/hono → server}/docs/http-doc.ts +3 -3
  436. package/src/{implementations/http/hono → server}/docs/http-stream-doc.ts +3 -3
  437. package/src/{implementations/http/hono → server}/docs/rpc-doc.ts +3 -3
  438. package/src/{implementations/http/hono → server}/docs/stream-doc.ts +3 -3
  439. package/src/server/errors/dispatch.test.ts +450 -0
  440. package/src/server/errors/dispatch.ts +189 -0
  441. package/src/{implementations/http/error-taxonomy.test.ts → server/errors/taxonomy.test.ts} +45 -39
  442. package/src/{implementations/http/error-taxonomy.ts → server/errors/taxonomy.ts} +8 -17
  443. package/src/server/index.ts +29 -0
  444. package/src/server/no-framework-imports.test.ts +43 -0
  445. package/src/server/paths.test.ts +141 -0
  446. package/src/{implementations/http/hono/path.ts → server/paths.ts} +2 -13
  447. package/src/server/request/params.test.ts +143 -0
  448. package/src/server/request/params.ts +68 -0
  449. package/src/server/request/query.test.ts +70 -0
  450. package/src/server/request/query.ts +24 -0
  451. package/src/server/sse.test.ts +113 -0
  452. package/src/server/sse.ts +117 -0
  453. package/src/{implementations → server}/types.ts +17 -16
  454. package/build/create-http-stream.d.ts +0 -58
  455. package/build/create-http-stream.js +0 -122
  456. package/build/create-http-stream.js.map +0 -1
  457. package/build/create-http-stream.test.js.map +0 -1
  458. package/build/create-http.d.ts +0 -49
  459. package/build/create-http.js +0 -108
  460. package/build/create-http.js.map +0 -1
  461. package/build/create-http.test.js.map +0 -1
  462. package/build/create-stream.d.ts +0 -35
  463. package/build/create-stream.js +0 -123
  464. package/build/create-stream.js.map +0 -1
  465. package/build/create-stream.test.js.map +0 -1
  466. package/build/create.d.ts +0 -28
  467. package/build/create.js +0 -82
  468. package/build/create.js.map +0 -1
  469. package/build/create.test.js.map +0 -1
  470. package/build/doc-envelope.js.map +0 -1
  471. package/build/doc-envelope.test.js.map +0 -1
  472. package/build/errors.js.map +0 -1
  473. package/build/errors.test.js.map +0 -1
  474. package/build/implementations/http/astro/astro-context.js.map +0 -1
  475. package/build/implementations/http/astro/create-handler.js.map +0 -1
  476. package/build/implementations/http/astro/index.js.map +0 -1
  477. package/build/implementations/http/astro/index.test.js.map +0 -1
  478. package/build/implementations/http/astro/rewrite-request.js.map +0 -1
  479. package/build/implementations/http/doc-registry.js.map +0 -1
  480. package/build/implementations/http/doc-registry.test.js.map +0 -1
  481. package/build/implementations/http/error-dispatch.d.ts +0 -76
  482. package/build/implementations/http/error-dispatch.js.map +0 -1
  483. package/build/implementations/http/error-dispatch.test.js +0 -254
  484. package/build/implementations/http/error-dispatch.test.js.map +0 -1
  485. package/build/implementations/http/error-taxonomy.js.map +0 -1
  486. package/build/implementations/http/error-taxonomy.test.js.map +0 -1
  487. package/build/implementations/http/hono/docs/http-doc.js.map +0 -1
  488. package/build/implementations/http/hono/docs/http-stream-doc.js.map +0 -1
  489. package/build/implementations/http/hono/docs/rpc-doc.js.map +0 -1
  490. package/build/implementations/http/hono/docs/stream-doc.js.map +0 -1
  491. package/build/implementations/http/hono/handlers/http-stream.js +0 -123
  492. package/build/implementations/http/hono/handlers/http-stream.js.map +0 -1
  493. package/build/implementations/http/hono/handlers/http-stream.test.js.map +0 -1
  494. package/build/implementations/http/hono/handlers/http.js +0 -110
  495. package/build/implementations/http/hono/handlers/http.js.map +0 -1
  496. package/build/implementations/http/hono/handlers/http.test.js.map +0 -1
  497. package/build/implementations/http/hono/handlers/rpc.js +0 -32
  498. package/build/implementations/http/hono/handlers/rpc.js.map +0 -1
  499. package/build/implementations/http/hono/handlers/rpc.test.js.map +0 -1
  500. package/build/implementations/http/hono/handlers/stream.d.ts +0 -23
  501. package/build/implementations/http/hono/handlers/stream.js +0 -147
  502. package/build/implementations/http/hono/handlers/stream.js.map +0 -1
  503. package/build/implementations/http/hono/handlers/stream.test.js.map +0 -1
  504. package/build/implementations/http/hono/index.js.map +0 -1
  505. package/build/implementations/http/hono/index.test.js.map +0 -1
  506. package/build/implementations/http/hono/path.js.map +0 -1
  507. package/build/implementations/http/hono/path.test.js +0 -83
  508. package/build/implementations/http/hono/path.test.js.map +0 -1
  509. package/build/implementations/http/hono/types.d.ts +0 -51
  510. package/build/implementations/http/hono/types.js.map +0 -1
  511. package/build/implementations/http/on-request-error.test.js.map +0 -1
  512. package/build/implementations/http/route-errors.test.js.map +0 -1
  513. package/build/index.d.ts +0 -175
  514. package/build/index.js +0 -47
  515. package/build/index.js.map +0 -1
  516. package/build/index.test.js.map +0 -1
  517. package/build/migration.test.js.map +0 -1
  518. package/build/schema/extract-json-schema.d.ts +0 -2
  519. package/build/schema/extract-json-schema.js +0 -12
  520. package/build/schema/extract-json-schema.js.map +0 -1
  521. package/build/schema/extract-json-schema.test.js +0 -23
  522. package/build/schema/extract-json-schema.test.js.map +0 -1
  523. package/build/schema/parser.d.ts +0 -36
  524. package/build/schema/parser.js +0 -210
  525. package/build/schema/parser.js.map +0 -1
  526. package/build/schema/parser.test.js +0 -120
  527. package/build/schema/parser.test.js.map +0 -1
  528. package/build/schema/resolve-schema-lib.d.ts +0 -12
  529. package/build/schema/resolve-schema-lib.js +0 -11
  530. package/build/schema/resolve-schema-lib.js.map +0 -1
  531. package/build/schema/resolve-schema-lib.test.js +0 -17
  532. package/build/schema/resolve-schema-lib.test.js.map +0 -1
  533. package/build/schema/types.d.ts +0 -8
  534. package/build/schema/types.js +0 -2
  535. package/build/stack-utils.d.ts +0 -25
  536. package/build/stack-utils.js.map +0 -1
  537. package/build/stack-utils.test.js.map +0 -1
  538. package/build/types.d.ts +0 -142
  539. package/build/types.js +0 -2
  540. package/build/types.js.map +0 -1
  541. package/docs/decisions/2026-06-02-monorepo-split-evaluation.md +0 -80
  542. package/docs/handoffs/2026-06-08-dx-round2-declines.md +0 -45
  543. package/docs/handoffs/ajsc-named-type-collision.md +0 -134
  544. package/docs/handoffs/ajsc-named-type-support.md +0 -181
  545. package/docs/handoffs/shared-models-auto-resolve-response.md +0 -181
  546. package/docs/npm-workspaces-migration-plan.md +0 -611
  547. package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +0 -886
  548. package/docs/superpowers/plans/2026-04-24-kotlin-codegen-target.md +0 -1265
  549. package/docs/superpowers/plans/2026-04-25-ajsc-v7-kotlin-polish.md +0 -1993
  550. package/docs/superpowers/plans/2026-04-29-safe-result-api.md +0 -2293
  551. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +0 -1391
  552. package/docs/superpowers/plans/2026-05-08-create-http.md +0 -3355
  553. package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +0 -3365
  554. package/docs/superpowers/plans/2026-06-05-dx-feedback-round.md +0 -1292
  555. package/docs/superpowers/plans/2026-06-06-shared-models-convention-and-diagnostics.md +0 -659
  556. package/docs/superpowers/plans/2026-06-08-codegen-dx-surfacing.md +0 -428
  557. package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +0 -401
  558. package/docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md +0 -314
  559. package/docs/superpowers/specs/2026-04-25-swift-codegen-design.md +0 -264
  560. package/docs/superpowers/specs/2026-04-29-safe-result-api-design.md +0 -324
  561. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +0 -252
  562. package/docs/superpowers/specs/2026-05-08-create-http-design.md +0 -409
  563. package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +0 -411
  564. package/docs/superpowers/specs/2026-06-05-dx-feedback-round-design.md +0 -285
  565. package/docs/superpowers/specs/2026-06-08-dx-feedback-round-2-design.md +0 -376
  566. package/src/create-http-stream.ts +0 -191
  567. package/src/create-http.ts +0 -210
  568. package/src/create-stream.ts +0 -228
  569. package/src/create.ts +0 -172
  570. package/src/implementations/http/README.md +0 -390
  571. package/src/implementations/http/error-dispatch.test.ts +0 -283
  572. package/src/implementations/http/error-dispatch.ts +0 -176
  573. package/src/implementations/http/hono/handlers/http-stream.ts +0 -152
  574. package/src/implementations/http/hono/handlers/http.ts +0 -145
  575. package/src/implementations/http/hono/handlers/rpc.ts +0 -54
  576. package/src/implementations/http/hono/path.test.ts +0 -96
  577. package/src/index.ts +0 -101
  578. package/src/schema/extract-json-schema.test.ts +0 -25
  579. package/src/schema/extract-json-schema.ts +0 -15
  580. package/src/schema/parser.test.ts +0 -182
  581. package/src/schema/parser.ts +0 -265
  582. package/src/schema/resolve-schema-lib.test.ts +0 -19
  583. package/src/schema/resolve-schema-lib.ts +0 -29
  584. package/src/schema/types.ts +0 -20
  585. package/src/types.ts +0 -133
  586. /package/build/{implementations/http → adapters}/astro/astro-context.d.ts +0 -0
  587. /package/build/{implementations/http → adapters}/astro/astro-context.js +0 -0
  588. /package/build/{implementations/http → adapters}/astro/create-handler.d.ts +0 -0
  589. /package/build/{implementations/http → adapters}/astro/create-handler.js +0 -0
  590. /package/build/{implementations/http → adapters}/astro/index.d.ts +0 -0
  591. /package/build/{implementations/http → adapters}/astro/index.js +0 -0
  592. /package/build/{implementations/http → adapters}/astro/index.test.d.ts +0 -0
  593. /package/build/{implementations/http → adapters}/astro/rewrite-request.d.ts +0 -0
  594. /package/build/{implementations/http → adapters}/astro/rewrite-request.js +0 -0
  595. /package/build/{create-http-stream.test.d.ts → adapters/hono/envelope-parity.test.d.ts} +0 -0
  596. /package/build/{implementations/http → adapters}/hono/handlers/http-stream.test.d.ts +0 -0
  597. /package/build/{implementations/http → adapters}/hono/handlers/http.test.d.ts +0 -0
  598. /package/build/{implementations/http → adapters}/hono/handlers/rpc.test.d.ts +0 -0
  599. /package/build/{implementations/http → adapters}/hono/handlers/stream.test.d.ts +0 -0
  600. /package/build/{implementations/http → adapters}/hono/index.test.d.ts +0 -0
  601. /package/build/{implementations/http → adapters/hono}/on-request-error.test.d.ts +0 -0
  602. /package/build/{implementations/http → adapters/hono}/route-errors.test.d.ts +0 -0
  603. /package/build/{create-http.test.d.ts → client/freeze.test.d.ts} +0 -0
  604. /package/build/{create-stream.test.d.ts → codegen/goldens.test.d.ts} +0 -0
  605. /package/build/{create.test.d.ts → core/create-http-stream.test.d.ts} +0 -0
  606. /package/build/{doc-envelope.test.d.ts → core/create-http.test.d.ts} +0 -0
  607. /package/build/{errors.test.d.ts → core/create-stream.test.d.ts} +0 -0
  608. /package/build/{implementations/http/doc-registry.test.d.ts → core/create.test.d.ts} +0 -0
  609. /package/build/{implementations/http/error-dispatch.test.d.ts → core/definition-site.test.d.ts} +0 -0
  610. /package/build/{implementations/http/error-taxonomy.test.d.ts → core/errors.test.d.ts} +0 -0
  611. /package/build/{errors.test.js → core/errors.test.js} +0 -0
  612. /package/build/{implementations/http/hono/path.test.d.ts → core/factory-options.test.d.ts} +0 -0
  613. /package/build/{migration.test.d.ts → core/migration.test.d.ts} +0 -0
  614. /package/build/{index.test.d.ts → core/procedures.test.d.ts} +0 -0
  615. /package/build/{implementations/http/hono → core}/types.js +0 -0
  616. /package/build/schema/{extract-json-schema.test.d.ts → adapter.test.d.ts} +0 -0
  617. /package/build/schema/{parser.test.d.ts → compile.test.d.ts} +0 -0
  618. /package/build/schema/{resolve-schema-lib.test.d.ts → typebox.test.d.ts} +0 -0
  619. /package/build/{stack-utils.test.d.ts → server/context.test.d.ts} +0 -0
  620. /package/build/{doc-envelope.js → server/doc-envelope.js} +0 -0
  621. /package/build/{doc-envelope.test.js → server/doc-envelope.test.js} +0 -0
  622. /package/build/{implementations → server}/types.js +0 -0
  623. /package/src/{implementations/http → adapters}/astro/README.md +0 -0
  624. /package/src/{implementations/http → adapters}/astro/astro-context.ts +0 -0
  625. /package/src/{implementations/http → adapters}/astro/create-handler.ts +0 -0
  626. /package/src/{implementations/http → adapters}/astro/index.ts +0 -0
  627. /package/src/{implementations/http → adapters}/astro/rewrite-request.ts +0 -0
@@ -0,0 +1,1319 @@
1
+ // Auto-generated by ts-procedures-codegen (v8.6.0) — do not edit
2
+ // Source hash: cb5f789247045d83e6eb2611821116bb
3
+
4
+ import type {
5
+ ClientAdapter,
6
+ AdapterRequest,
7
+ AdapterResponse,
8
+ AdapterStreamResponse,
9
+ ClientHooks,
10
+ BeforeRequestContext,
11
+ AfterResponseContext,
12
+ ErrorContext,
13
+ CallDescriptor,
14
+ StreamDescriptor,
15
+ TypedStream,
16
+ ClientInstance,
17
+ ProcedureCallDefaults,
18
+ ProcedureCallOptions,
19
+ ClientHeadersInit,
20
+ CreateClientConfig,
21
+ RequestMeta,
22
+ ErrorRegistry,
23
+ ErrorFactory,
24
+ ErrorResponseMeta,
25
+ ErrorClassifier,
26
+ ClassifyErrorContext,
27
+ ClassifiedError,
28
+ Result,
29
+ ResultNoTyped,
30
+ ClientErrorMap,
31
+ FrameworkFailure,
32
+ } from './_types'
33
+
34
+ export class ClientHttpError extends Error {
35
+ readonly name = 'ClientHttpError'
36
+ readonly status: number
37
+ readonly headers: Record<string, string>
38
+ readonly body: unknown
39
+ readonly procedureName: string
40
+ readonly scope: string
41
+
42
+ constructor(opts: {
43
+ status: number
44
+ headers: Record<string, string>
45
+ body: unknown
46
+ procedureName: string
47
+ scope: string
48
+ cause?: unknown
49
+ }) {
50
+ super(
51
+ `${opts.procedureName} (${opts.scope}) failed with status ${opts.status}`,
52
+ { cause: opts.cause }
53
+ )
54
+ this.status = opts.status
55
+ this.headers = opts.headers
56
+ this.body = opts.body
57
+ this.procedureName = opts.procedureName
58
+ this.scope = opts.scope
59
+ }
60
+ }
61
+
62
+ /** @deprecated Renamed to `ClientHttpError`. The alias is retained for one minor release after the 7.0.0 major bump and will be removed in a subsequent minor (e.g. 7.1.0). Migrate to `ClientHttpError` now. */
63
+
64
+ export const ClientRequestError = ClientHttpError
65
+ /** @deprecated Renamed to `ClientHttpError`. The alias is retained for one minor release after the 7.0.0 major bump and will be removed in a subsequent minor (e.g. 7.1.0). Migrate to `ClientHttpError` now. */
66
+ // eslint-disable-next-line no-redeclare
67
+ export type ClientRequestError = ClientHttpError
68
+
69
+ export class ClientPathParamError extends Error {
70
+ readonly name = 'ClientPathParamError'
71
+
72
+ constructor(param: string, path: string, procedureName: string, cause?: unknown) {
73
+ super(`Missing path parameter "${param}" in "${path}" for procedure ${procedureName}`, { cause })
74
+ }
75
+ }
76
+
77
+ export class ClientStreamError extends Error {
78
+ readonly name = 'ClientStreamError'
79
+ readonly procedureName: string
80
+ readonly scope: string
81
+
82
+ constructor(message: string, procedureName: string, scope: string, cause?: unknown) {
83
+ super(message, { cause })
84
+ this.procedureName = procedureName
85
+ this.scope = scope
86
+ }
87
+ }
88
+
89
+ export class ClientNetworkError extends Error {
90
+ readonly name = 'ClientNetworkError'
91
+ readonly procedureName: string
92
+ readonly scope: string
93
+
94
+ constructor(opts: { procedureName: string; scope: string; cause?: unknown; message?: string }) {
95
+ super(opts.message ?? `${opts.procedureName} (${opts.scope}) failed: network error`, { cause: opts.cause })
96
+ this.procedureName = opts.procedureName
97
+ this.scope = opts.scope
98
+ }
99
+ }
100
+
101
+ export class ClientTimeoutError extends Error {
102
+ readonly name = 'ClientTimeoutError'
103
+ readonly procedureName: string
104
+ readonly scope: string
105
+ readonly timeoutMs: number
106
+
107
+ constructor(opts: { procedureName: string; scope: string; timeoutMs: number; cause?: unknown }) {
108
+ super(`${opts.procedureName} (${opts.scope}) timed out after ${opts.timeoutMs}ms`, { cause: opts.cause })
109
+ this.procedureName = opts.procedureName
110
+ this.scope = opts.scope
111
+ this.timeoutMs = opts.timeoutMs
112
+ }
113
+ }
114
+
115
+ export class ClientAbortError extends Error {
116
+ readonly name = 'ClientAbortError'
117
+ readonly procedureName: string
118
+ readonly scope: string
119
+ readonly reason: unknown
120
+
121
+ constructor(opts: { procedureName: string; scope: string; reason?: unknown; cause?: unknown }) {
122
+ super(`${opts.procedureName} (${opts.scope}) aborted`, { cause: opts.cause })
123
+ this.procedureName = opts.procedureName
124
+ this.scope = opts.scope
125
+ this.reason = opts.reason
126
+ }
127
+ }
128
+
129
+ export class ClientParseError extends Error {
130
+ readonly name = 'ClientParseError'
131
+ readonly procedureName: string
132
+ readonly scope: string
133
+
134
+ constructor(opts: { procedureName: string; scope: string; cause?: unknown; message?: string }) {
135
+ super(opts.message ?? `${opts.procedureName} (${opts.scope}) response could not be parsed`, { cause: opts.cause })
136
+ this.procedureName = opts.procedureName
137
+ this.scope = opts.scope
138
+ }
139
+ }
140
+ /**
141
+ * Default classifier — recognizes:
142
+ * - `TypeError` from fetch → `ClientNetworkError`
143
+ * - `DOMException` with `name: 'AbortError'` + timeout-signal-aborted → `ClientTimeoutError`
144
+ * - `DOMException` with `name: 'AbortError'` + user-signal-aborted → `ClientAbortError`
145
+ *
146
+ * Returns `null` for anything else. Timeout precedence: when both signals
147
+ * fired, classifies as `timeout`.
148
+ */
149
+ export const defaultClassifyError: ErrorClassifier = (raw, ctx) => {
150
+ const meta = { procedureName: ctx.procedureName, scope: ctx.scope }
151
+
152
+ if (raw instanceof TypeError) {
153
+ return {
154
+ kind: 'network',
155
+ error: new ClientNetworkError({ ...meta, cause: raw, message: raw.message }),
156
+ }
157
+ }
158
+
159
+ if (
160
+ raw instanceof DOMException &&
161
+ raw.name === 'AbortError'
162
+ ) {
163
+ if (ctx.timeoutSignal?.aborted) {
164
+ return {
165
+ kind: 'timeout',
166
+ error: new ClientTimeoutError({
167
+ ...meta,
168
+ timeoutMs: ctx.timeoutMs ?? 0,
169
+ cause: raw,
170
+ }),
171
+ }
172
+ }
173
+ if (ctx.userSignal?.aborted) {
174
+ return {
175
+ kind: 'aborted',
176
+ error: new ClientAbortError({
177
+ ...meta,
178
+ reason: ctx.userSignal.reason,
179
+ cause: raw,
180
+ }),
181
+ }
182
+ }
183
+ // AbortError without a tracked source — treat as user abort with no reason.
184
+ return {
185
+ kind: 'aborted',
186
+ error: new ClientAbortError({ ...meta, cause: raw }),
187
+ }
188
+ }
189
+
190
+ return null
191
+ }
192
+ /**
193
+ * Attempts to construct a typed error from the response body using the
194
+ * registry. Returns `null` when:
195
+ * - no registry is configured,
196
+ * - the body is not a plain object with a `name` string,
197
+ * - no registry key matches the body's `name`, or
198
+ * - `fromResponse` returns a non-Error value (defensive — registry entries
199
+ * are expected to return `Error` subclasses).
200
+ *
201
+ * Callers fall back to `ClientHttpError` when this returns `null`.
202
+ */
203
+ export function dispatchTypedError(
204
+ registry: ErrorRegistry | undefined,
205
+ body: unknown,
206
+ meta: ErrorResponseMeta
207
+ ): Error | null {
208
+ if (!registry) return null
209
+ if (!body || typeof body !== 'object') return null
210
+ const name = (body as { name?: unknown }).name
211
+ if (typeof name !== 'string') return null
212
+ const factory = registry[name]
213
+ if (!factory?.fromResponse) return null
214
+ const result = factory.fromResponse(body, meta)
215
+ return result instanceof Error ? result : null
216
+ }
217
+ /**
218
+ * Replaces `:paramName` segments in `path` with URI-encoded values from `params`.
219
+ * Throws `ClientPathParamError` if a required segment is missing from `params`.
220
+ */
221
+ export function interpolatePath(
222
+ path: string,
223
+ params: Record<string, unknown>,
224
+ procedureName: string
225
+ ): string {
226
+ return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_match, key: string) => {
227
+ const value = params[key]
228
+ if (value === undefined || value === null) {
229
+ throw new ClientPathParamError(key, path, procedureName)
230
+ }
231
+ return encodeURIComponent(String(value))
232
+ })
233
+ }
234
+
235
+ /**
236
+ * Builds an `AdapterRequest` from a `CallDescriptor` and a base URL.
237
+ *
238
+ * - `kind === 'rpc'` or `kind === 'stream'`: params are flat — sent as the JSON body.
239
+ * - `kind === 'api'` or `kind === 'http-stream'`: params are structured channels — `pathParams`, `query`, `body`, `headers`.
240
+ */
241
+ export function buildAdapterRequest(descriptor: CallDescriptor, basePath: string): AdapterRequest {
242
+ const { name, path, method, kind, params } = descriptor
243
+
244
+ if (kind === 'rpc' || kind === 'stream') {
245
+ return {
246
+ url: `${basePath}${path}`,
247
+ method,
248
+ body: params,
249
+ }
250
+ }
251
+
252
+ // kind === 'api' | 'http-stream' — params are structured channels
253
+ const structured = (params ?? {}) as {
254
+ pathParams?: Record<string, unknown>
255
+ query?: Record<string, unknown>
256
+ body?: unknown
257
+ headers?: Record<string, string>
258
+ }
259
+
260
+ // Interpolate path params
261
+ const interpolatedPath = structured.pathParams
262
+ ? interpolatePath(path, structured.pathParams, name)
263
+ : path
264
+
265
+ // Build query string
266
+ let url = `${basePath}${interpolatedPath}`
267
+ if (structured.query && Object.keys(structured.query).length > 0) {
268
+ const searchParams = new URLSearchParams(
269
+ Object.entries(structured.query).map(([k, v]) => [k, String(v)])
270
+ )
271
+ url = `${url}?${searchParams.toString()}`
272
+ }
273
+
274
+ // Build headers
275
+ const headers =
276
+ structured.headers && Object.keys(structured.headers).length > 0
277
+ ? structured.headers
278
+ : undefined
279
+
280
+ return {
281
+ url,
282
+ method,
283
+ headers,
284
+ body: structured.body,
285
+ }
286
+ }
287
+ /**
288
+ * Resolves the effective base path:
289
+ * per-call `basePath` > default `basePath` > config `basePath` (fallback).
290
+ */
291
+ export function resolveBasePath(
292
+ defaults: ProcedureCallDefaults | undefined,
293
+ options: ProcedureCallOptions | undefined,
294
+ fallback: string,
295
+ ): string {
296
+ return options?.basePath ?? defaults?.basePath ?? fallback
297
+ }
298
+
299
+ /**
300
+ * Resolves signal sources individually so callers can distinguish which
301
+ * underlying signal fired (e.g. timeout vs user abort in the error classifier).
302
+ *
303
+ * - `userSignal`: the per-call signal (if any); separate from the default signal
304
+ * - `timeoutSignal`: created via `AbortSignal.timeout(timeoutMs)` when timeout > 0
305
+ * - `timeoutMs`: the resolved timeout value (may be 0 if per-call explicitly disables)
306
+ * - `combined`: `AbortSignal.any([...])` of all active signals, or undefined if none
307
+ *
308
+ * Per-call `timeout: 0` disables an inherited default timeout (same as `resolveSignal`).
309
+ * Both `defaults.signal` and `options.signal` are included in `combined` when present.
310
+ */
311
+ export interface SignalSources {
312
+ combined?: AbortSignal
313
+ timeoutSignal?: AbortSignal
314
+ /** The per-call signal, if provided. Separate from the default signal. */
315
+ userSignal?: AbortSignal
316
+ timeoutMs?: number
317
+ }
318
+
319
+ export function resolveSignalSources(
320
+ defaults: ProcedureCallDefaults | undefined,
321
+ options: ProcedureCallOptions | undefined,
322
+ ): SignalSources {
323
+ const userSignal = options?.signal
324
+ // Use explicit undefined check so timeout: 0 overrides defaults (not just nullish)
325
+ const timeoutMs = options?.timeout !== undefined ? options.timeout : defaults?.timeout
326
+ const timeoutSignal =
327
+ timeoutMs != null && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined
328
+
329
+ const signals: AbortSignal[] = []
330
+ if (defaults?.signal) signals.push(defaults.signal)
331
+ if (userSignal) signals.push(userSignal)
332
+ if (timeoutSignal) signals.push(timeoutSignal)
333
+
334
+ let combined: AbortSignal | undefined
335
+ if (signals.length === 1) combined = signals[0]
336
+ else if (signals.length > 1) combined = AbortSignal.any(signals)
337
+
338
+ return { combined, timeoutSignal, userSignal, timeoutMs }
339
+ }
340
+
341
+ /**
342
+ * Resolves the effective AbortSignal by combining (via `AbortSignal.any`):
343
+ * - default signal (if any)
344
+ * - per-call signal (if any)
345
+ * - timeout signal (if resolved timeout > 0)
346
+ *
347
+ * Returns undefined when none apply. Per-call `timeout: 0` disables an
348
+ * inherited default timeout.
349
+ *
350
+ * @deprecated Prefer `resolveSignalSources` when you need access to individual
351
+ * signal references (e.g. for abort-cause classification).
352
+ */
353
+ export function resolveSignal(
354
+ defaults: ProcedureCallDefaults | undefined,
355
+ options: ProcedureCallOptions | undefined,
356
+ ): AbortSignal | undefined {
357
+ return resolveSignalSources(defaults, options).combined
358
+ }
359
+
360
+ /**
361
+ * Resolves a `ClientHeadersInit` value: a static record passes through, a
362
+ * function is invoked and (if async) awaited — re-evaluated on every call so
363
+ * values like a rotating bearer token never go stale.
364
+ */
365
+ async function resolveHeadersValue(
366
+ h: ClientHeadersInit | undefined,
367
+ ): Promise<Record<string, string> | undefined> {
368
+ if (h == null) return undefined
369
+ return typeof h === 'function' ? await h() : h
370
+ }
371
+
372
+ /**
373
+ * Merges headers with precedence: default < per-call. Function-valued headers
374
+ * are evaluated (and awaited) per call. Returns undefined if no headers would
375
+ * be set.
376
+ */
377
+ export async function resolveHeaders(
378
+ defaults: ProcedureCallDefaults | undefined,
379
+ options: ProcedureCallOptions | undefined,
380
+ ): Promise<Record<string, string> | undefined> {
381
+ const defaultHeaders = await resolveHeadersValue(defaults?.headers)
382
+ const callHeaders = await resolveHeadersValue(options?.headers)
383
+
384
+ if (!defaultHeaders && !callHeaders) return undefined
385
+
386
+ return { ...defaultHeaders, ...callHeaders }
387
+ }
388
+
389
+ /**
390
+ * Merges meta with precedence: default < per-call. Returns undefined if
391
+ * no meta fields would be set.
392
+ *
393
+ * The cast is load-bearing: when a developer augments `RequestMeta` with
394
+ * required fields, spread of two `RequestMeta | undefined` values widens to
395
+ * a partial shape, which TypeScript can't prove satisfies `RequestMeta`.
396
+ * At runtime, the merged object carries whichever keys the caller supplied —
397
+ * the contract is "if you declare required fields in RequestMeta, supply them
398
+ * somewhere (defaults or per-call)."
399
+ */
400
+ export function resolveMeta(
401
+ defaults: ProcedureCallDefaults | undefined,
402
+ options: ProcedureCallOptions | undefined,
403
+ ): RequestMeta | undefined {
404
+ const defaultMeta = defaults?.meta
405
+ const callMeta = options?.meta
406
+
407
+ if (!defaultMeta && !callMeta) return undefined
408
+
409
+ return { ...defaultMeta, ...callMeta } as RequestMeta
410
+ }
411
+
412
+ export interface ApplyRequestOptionsResult {
413
+ request: AdapterRequest
414
+ signalSources: SignalSources
415
+ }
416
+
417
+ /**
418
+ * Applies resolved default + per-call options to an AdapterRequest.
419
+ *
420
+ * Runs before hooks, so `onBeforeRequest` observes the merged request and can
421
+ * still override any field.
422
+ *
423
+ * Headers produced by the request builder (e.g., `schema.input.headers` for
424
+ * API routes) are preserved; resolved headers merge underneath them so the
425
+ * route-declared headers win, matching the adapter.config → defaults → call
426
+ * → route-declared → hooks precedence chain documented in the types.
427
+ *
428
+ * Returns both the merged request and the raw `signalSources` so callers (e.g.
429
+ * the error classifier in Task 6) can distinguish timeout from user abort without
430
+ * losing provenance after `AbortSignal.any` collapses the originals.
431
+ */
432
+ export async function applyRequestOptions(
433
+ request: AdapterRequest,
434
+ defaults: ProcedureCallDefaults | undefined,
435
+ options: ProcedureCallOptions | undefined,
436
+ ): Promise<ApplyRequestOptionsResult> {
437
+ const signalSources = resolveSignalSources(defaults, options)
438
+ const resolvedHeaders = await resolveHeaders(defaults, options)
439
+ const meta = resolveMeta(defaults, options)
440
+
441
+ const headers =
442
+ resolvedHeaders || request.headers
443
+ ? { ...resolvedHeaders, ...request.headers }
444
+ : undefined
445
+
446
+ return {
447
+ request: { ...request, headers, signal: signalSources.combined, meta },
448
+ signalSources,
449
+ }
450
+ }
451
+ /**
452
+ * Runs `onBeforeRequest` hooks: global first, then per-procedure.
453
+ * Each hook receives the (possibly mutated) context from the previous hook.
454
+ * Returns the final context.
455
+ */
456
+ export async function runBeforeRequest(
457
+ ctx: BeforeRequestContext,
458
+ globalHooks: ClientHooks,
459
+ localHooks: ClientHooks | undefined
460
+ ): Promise<BeforeRequestContext> {
461
+ let current = ctx
462
+
463
+ if (globalHooks.onBeforeRequest) {
464
+ current = await globalHooks.onBeforeRequest(current)
465
+ }
466
+
467
+ if (localHooks?.onBeforeRequest) {
468
+ current = await localHooks.onBeforeRequest(current)
469
+ }
470
+
471
+ return current
472
+ }
473
+
474
+ /**
475
+ * Runs `onAfterResponse` hooks: global first, then per-procedure.
476
+ * Returns void.
477
+ */
478
+ export async function runAfterResponse(
479
+ ctx: AfterResponseContext,
480
+ globalHooks: ClientHooks,
481
+ localHooks: ClientHooks | undefined
482
+ ): Promise<void> {
483
+ if (globalHooks.onAfterResponse) {
484
+ await globalHooks.onAfterResponse(ctx)
485
+ }
486
+
487
+ if (localHooks?.onAfterResponse) {
488
+ await localHooks.onAfterResponse(ctx)
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Runs `onError` hooks: global first, then per-procedure.
494
+ * Returns void.
495
+ */
496
+ export async function runOnError(
497
+ ctx: ErrorContext,
498
+ globalHooks: ClientHooks,
499
+ localHooks: ClientHooks | undefined
500
+ ): Promise<void> {
501
+ if (globalHooks.onError) {
502
+ await globalHooks.onError(ctx)
503
+ }
504
+
505
+ if (localHooks?.onError) {
506
+ await localHooks.onError(ctx)
507
+ }
508
+ }
509
+ export interface ExecuteCallConfig {
510
+ descriptor: CallDescriptor
511
+ basePath: string
512
+ adapter: ClientAdapter
513
+ hooks: ClientHooks
514
+ defaults?: ProcedureCallDefaults
515
+ options?: ProcedureCallOptions
516
+ errorRegistry?: ErrorRegistry
517
+ }
518
+
519
+ /**
520
+ * Executes a single procedure call through the adapter.
521
+ *
522
+ * Flow:
523
+ * 1. Resolve base path (per-call > defaults > config) and build AdapterRequest
524
+ * 2. Apply request options (headers, signal, timeout, meta) from defaults + per-call
525
+ * 3. Run onBeforeRequest hooks (global then local) — may further mutate request
526
+ * 4. Call adapter.request()
527
+ * 5. On adapter error: run onError hooks, re-throw
528
+ * 6. Run onAfterResponse hooks (may mutate response.status to swallow errors)
529
+ * 7. If response status is non-2xx: throw ClientHttpError
530
+ * 8. Return response.body as TResponse
531
+ */
532
+ export async function executeCall<TResponse>(config: ExecuteCallConfig): Promise<TResponse> {
533
+ const { descriptor, basePath, adapter, hooks, defaults, options, errorRegistry } = config
534
+
535
+ // 1. Build the initial request (path/query/body from descriptor)
536
+ const resolvedBasePath = resolveBasePath(defaults, options, basePath)
537
+ let request = buildAdapterRequest(descriptor, resolvedBasePath)
538
+
539
+ // 2. Apply request-level options (headers, signal, timeout, meta)
540
+ const applied = await applyRequestOptions(request, defaults, options)
541
+ request = applied.request
542
+ const signalSources = applied.signalSources
543
+
544
+ // 3. Run before-request hooks — they may further mutate the request
545
+ const beforeCtx = await runBeforeRequest(
546
+ { procedureName: descriptor.name, scope: descriptor.scope, request },
547
+ hooks,
548
+ options,
549
+ )
550
+ request = beforeCtx.request
551
+
552
+ // 4. Call the adapter
553
+ let response
554
+ try {
555
+ response = await adapter.request(request)
556
+ } catch (rawErr) {
557
+ // 5. On adapter error: classify (adapter > default > fallthrough), then run
558
+ // onError hooks with the normalized error, then throw.
559
+ const classifyCtx: ClassifyErrorContext = {
560
+ procedureName: descriptor.name,
561
+ scope: descriptor.scope,
562
+ timeoutSignal: signalSources.timeoutSignal,
563
+ userSignal: signalSources.userSignal,
564
+ timeoutMs: signalSources.timeoutMs,
565
+ }
566
+ const classified =
567
+ adapter.classifyError?.(rawErr, classifyCtx) ??
568
+ defaultClassifyError(rawErr, classifyCtx)
569
+ // Tag the classified error so executeSafeCall can identify its kind without
570
+ // re-classifying. Default kinds are recognized via instanceof in
571
+ // classifyThrownError; only custom adapter kinds need an explicit tag.
572
+ if (classified) {
573
+ const defaultKinds = new Set(['network', 'timeout', 'aborted', 'parse'])
574
+ if (!defaultKinds.has(classified.kind)) {
575
+ ;(classified.error as unknown as { __tsProceduresKind?: string }).__tsProceduresKind = classified.kind
576
+ }
577
+ }
578
+ const finalError = classified?.error ?? rawErr
579
+
580
+ await runOnError(
581
+ { procedureName: descriptor.name, scope: descriptor.scope, request, error: finalError },
582
+ hooks,
583
+ options,
584
+ )
585
+ throw finalError
586
+ }
587
+
588
+ // 6. Run after-response hooks — they may mutate response.status to swallow errors
589
+ await runAfterResponse(
590
+ { procedureName: descriptor.name, scope: descriptor.scope, request, response },
591
+ hooks,
592
+ options,
593
+ )
594
+
595
+ // 7. Check status AFTER hooks (hooks may have swallowed the error by mutating status)
596
+ if (response.status < 200 || response.status >= 300) {
597
+ const typed = dispatchTypedError(errorRegistry, response.body, {
598
+ status: response.status,
599
+ procedureName: descriptor.name,
600
+ scope: descriptor.scope,
601
+ })
602
+ if (typed) {
603
+ // Tag so executeSafeCall can distinguish typed registry errors from plain
604
+ // ClientHttpError without re-inspecting the registry.
605
+ ;(typed as unknown as { __tsProceduresTyped?: boolean }).__tsProceduresTyped = true
606
+ throw typed
607
+ }
608
+ throw new ClientHttpError({
609
+ status: response.status,
610
+ headers: response.headers,
611
+ body: response.body,
612
+ procedureName: descriptor.name,
613
+ scope: descriptor.scope,
614
+ })
615
+ }
616
+
617
+ // 8. Return the body (or { body, headers } when the route declares res.headers)
618
+ if (descriptor.responseHeadersDeclared) {
619
+ return { body: response.body, headers: response.headers } as TResponse
620
+ }
621
+ return response.body as TResponse
622
+ }
623
+
624
+ /**
625
+ * Wraps `executeCall` and returns a discriminated `Result` instead of throwing.
626
+ *
627
+ * Three failure-source paths map to distinct kinds:
628
+ * 1. Pre-adapter throw (e.g. `ClientPathParamError`) → `kind: 'usage'`
629
+ * 2. Adapter throw, classified → `kind: 'network' | 'timeout' | 'aborted' | <custom> | 'unknown'`
630
+ * 3. Adapter returns non-2xx → `kind: 'typed'` (registry match) or `kind: 'http'`
631
+ *
632
+ * `onError` hook fires on path 2 and 3 (cross-cutting telemetry); NOT on
633
+ * path 1 (usage errors bypass the classifier and onError entirely).
634
+ */
635
+ export async function executeSafeCall<TResponse, ETyped = never>(
636
+ config: ExecuteCallConfig,
637
+ ): Promise<Result<TResponse, ETyped>> {
638
+ try {
639
+ const value = await executeCall<TResponse>(config)
640
+ return { ok: true, value }
641
+ } catch (err) {
642
+ return classifyThrownError<ETyped>(err)
643
+ }
644
+ }
645
+
646
+ function classifyThrownError<ETyped>(err: unknown): Result<never, ETyped> {
647
+ // Path 1: pre-adapter usage error — bypasses classifier and onError
648
+ if (err instanceof ClientPathParamError) {
649
+ return { ok: false, kind: 'usage', error: err }
650
+ }
651
+ // Path 3: post-status-check typed registry match — tagged by executeCall
652
+ if (err instanceof Error && (err as unknown as { __tsProceduresTyped?: boolean }).__tsProceduresTyped) {
653
+ return { ok: false, kind: 'typed', error: err as ETyped }
654
+ }
655
+ // Path 3: non-2xx fallback (no registry match)
656
+ if (err instanceof ClientHttpError) {
657
+ return { ok: false, kind: 'http', error: err }
658
+ }
659
+ // Path 2: classifier output (already normalized by executeCall)
660
+ if (err instanceof ClientNetworkError) {
661
+ return { ok: false, kind: 'network', error: err }
662
+ }
663
+ if (err instanceof ClientTimeoutError) {
664
+ return { ok: false, kind: 'timeout', error: err }
665
+ }
666
+ if (err instanceof ClientAbortError) {
667
+ return { ok: false, kind: 'aborted', error: err }
668
+ }
669
+ if (err instanceof ClientParseError) {
670
+ return { ok: false, kind: 'parse', error: err }
671
+ }
672
+ // Custom adapter-classified error — tagged with its kind by executeCall.
673
+ // The cast is intentional: the framework knows only the default ClientErrorMap
674
+ // keys, but consumer-augmented kinds are valid at the consumer's site (where
675
+ // the augmented map is in scope). The runtime kind string is whatever the
676
+ // adapter classifier returned; we trust it to match a registered entry.
677
+ if (err instanceof Error && typeof (err as unknown as { __tsProceduresKind?: string }).__tsProceduresKind === 'string') {
678
+ const kind = (err as unknown as { __tsProceduresKind: string }).__tsProceduresKind
679
+ return { ok: false, kind: kind as keyof ClientErrorMap, error: err } as FrameworkFailure
680
+ }
681
+ // Fallthrough — unrecognized throw type (non-Error or unclassified)
682
+ return { ok: false, kind: 'unknown', error: err }
683
+ }
684
+ // ── SSE item shape ────────────────────────────────────────
685
+
686
+ interface SSEItem {
687
+ data: unknown
688
+ event?: string
689
+ id?: string
690
+ }
691
+
692
+ // ── createTypedStream ─────────────────────────────────────
693
+
694
+ /**
695
+ * Optional context for SSE-mode typed-error dispatch. When supplied,
696
+ * `event: 'error'` items are decoded through the registry instead of being
697
+ * yielded as data — giving streams the same typed-error contract as RPC/API
698
+ * calls. See `dispatchTypedError`.
699
+ */
700
+ export interface CreateTypedStreamOptions {
701
+ errorRegistry?: ErrorRegistry
702
+ /** Status to attach to typed-error meta when an `event: 'error'` is decoded. Defaults to 200 (the response was OK; the error was inline). */
703
+ errorStatus?: number
704
+ procedureName?: string
705
+ scope?: string
706
+ }
707
+
708
+ /**
709
+ * Wraps an AsyncIterable into a TypedStream.
710
+ *
711
+ * SSE mode: each item is `{ data, event?, id? }`.
712
+ * - If `event === 'return'`, the data resolves `.result` and is NOT yielded.
713
+ * - If `event === 'error'`, the data is dispatched through the optional
714
+ * `errorRegistry` (when supplied) — the resulting typed error rejects
715
+ * `.result` and is thrown from the iterator. Falls back to
716
+ * `ClientStreamError` when no registry entry matches.
717
+ * - Otherwise, `data` is yielded.
718
+ *
719
+ * Text mode: each item is yielded as-is.
720
+ * - `.result` resolves to `void` on completion.
721
+ *
722
+ * On error: `.result` rejects and the error is re-thrown from the async iterator.
723
+ *
724
+ * The internal `.result` promise gets a no-op rejection handler attached so
725
+ * that consumers who only iterate (and never `await stream.result`) don't
726
+ * trigger an unhandled-rejection warning under Node's strict runners
727
+ * (e.g. node:test). Consumers that DO await `.result` still observe the
728
+ * rejection — `.catch(() => undefined)` returns a new promise without
729
+ * suppressing the original.
730
+ */
731
+ export function createTypedStream<TYield, TReturn = void>(
732
+ source: AsyncIterable<unknown>,
733
+ streamMode: 'sse' | 'text',
734
+ options?: CreateTypedStreamOptions
735
+ ): TypedStream<TYield, TReturn> {
736
+ let resolveResult: (value: TReturn) => void
737
+ let rejectResult: (reason: unknown) => void
738
+
739
+ const resultPromise = new Promise<TReturn>((resolve, reject) => {
740
+ resolveResult = resolve
741
+ rejectResult = reject
742
+ })
743
+
744
+ // Attach a no-op rejection sink so unhandled-rejection trackers don't fire
745
+ // when the iterator throws but the consumer never `await`s `.result`. This
746
+ // is independent of the public promise — consumers awaiting `.result` still
747
+ // see the rejection.
748
+ resultPromise.catch(() => undefined)
749
+
750
+ async function* generate(): AsyncGenerator<TYield> {
751
+ try {
752
+ if (streamMode === 'sse') {
753
+ let returnValue: TReturn | undefined
754
+ let hasReturn = false
755
+
756
+ for await (const item of source) {
757
+ const sseItem = item as SSEItem
758
+ if (sseItem.event === 'return') {
759
+ returnValue = sseItem.data as TReturn
760
+ hasReturn = true
761
+ } else if (sseItem.event === 'error') {
762
+ // Mid-stream typed error. Mirror the call.ts dispatch contract:
763
+ // body shape is `{ name, ... }` and the registry maps `name` →
764
+ // typed class. When no registry / no match, surface a
765
+ // ClientStreamError carrying the raw body.
766
+ const typed = dispatchTypedError(options?.errorRegistry, sseItem.data, {
767
+ status: options?.errorStatus ?? 200,
768
+ procedureName: options?.procedureName ?? '',
769
+ scope: options?.scope ?? '',
770
+ })
771
+ if (typed) throw typed
772
+ const message = (() => {
773
+ const data = sseItem.data
774
+ if (data && typeof data === 'object' && 'message' in data && typeof (data as { message: unknown }).message === 'string') {
775
+ return (data as { message: string }).message
776
+ }
777
+ return `Stream error event for ${options?.procedureName ?? 'procedure'}`
778
+ })()
779
+ throw new ClientStreamError(
780
+ message,
781
+ options?.procedureName ?? '',
782
+ options?.scope ?? '',
783
+ sseItem.data,
784
+ )
785
+ } else {
786
+ yield sseItem.data as TYield
787
+ }
788
+ }
789
+
790
+ // Resolve result after iteration completes
791
+ if (hasReturn) {
792
+ resolveResult(returnValue as TReturn)
793
+ } else {
794
+ resolveResult(undefined as TReturn)
795
+ }
796
+ } else {
797
+ // text mode: yield each item as-is
798
+ for await (const item of source) {
799
+ yield item as TYield
800
+ }
801
+ resolveResult(undefined as TReturn)
802
+ }
803
+ } catch (err) {
804
+ rejectResult(err)
805
+ throw err
806
+ }
807
+ }
808
+
809
+ const iterator = generate()
810
+
811
+ return {
812
+ [Symbol.asyncIterator]() {
813
+ return iterator
814
+ },
815
+ result: resultPromise,
816
+ }
817
+ }
818
+
819
+ // ── executeStream ─────────────────────────────────────────
820
+
821
+ export interface ExecuteStreamConfig {
822
+ descriptor: StreamDescriptor
823
+ basePath: string
824
+ adapter: ClientAdapter
825
+ hooks: ClientHooks
826
+ defaults?: ProcedureCallDefaults
827
+ options?: ProcedureCallOptions
828
+ errorRegistry?: ErrorRegistry
829
+ }
830
+
831
+ /**
832
+ * Executes a streaming procedure call through the adapter.
833
+ *
834
+ * Flow:
835
+ * 1. Resolve base path and build AdapterRequest
836
+ * 2. Apply request options (headers, signal, timeout, meta) from defaults + per-call
837
+ * 3. Run onBeforeRequest hooks
838
+ * 4. Call adapter.stream()
839
+ * 5. On adapter error: run onError hooks, re-throw
840
+ * 6. Run onAfterResponse immediately (before iteration), body is null
841
+ * 7. If non-2xx: throw ClientHttpError
842
+ * 8. Return createTypedStream(streamResponse.body, descriptor.streamMode)
843
+ */
844
+ export async function executeStream<TYield, TReturn = void>(
845
+ config: ExecuteStreamConfig,
846
+ ): Promise<TypedStream<TYield, TReturn>> {
847
+ const { descriptor, basePath, adapter, hooks, defaults, options, errorRegistry } = config
848
+
849
+ // 1. Build the initial request
850
+ const resolvedBasePath = resolveBasePath(defaults, options, basePath)
851
+ let request = buildAdapterRequest(descriptor, resolvedBasePath)
852
+
853
+ // 2. Apply request-level options (headers, signal, timeout, meta)
854
+ const applied = await applyRequestOptions(request, defaults, options)
855
+ request = applied.request
856
+ const signalSources = applied.signalSources
857
+
858
+ // 3. Run before-request hooks
859
+ const beforeCtx = await runBeforeRequest(
860
+ { procedureName: descriptor.name, scope: descriptor.scope, request },
861
+ hooks,
862
+ options,
863
+ )
864
+ request = beforeCtx.request
865
+
866
+ // 4. Call the adapter
867
+ let streamResponse
868
+ try {
869
+ streamResponse = await adapter.stream(request)
870
+ } catch (rawErr) {
871
+ // 5. On adapter error: classify (adapter > default > fallthrough), then run
872
+ // onError hooks with the normalized error, then throw.
873
+ const classifyCtx: ClassifyErrorContext = {
874
+ procedureName: descriptor.name,
875
+ scope: descriptor.scope,
876
+ timeoutSignal: signalSources.timeoutSignal,
877
+ userSignal: signalSources.userSignal,
878
+ timeoutMs: signalSources.timeoutMs,
879
+ }
880
+ const classified =
881
+ adapter.classifyError?.(rawErr, classifyCtx) ??
882
+ defaultClassifyError(rawErr, classifyCtx)
883
+ const finalError = classified?.error ?? rawErr
884
+
885
+ await runOnError(
886
+ { procedureName: descriptor.name, scope: descriptor.scope, request, error: finalError },
887
+ hooks,
888
+ options,
889
+ )
890
+ throw finalError
891
+ }
892
+
893
+ // Convert the platform Headers object to a plain record for the hooks/error
894
+ // context (AdapterResponse.headers is Record<string, string>). The raw
895
+ // platform Headers object is kept separately for TypedStream.headers.
896
+ const headersRecord: Record<string, string> = {}
897
+ streamResponse.headers.forEach((value, key) => { headersRecord[key] = value })
898
+
899
+ // Build an AdapterResponse shape for the hooks. For success the body is null
900
+ // (the actual data flows through the async iterable); for non-2xx the adapter
901
+ // eagerly parses the JSON response body and surfaces it via `errorBody`.
902
+ const responseForHooks: AdapterResponse = {
903
+ status: streamResponse.status,
904
+ headers: headersRecord,
905
+ body: streamResponse.errorBody ?? null,
906
+ }
907
+
908
+ // 6. Run after-response hooks immediately (before iteration)
909
+ await runAfterResponse(
910
+ { procedureName: descriptor.name, scope: descriptor.scope, request, response: responseForHooks },
911
+ hooks,
912
+ options,
913
+ )
914
+
915
+ // 7. Check status after hooks (hooks may mutate responseForHooks.status)
916
+ if (responseForHooks.status < 200 || responseForHooks.status >= 300) {
917
+ const typed = dispatchTypedError(errorRegistry, responseForHooks.body, {
918
+ status: responseForHooks.status,
919
+ procedureName: descriptor.name,
920
+ scope: descriptor.scope,
921
+ })
922
+ if (typed) throw typed
923
+ throw new ClientHttpError({
924
+ status: responseForHooks.status,
925
+ headers: responseForHooks.headers,
926
+ body: responseForHooks.body,
927
+ procedureName: descriptor.name,
928
+ scope: descriptor.scope,
929
+ })
930
+ }
931
+
932
+ // 8. Return the typed stream — pass the registry/descriptor through so
933
+ // mid-stream `event: 'error'` items dispatch through the same registry as
934
+ // RPC/API calls.
935
+ const typedStream = createTypedStream<TYield, TReturn>(streamResponse.body, descriptor.streamMode, {
936
+ errorRegistry,
937
+ errorStatus: responseForHooks.status,
938
+ procedureName: descriptor.name,
939
+ scope: descriptor.scope,
940
+ })
941
+
942
+ // Wire the initial response headers onto the stream when the route declares
943
+ // res.headers. The platform Headers object is passed directly from the adapter.
944
+ if (descriptor.responseHeadersDeclared) {
945
+ typedStream.headers = streamResponse.headers
946
+ }
947
+
948
+ return typedStream
949
+ }
950
+ // ── Config ────────────────────────────────────────────────
951
+
952
+ export interface FetchAdapterConfig {
953
+ headers?: Record<string, string>
954
+ classifyError?: ErrorClassifier
955
+ }
956
+
957
+ // ── SSE parser ────────────────────────────────────────────
958
+
959
+ interface SSEEvent {
960
+ data: unknown
961
+ event?: string
962
+ id?: string
963
+ }
964
+
965
+ /**
966
+ * Parses an SSE message block (the text between double-newlines).
967
+ * Returns null if there is no data field (e.g., comment-only blocks).
968
+ */
969
+ function parseSSEBlock(block: string): SSEEvent | null {
970
+ const lines = block.split('\n')
971
+ let event: string | undefined
972
+ let id: string | undefined
973
+ const dataParts: string[] = []
974
+
975
+ for (const line of lines) {
976
+ if (line.startsWith('event:')) {
977
+ event = line.slice('event:'.length).trim()
978
+ } else if (line.startsWith('data:')) {
979
+ dataParts.push(line.slice('data:'.length).trimStart())
980
+ } else if (line.startsWith('id:')) {
981
+ id = line.slice('id:'.length).trim()
982
+ }
983
+ // Lines starting with ':' are comments — skip them
984
+ }
985
+
986
+ if (dataParts.length === 0) {
987
+ return null
988
+ }
989
+
990
+ const dataStr = dataParts.join('\n')
991
+ let data: unknown
992
+ try {
993
+ data = JSON.parse(dataStr)
994
+ } catch {
995
+ data = dataStr
996
+ }
997
+
998
+ return { data, event, id }
999
+ }
1000
+
1001
+ /**
1002
+ * Async generator that reads a ReadableStream<Uint8Array>, buffers text,
1003
+ * splits on double-newline SSE boundaries, and yields parsed SSE events.
1004
+ */
1005
+ async function* parseSseStream(
1006
+ readableStream: ReadableStream<Uint8Array>
1007
+ ): AsyncGenerator<SSEEvent> {
1008
+ const reader = readableStream.getReader()
1009
+ const decoder = new TextDecoder()
1010
+ let buffer = ''
1011
+
1012
+ try {
1013
+ while (true) {
1014
+ const { done, value } = await reader.read()
1015
+
1016
+ if (value) {
1017
+ buffer += decoder.decode(value, { stream: !done })
1018
+ }
1019
+
1020
+ // Process all complete SSE message blocks (split on \n\n)
1021
+ let boundary: number
1022
+ while ((boundary = buffer.indexOf('\n\n')) !== -1) {
1023
+ const block = buffer.slice(0, boundary).trim()
1024
+ buffer = buffer.slice(boundary + 2)
1025
+
1026
+ if (block.length > 0) {
1027
+ const event = parseSSEBlock(block)
1028
+ if (event !== null) {
1029
+ yield event
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ if (done) break
1035
+ }
1036
+
1037
+ // Handle any remaining buffer content (no trailing \n\n)
1038
+ const remaining = buffer.trim()
1039
+ if (remaining.length > 0) {
1040
+ const event = parseSSEBlock(remaining)
1041
+ if (event !== null) {
1042
+ yield event
1043
+ }
1044
+ }
1045
+ } finally {
1046
+ reader.releaseLock()
1047
+ }
1048
+ }
1049
+
1050
+ // ── Adapter ───────────────────────────────────────────────
1051
+
1052
+ /**
1053
+ * Extracts response headers as a plain Record<string, string>.
1054
+ */
1055
+ function extractHeaders(response: Response): Record<string, string> {
1056
+ const headers: Record<string, string> = {}
1057
+ response.headers.forEach((value, key) => {
1058
+ headers[key] = value
1059
+ })
1060
+ return headers
1061
+ }
1062
+
1063
+ /**
1064
+ * Attempts to parse the response body as JSON, then as text, then returns null.
1065
+ */
1066
+ async function parseResponseBody(response: Response): Promise<unknown> {
1067
+ // Clone so we can attempt multiple reads
1068
+ const clone = response.clone()
1069
+ try {
1070
+ return await clone.json()
1071
+ } catch {
1072
+ try {
1073
+ const text = await response.text()
1074
+ return text || null
1075
+ } catch {
1076
+ return null
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ /**
1082
+ * Creates a fetch-based ClientAdapter.
1083
+ *
1084
+ * - `config.headers` are default headers applied to every request.
1085
+ * - Per-request headers override config headers (spread order).
1086
+ * - Works in Node.js 18+ and browsers (uses standard fetch + ReadableStream).
1087
+ */
1088
+ export function createFetchAdapter(config?: FetchAdapterConfig): ClientAdapter {
1089
+ const configHeaders = config?.headers ?? {}
1090
+
1091
+ return {
1092
+ async request(req: AdapterRequest): Promise<AdapterResponse> {
1093
+ const mergedHeaders: Record<string, string> = {
1094
+ ...configHeaders,
1095
+ ...(req.headers ?? {}),
1096
+ }
1097
+
1098
+ const response = await fetch(req.url, {
1099
+ method: req.method,
1100
+ headers: mergedHeaders,
1101
+ body: req.body !== undefined ? JSON.stringify(req.body) : undefined,
1102
+ signal: req.signal,
1103
+ })
1104
+
1105
+ const headers = extractHeaders(response)
1106
+ const body = await parseResponseBody(response)
1107
+
1108
+ return { status: response.status, headers, body }
1109
+ },
1110
+
1111
+ async stream(req: AdapterRequest): Promise<AdapterStreamResponse> {
1112
+ const mergedHeaders: Record<string, string> = {
1113
+ ...configHeaders,
1114
+ ...(req.headers ?? {}),
1115
+ }
1116
+
1117
+ const response = await fetch(req.url, {
1118
+ method: req.method,
1119
+ headers: mergedHeaders,
1120
+ body: req.body !== undefined ? JSON.stringify(req.body) : undefined,
1121
+ signal: req.signal,
1122
+ })
1123
+
1124
+ // Expose the platform Headers object directly — callers that declared
1125
+ // res.headers receive it on TypedStream.headers without any conversion.
1126
+ const { headers } = response
1127
+ const emptyBody: AsyncIterable<unknown> = {
1128
+ [Symbol.asyncIterator]: async function* () {},
1129
+ }
1130
+
1131
+ // Non-2xx responses on a stream endpoint are JSON, not SSE. Parse the
1132
+ // body eagerly and surface it via errorBody so the client can dispatch
1133
+ // a typed error (or fall back to ClientHttpError with a real body).
1134
+ if (response.status < 200 || response.status >= 300) {
1135
+ const errorBody = await parseResponseBody(response)
1136
+ return { status: response.status, headers, body: emptyBody, errorBody }
1137
+ }
1138
+
1139
+ if (!response.body) {
1140
+ return { status: response.status, headers, body: emptyBody }
1141
+ }
1142
+
1143
+ const body = parseSseStream(response.body as ReadableStream<Uint8Array>)
1144
+ return { status: response.status, headers, body }
1145
+ },
1146
+
1147
+ classifyError: config?.classifyError,
1148
+ }
1149
+ }
1150
+ // ── createClient ──────────────────────────────────────────
1151
+
1152
+ /**
1153
+ * Folds `config.auth` into the resolved default headers as a single async
1154
+ * function-valued `ClientHeadersInit`. Resolves the user's existing default
1155
+ * headers (record OR function) to a record first, then appends
1156
+ * `Authorization: Bearer <token>` when `auth()` yields a non-null token.
1157
+ * Re-evaluated per request, so a rotating token never goes stale.
1158
+ */
1159
+ function composeAuthHeaders(
1160
+ base: ClientHeadersInit | undefined,
1161
+ auth: NonNullable<CreateClientConfig<unknown>['auth']>,
1162
+ ): ClientHeadersInit {
1163
+ return async () => {
1164
+ const resolvedBase = base == null ? {} : typeof base === 'function' ? await base() : base
1165
+ const token = await auth()
1166
+ return token ? { ...resolvedBase, Authorization: `Bearer ${token}` } : resolvedBase
1167
+ }
1168
+ }
1169
+
1170
+ /**
1171
+ * Creates a typed client from a config object.
1172
+ *
1173
+ * The `scopes` callback receives a `ClientInstance` and returns the typed
1174
+ * scope bindings (e.g., `{ users: { getUser, createUser }, posts: { ... } }`).
1175
+ * The return value of `createClient` is the scopes object.
1176
+ *
1177
+ * `client.stream()` must return `TypedStream` synchronously even though
1178
+ * `executeStream` is async. We achieve this by creating a deferred TypedStream:
1179
+ * - A deferred async generator awaits `executeStream` internally, then forwards
1180
+ * yields from the inner stream.
1181
+ * - The outer `.result` is wired up to the inner stream's `.result`.
1182
+ */
1183
+ export function createClient<TScopes>(config: CreateClientConfig<TScopes>): TScopes {
1184
+ const {
1185
+ adapter,
1186
+ basePath,
1187
+ hooks: globalHooks = {},
1188
+ defaults: configDefaults = {},
1189
+ errorRegistry,
1190
+ auth,
1191
+ scopes,
1192
+ } = config
1193
+
1194
+ // `auth` is sugar over a function-valued `defaults.headers`: fold it into the
1195
+ // resolved default headers as an async function so it shares the single
1196
+ // per-request header-resolution path (no new resolution branch). The user's
1197
+ // existing `defaults.headers` (record or function) is resolved first, then the
1198
+ // bearer token is appended — a null/undefined token omits the header.
1199
+ const globalDefaults: ProcedureCallDefaults = auth
1200
+ ? { ...configDefaults, headers: composeAuthHeaders(configDefaults.headers, auth) }
1201
+ : configDefaults
1202
+
1203
+ const instance: ClientInstance = {
1204
+ basePath,
1205
+ adapter,
1206
+ hooks: globalHooks,
1207
+ defaults: globalDefaults,
1208
+ errorRegistry,
1209
+
1210
+ call<TResponse>(
1211
+ descriptor: CallDescriptor,
1212
+ options?: ProcedureCallOptions,
1213
+ ): Promise<TResponse> {
1214
+ return executeCall<TResponse>({
1215
+ descriptor,
1216
+ basePath,
1217
+ adapter,
1218
+ hooks: globalHooks,
1219
+ defaults: globalDefaults,
1220
+ options,
1221
+ errorRegistry,
1222
+ })
1223
+ },
1224
+
1225
+ safeCall<TResponse, ETyped = never>(
1226
+ descriptor: CallDescriptor,
1227
+ options?: ProcedureCallOptions,
1228
+ ): Promise<Result<TResponse, ETyped>> {
1229
+ return executeSafeCall<TResponse, ETyped>({
1230
+ descriptor,
1231
+ basePath,
1232
+ adapter,
1233
+ hooks: globalHooks,
1234
+ defaults: globalDefaults,
1235
+ options,
1236
+ errorRegistry,
1237
+ })
1238
+ },
1239
+
1240
+ bindCallable<TParams, TResponse>(descriptor: Omit<CallDescriptor, 'params'>) {
1241
+ const call = (params: TParams, options?: ProcedureCallOptions) =>
1242
+ instance.call<TResponse>({ ...descriptor, params }, options)
1243
+ Object.defineProperty(call, 'name', { value: descriptor.name, configurable: true })
1244
+ return Object.assign(call, {
1245
+ safe: (params: TParams, options?: ProcedureCallOptions) =>
1246
+ instance.safeCall<TResponse>({ ...descriptor, params }, options) as Promise<ResultNoTyped<TResponse>>,
1247
+ })
1248
+ },
1249
+
1250
+ bindCallableTyped<TParams, TResponse, ETyped>(descriptor: Omit<CallDescriptor, 'params'>) {
1251
+ const call = (params: TParams, options?: ProcedureCallOptions) =>
1252
+ instance.call<TResponse>({ ...descriptor, params }, options)
1253
+ Object.defineProperty(call, 'name', { value: descriptor.name, configurable: true })
1254
+ return Object.assign(call, {
1255
+ safe: (params: TParams, options?: ProcedureCallOptions) =>
1256
+ instance.safeCall<TResponse, ETyped>({ ...descriptor, params }, options),
1257
+ })
1258
+ },
1259
+
1260
+ stream<TYield, TReturn>(
1261
+ descriptor: StreamDescriptor,
1262
+ options?: ProcedureCallOptions,
1263
+ ): TypedStream<TYield, TReturn> {
1264
+ // executeStream is async but stream() must be synchronous.
1265
+ // Create a deferred TypedStream that wraps the async executeStream call.
1266
+
1267
+ let resolveResult: (value: TReturn) => void
1268
+ let rejectResult: (reason: unknown) => void
1269
+
1270
+ const resultPromise = new Promise<TReturn>((resolve, reject) => {
1271
+ resolveResult = resolve
1272
+ rejectResult = reject
1273
+ })
1274
+
1275
+ // Attach a no-op rejection sink so unhandled-rejection trackers (e.g.
1276
+ // node:test) don't fire when the iterator throws but the consumer never
1277
+ // awaits `.result`. Returns a new promise — consumers awaiting
1278
+ // `resultPromise` still observe the rejection.
1279
+ resultPromise.catch(() => undefined)
1280
+
1281
+ // The deferred async generator: awaits executeStream, then forwards
1282
+ async function* deferredGenerator(): AsyncGenerator<TYield> {
1283
+ let innerStream: TypedStream<TYield, TReturn>
1284
+ try {
1285
+ innerStream = await executeStream<TYield, TReturn>({
1286
+ descriptor,
1287
+ basePath,
1288
+ adapter,
1289
+ hooks: globalHooks,
1290
+ defaults: globalDefaults,
1291
+ options,
1292
+ errorRegistry,
1293
+ })
1294
+ } catch (err) {
1295
+ rejectResult(err)
1296
+ throw err
1297
+ }
1298
+
1299
+ // Wire up .result from the inner stream
1300
+ innerStream.result.then(resolveResult, rejectResult)
1301
+
1302
+ for await (const item of innerStream) {
1303
+ yield item
1304
+ }
1305
+ }
1306
+
1307
+ const iterator = deferredGenerator()
1308
+
1309
+ return {
1310
+ [Symbol.asyncIterator]() {
1311
+ return iterator
1312
+ },
1313
+ result: resultPromise,
1314
+ }
1315
+ },
1316
+ }
1317
+
1318
+ return scopes(instance)
1319
+ }