ts-procedures 7.2.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -3
- package/agent_config/claude-code/agents/ts-procedures-architect.md +6 -8
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +30 -33
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +139 -53
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +208 -231
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +80 -153
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +4 -5
- package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +4 -7
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono.md +223 -0
- package/agent_config/copilot/copilot-instructions.md +36 -48
- package/agent_config/cursor/cursorrules +36 -48
- package/build/client/call.js +4 -1
- package/build/client/call.js.map +1 -1
- package/build/client/call.test.js +23 -0
- package/build/client/call.test.js.map +1 -1
- package/build/client/fetch-adapter.js +3 -1
- package/build/client/fetch-adapter.js.map +1 -1
- package/build/client/fetch-adapter.test.js +11 -1
- package/build/client/fetch-adapter.test.js.map +1 -1
- package/build/client/index.test.js +7 -7
- package/build/client/index.test.js.map +1 -1
- package/build/client/request-builder.d.ts +1 -1
- package/build/client/request-builder.js +2 -2
- package/build/client/request-builder.js.map +1 -1
- package/build/client/stream.js +13 -2
- package/build/client/stream.js.map +1 -1
- package/build/client/stream.test.js +32 -7
- package/build/client/stream.test.js.map +1 -1
- package/build/client/typed-error-dispatch.test.js +8 -92
- package/build/client/typed-error-dispatch.test.js.map +1 -1
- package/build/client/types.d.ts +21 -3
- package/build/codegen/bin/cli.js +0 -0
- package/build/codegen/e2e.test.js +87 -23
- package/build/codegen/e2e.test.js.map +1 -1
- package/build/codegen/emit-errors.integration.test.js +1 -1
- package/build/codegen/emit-errors.integration.test.js.map +1 -1
- package/build/codegen/emit-scope.js +308 -47
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/emit-scope.test.js +363 -110
- package/build/codegen/emit-scope.test.js.map +1 -1
- package/build/codegen/pipeline.test.js +7 -7
- package/build/codegen/pipeline.test.js.map +1 -1
- package/build/codegen/resolve-envelope.js +1 -1
- package/build/codegen/resolve-envelope.js.map +1 -1
- package/build/codegen/resolve-envelope.test.js +5 -5
- package/build/codegen/resolve-envelope.test.js.map +1 -1
- package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
- package/build/codegen/targets/_shared/route-slots.js +49 -8
- package/build/codegen/targets/_shared/route-slots.js.map +1 -1
- package/build/codegen/targets/_shared/route-slots.test.js +99 -26
- package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/integration.test.js +6 -0
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
- package/build/codegen/targets/swift/access-level.test.js +8 -11
- package/build/codegen/targets/swift/access-level.test.js.map +1 -1
- package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
- package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
- package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
- package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
- package/build/codegen/targets/swift/integration.test.js +6 -0
- package/build/codegen/targets/swift/integration.test.js.map +1 -1
- package/build/create-http-stream.d.ts +58 -0
- package/build/create-http-stream.js +122 -0
- package/build/create-http-stream.js.map +1 -0
- package/build/create-http-stream.test.js +88 -0
- package/build/create-http-stream.test.js.map +1 -0
- package/build/create-http.d.ts +49 -0
- package/build/create-http.js +108 -0
- package/build/create-http.js.map +1 -0
- package/build/create-http.test.js +137 -0
- package/build/create-http.test.js.map +1 -0
- package/build/create-stream.d.ts +35 -0
- package/build/create-stream.js +123 -0
- package/build/create-stream.js.map +1 -0
- package/build/create-stream.test.js +428 -0
- package/build/create-stream.test.js.map +1 -0
- package/build/create.d.ts +28 -0
- package/build/create.js +82 -0
- package/build/create.js.map +1 -0
- package/build/create.test.js +483 -0
- package/build/create.test.js.map +1 -0
- package/build/exports.d.ts +2 -0
- package/build/implementations/http/astro/index.test.js +20 -12
- package/build/implementations/http/astro/index.test.js.map +1 -1
- package/build/implementations/http/doc-registry.js +1 -1
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +36 -5
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-dispatch.d.ts +76 -0
- package/build/implementations/http/error-dispatch.js +77 -0
- package/build/implementations/http/error-dispatch.js.map +1 -0
- package/build/implementations/http/error-dispatch.test.js +254 -0
- package/build/implementations/http/error-dispatch.test.js.map +1 -0
- package/build/implementations/http/error-taxonomy.d.ts +5 -5
- package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/http-doc.js +42 -0
- package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
- package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
- package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/stream-doc.js +42 -0
- package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
- package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
- package/build/implementations/http/hono/handlers/http-stream.js +123 -0
- package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
- package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
- package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/http.d.ts +10 -0
- package/build/implementations/http/hono/handlers/http.js +115 -0
- package/build/implementations/http/hono/handlers/http.js.map +1 -0
- package/build/implementations/http/hono/handlers/http.test.js +118 -0
- package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
- package/build/implementations/http/hono/handlers/rpc.js +32 -0
- package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
- package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
- package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
- package/build/implementations/http/hono/handlers/stream.js +147 -0
- package/build/implementations/http/hono/handlers/stream.js.map +1 -0
- package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
- package/build/implementations/http/hono/handlers/stream.test.js +177 -0
- package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
- package/build/implementations/http/hono/index.d.ts +57 -0
- package/build/implementations/http/hono/index.js +149 -0
- package/build/implementations/http/hono/index.js.map +1 -0
- package/build/implementations/http/hono/index.test.d.ts +1 -0
- package/build/implementations/http/hono/index.test.js +274 -0
- package/build/implementations/http/hono/index.test.js.map +1 -0
- package/build/implementations/http/hono/path.d.ts +17 -0
- package/build/implementations/http/hono/path.js +39 -0
- package/build/implementations/http/hono/path.js.map +1 -0
- package/build/implementations/http/hono/path.test.d.ts +1 -0
- package/build/implementations/http/hono/path.test.js +83 -0
- package/build/implementations/http/hono/path.test.js.map +1 -0
- package/build/implementations/http/hono/types.d.ts +51 -0
- package/build/implementations/http/hono/types.js.map +1 -0
- package/build/implementations/http/on-request-error.test.js +6 -96
- package/build/implementations/http/on-request-error.test.js.map +1 -1
- package/build/implementations/http/route-errors.test.js +11 -59
- package/build/implementations/http/route-errors.test.js.map +1 -1
- package/build/implementations/types.d.ts +43 -9
- package/build/index.d.ts +125 -115
- package/build/index.js +10 -222
- package/build/index.js.map +1 -1
- package/build/index.test.js +30 -822
- package/build/index.test.js.map +1 -1
- package/build/migration.test.d.ts +1 -0
- package/build/migration.test.js +34 -0
- package/build/migration.test.js.map +1 -0
- package/build/schema/compute-schema.d.ts +11 -3
- package/build/schema/compute-schema.js +13 -7
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +11 -3
- package/build/schema/parser.js +49 -9
- package/build/schema/parser.js.map +1 -1
- package/build/stack-utils.js +8 -0
- package/build/stack-utils.js.map +1 -1
- package/build/types.d.ts +142 -0
- package/build/types.js.map +1 -0
- package/docs/astro-adapter.md +5 -5
- package/docs/core.md +34 -17
- package/docs/http-integrations.md +83 -170
- package/docs/streaming.md +3 -60
- package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
- package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
- package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
- package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
- package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
- package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
- package/package.json +4 -22
- package/src/client/call.test.ts +26 -0
- package/src/client/call.ts +4 -1
- package/src/client/fetch-adapter.test.ts +14 -1
- package/src/client/fetch-adapter.ts +3 -1
- package/src/client/index.test.ts +7 -7
- package/src/client/request-builder.ts +2 -2
- package/src/client/stream.test.ts +39 -7
- package/src/client/stream.ts +16 -2
- package/src/client/typed-error-dispatch.test.ts +7 -97
- package/src/client/types.ts +21 -3
- package/src/codegen/__fixtures__/users-envelope.json +119 -38
- package/src/codegen/e2e.test.ts +98 -24
- package/src/codegen/emit-errors.integration.test.ts +1 -1
- package/src/codegen/emit-scope.test.ts +395 -110
- package/src/codegen/emit-scope.ts +350 -55
- package/src/codegen/pipeline.test.ts +7 -7
- package/src/codegen/resolve-envelope.test.ts +5 -5
- package/src/codegen/resolve-envelope.ts +1 -1
- package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
- package/src/codegen/targets/_shared/route-slots.ts +48 -11
- package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
- package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
- package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
- package/src/codegen/targets/kotlin/integration.test.ts +19 -0
- package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
- package/src/codegen/targets/swift/access-level.test.ts +8 -11
- package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
- package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
- package/src/codegen/targets/swift/integration.test.ts +17 -0
- package/src/create-http-stream.test.ts +97 -0
- package/src/create-http-stream.ts +191 -0
- package/src/create-http.test.ts +163 -0
- package/src/create-http.ts +211 -0
- package/src/create-stream.test.ts +565 -0
- package/src/create-stream.ts +228 -0
- package/src/create.test.ts +658 -0
- package/src/create.ts +172 -0
- package/src/exports.ts +2 -0
- package/src/implementations/http/README.md +135 -95
- package/src/implementations/http/astro/README.md +4 -5
- package/src/implementations/http/astro/index.test.ts +25 -18
- package/src/implementations/http/doc-registry.test.ts +42 -5
- package/src/implementations/http/doc-registry.ts +1 -1
- package/src/implementations/http/error-dispatch.test.ts +283 -0
- package/src/implementations/http/error-dispatch.ts +176 -0
- package/src/implementations/http/error-taxonomy.ts +5 -5
- package/src/implementations/http/hono/docs/http-doc.ts +43 -0
- package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
- package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
- package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
- package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
- package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
- package/src/implementations/http/hono/handlers/http.test.ts +130 -0
- package/src/implementations/http/hono/handlers/http.ts +147 -0
- package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
- package/src/implementations/http/hono/handlers/rpc.ts +54 -0
- package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
- package/src/implementations/http/hono/handlers/stream.ts +208 -0
- package/src/implementations/http/hono/index.test.ts +329 -0
- package/src/implementations/http/hono/index.ts +204 -0
- package/src/implementations/http/hono/path.test.ts +96 -0
- package/src/implementations/http/hono/path.ts +59 -0
- package/src/implementations/http/hono/types.ts +93 -0
- package/src/implementations/http/on-request-error.test.ts +10 -116
- package/src/implementations/http/route-errors.test.ts +11 -77
- package/src/implementations/types.ts +44 -9
- package/src/index.test.ts +35 -1091
- package/src/index.ts +50 -474
- package/src/migration.test.ts +48 -0
- package/src/schema/compute-schema.ts +26 -12
- package/src/schema/parser.ts +62 -12
- package/src/stack-utils.ts +8 -0
- package/src/types.ts +133 -0
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/express-rpc/index.d.ts +0 -125
- package/build/implementations/http/express-rpc/index.js +0 -216
- package/build/implementations/http/express-rpc/index.js.map +0 -1
- package/build/implementations/http/express-rpc/index.test.js +0 -684
- package/build/implementations/http/express-rpc/index.test.js.map +0 -1
- package/build/implementations/http/express-rpc/types.d.ts +0 -11
- package/build/implementations/http/express-rpc/types.js.map +0 -1
- package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-api/index.d.ts +0 -151
- package/build/implementations/http/hono-api/index.js +0 -344
- package/build/implementations/http/hono-api/index.js.map +0 -1
- package/build/implementations/http/hono-api/index.test.js +0 -992
- package/build/implementations/http/hono-api/index.test.js.map +0 -1
- package/build/implementations/http/hono-api/types.d.ts +0 -13
- package/build/implementations/http/hono-api/types.js.map +0 -1
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-rpc/index.d.ts +0 -130
- package/build/implementations/http/hono-rpc/index.js +0 -209
- package/build/implementations/http/hono-rpc/index.js.map +0 -1
- package/build/implementations/http/hono-rpc/index.test.js +0 -828
- package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
- package/build/implementations/http/hono-rpc/types.d.ts +0 -11
- package/build/implementations/http/hono-rpc/types.js +0 -2
- package/build/implementations/http/hono-rpc/types.js.map +0 -1
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-stream/index.d.ts +0 -171
- package/build/implementations/http/hono-stream/index.js +0 -415
- package/build/implementations/http/hono-stream/index.js.map +0 -1
- package/build/implementations/http/hono-stream/index.test.js +0 -1383
- package/build/implementations/http/hono-stream/index.test.js.map +0 -1
- package/build/implementations/http/hono-stream/types.d.ts +0 -15
- package/build/implementations/http/hono-stream/types.js +0 -2
- package/build/implementations/http/hono-stream/types.js.map +0 -1
- package/src/implementations/http/express-rpc/README.md +0 -280
- package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
- package/src/implementations/http/express-rpc/index.test.ts +0 -957
- package/src/implementations/http/express-rpc/index.ts +0 -327
- package/src/implementations/http/express-rpc/types.ts +0 -16
- package/src/implementations/http/hono-api/README.md +0 -284
- package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
- package/src/implementations/http/hono-api/index.test.ts +0 -1341
- package/src/implementations/http/hono-api/index.ts +0 -519
- package/src/implementations/http/hono-api/types.ts +0 -16
- package/src/implementations/http/hono-rpc/README.md +0 -357
- package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
- package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
- package/src/implementations/http/hono-rpc/index.ts +0 -320
- package/src/implementations/http/hono-rpc/types.ts +0 -16
- package/src/implementations/http/hono-stream/README.md +0 -559
- package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
- package/src/implementations/http/hono-stream/index.test.ts +0 -1804
- package/src/implementations/http/hono-stream/index.ts +0 -622
- package/src/implementations/http/hono-stream/types.ts +0 -20
- /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
- /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
- /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
- /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
- /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
- /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
|
@@ -0,0 +1,3355 @@
|
|
|
1
|
+
# CreateHttp & CreateHttpStream (v8) — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Split the core procedure surface into four purpose-shaped creators (`Create` / `CreateStream` / `CreateHttp` / `CreateHttpStream`), eliminating the long-standing `schema.params` vs `schema.input` ambiguity and adding first-class typed response (body + headers) to HTTP routes.
|
|
6
|
+
|
|
7
|
+
**Architecture:** `Create` and `CreateStream` lose `schema.input` entirely (RPC primitives only). New `CreateHttp` / `CreateHttpStream` carry HTTP fields (`path`/`method`/`successStatus`/`scope`/`errors`) and a structured `req` / `res` schema. A `kind` discriminant on every registration drives builder routing. `HonoAPIAppBuilder` becomes the unified HTTP builder serving both `'http'` and `'http-stream'` registrations. Doc envelope regroups `jsonSchema` under `req`/`res`. Codegen emits `Req.*` / `Response.*` namespaces and conditionally typed return shapes.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Vitest, AJV, TypeBox, Hono. Existing `ts-procedures` codegen pipeline. No new runtime dependencies.
|
|
10
|
+
|
|
11
|
+
**Spec:** [docs/superpowers/specs/2026-05-08-create-http-design.md](../specs/2026-05-08-create-http-design.md)
|
|
12
|
+
|
|
13
|
+
**Branch:** Already on `refactor-http-api-v8` (worktree off master). This is a major version (8.0.0) so changes ship as a clean break — no v7 compat shim.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File map
|
|
18
|
+
|
|
19
|
+
**Create:**
|
|
20
|
+
- `src/types.ts` — shared `TLocalContext`, `TStreamContext`, `TProcedureRegistration`, `TStreamProcedureRegistration`, `ProcedureKind`, `TBuilderConfig`
|
|
21
|
+
- `src/create.ts` — `Create` factory (RPC unary)
|
|
22
|
+
- `src/create-stream.ts` — `CreateStream` factory (RPC streaming)
|
|
23
|
+
- `src/create-http.ts` — `CreateHttp` factory + path-param consistency check (HTTP unary)
|
|
24
|
+
- `src/create-http-stream.ts` — `CreateHttpStream` factory (HTTP streaming)
|
|
25
|
+
- `src/create.test.ts` — extracted from `src/index.test.ts`
|
|
26
|
+
- `src/create-stream.test.ts` — extracted from `src/index.test.ts`
|
|
27
|
+
- `src/create-http.test.ts` — new
|
|
28
|
+
- `src/create-http-stream.test.ts` — new
|
|
29
|
+
- `src/migration.test.ts` — registration errors on v7 shapes (schema.input on Create, etc.)
|
|
30
|
+
|
|
31
|
+
**Modify:**
|
|
32
|
+
- `src/index.ts` — slimmed to `Procedures` factory glue (~80 lines, was 537)
|
|
33
|
+
- `src/index.test.ts` — keep only Procedures factory tests; per-creator tests moved out
|
|
34
|
+
- `src/schema/compute-schema.ts` — drop `input` shape, add `req`/`res` shape, update mutual-exclusivity guard
|
|
35
|
+
- `src/schema/parser.ts` — wire `req` channels through schemaParser (mirror today's `input` path)
|
|
36
|
+
- `src/implementations/types.ts` — restructure `APIHttpRouteDoc.jsonSchema` to `req`/`res`; add `HttpStreamRouteDoc`; remove `APIInput` / `APIConfig` (or simplify); extend `AnyHttpRouteDoc` union
|
|
37
|
+
- `src/implementations/http/hono-api/index.ts` — read `CreateHttp` registrations directly; add `'http-stream'` branch; apply response headers; drop `APIConfig` extension generic
|
|
38
|
+
- `src/implementations/http/hono-api/types.ts` — simplify `HonoAPIFactoryItem` (no APIConfig generic)
|
|
39
|
+
- `src/implementations/http/hono-api/index.test.ts` — both kinds, response headers
|
|
40
|
+
- `src/implementations/http/hono-rpc/index.ts` — filter by `kind === 'rpc'`
|
|
41
|
+
- `src/implementations/http/hono-stream/index.ts` — filter by `kind === 'rpc-stream'`
|
|
42
|
+
- `src/implementations/http/{hono-rpc,hono-stream}/index.test.ts` — kind-mismatch coverage via `skippedProcedures`
|
|
43
|
+
- `src/implementations/http/astro/*` — kind-aware updates mirroring hono-api
|
|
44
|
+
- `src/implementations/http/doc-registry.ts` — handle new `'http-stream'` kind, regrouped `req`/`res`
|
|
45
|
+
- `src/implementations/http/doc-registry.test.ts` — coverage updates
|
|
46
|
+
- `src/codegen/group-routes.ts` — recognize `'http-stream'` kind
|
|
47
|
+
- `src/codegen/targets/_shared/route-slots.ts` — add response headers slot
|
|
48
|
+
- `src/codegen/targets/ts/run.ts` + supporting `emit-scope.ts` — `Req` / `Response` namespacing, conditional return shape
|
|
49
|
+
- `src/codegen/targets/kotlin/run.ts` — `Response.Headers` data class when declared
|
|
50
|
+
- `src/codegen/targets/swift/run.ts` — `Response.Headers` struct when declared
|
|
51
|
+
- `src/codegen/__fixtures__/users-envelope.json` — regenerate to v8 envelope shape
|
|
52
|
+
- `src/codegen/emit-scope.test.ts` — snapshot updates
|
|
53
|
+
- `src/codegen/targets/{ts,kotlin,swift}/run.test.ts` — snapshot updates
|
|
54
|
+
- `src/client/types.ts` — `AdapterStreamResponse.headers`, `TypedStream.headers` (optional)
|
|
55
|
+
- `src/client/call.ts` — surface response headers when route declares `res.headers`
|
|
56
|
+
- `src/client/stream.ts` — wire initial response headers into `TypedStream`
|
|
57
|
+
- `src/client/fetch-adapter.ts` — populate `AdapterStreamResponse.headers`
|
|
58
|
+
- `src/client/{call,stream,fetch-adapter,index}.test.ts` — coverage updates
|
|
59
|
+
- `package.json` — bump to `8.0.0`
|
|
60
|
+
- `CHANGELOG.md` — major bump entry with migration table
|
|
61
|
+
- `README.md` — update headline examples
|
|
62
|
+
- `CLAUDE.md` — update architecture section
|
|
63
|
+
- `agent_config/claude-code/skills/ts-procedures/{patterns.md,anti-patterns.md,api-reference.md}` — CreateHttp examples
|
|
64
|
+
- `agent_config/claude-code/skills/ts-procedures-scaffold/templates/*` — new templates
|
|
65
|
+
- `agent_config/copilot/copilot-instructions.md`, `agent_config/cursor/cursorrules` — condensed mirror updates
|
|
66
|
+
|
|
67
|
+
**Delete:**
|
|
68
|
+
- (Nothing deleted outright — `APIInput` and the `schema.input` field are removed from their containing files but no whole-file deletions.)
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Phase 1 — Core file split (pure refactor, zero behavior change)
|
|
73
|
+
|
|
74
|
+
This phase moves code into new files without changing semantics. Run the existing test suite after each task to confirm green. Commit between every task.
|
|
75
|
+
|
|
76
|
+
### Task 1: Extract shared types into `src/types.ts`
|
|
77
|
+
|
|
78
|
+
**Files:**
|
|
79
|
+
- Create: `src/types.ts`
|
|
80
|
+
- Modify: `src/index.ts`
|
|
81
|
+
|
|
82
|
+
- [ ] **Step 1: Create `src/types.ts` with the shared exports**
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { ProcedureError } from './errors.js'
|
|
86
|
+
import { TJSONSchema } from './schema/types.js'
|
|
87
|
+
|
|
88
|
+
export type TNoContextProvided = unknown
|
|
89
|
+
|
|
90
|
+
export type TLocalContext = {
|
|
91
|
+
error: (message: string, meta?: object) => ProcedureError
|
|
92
|
+
signal?: AbortSignal
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type TStreamContext = TLocalContext & {
|
|
96
|
+
signal: AbortSignal
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type ProcedureKind = 'rpc' | 'rpc-stream' | 'http' | 'http-stream'
|
|
100
|
+
|
|
101
|
+
export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
|
|
102
|
+
name: string
|
|
103
|
+
kind: 'rpc'
|
|
104
|
+
isStream?: false
|
|
105
|
+
config: {
|
|
106
|
+
description?: string
|
|
107
|
+
schema?: {
|
|
108
|
+
params?: TJSONSchema
|
|
109
|
+
returnType?: TJSONSchema
|
|
110
|
+
}
|
|
111
|
+
validation?: {
|
|
112
|
+
params?: (params: any) => { errors?: any[] }
|
|
113
|
+
}
|
|
114
|
+
} & TExtendedConfig
|
|
115
|
+
handler: (ctx: TContext, params?: any) => Promise<any>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type TStreamProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
|
|
119
|
+
name: string
|
|
120
|
+
kind: 'rpc-stream'
|
|
121
|
+
isStream: true
|
|
122
|
+
config: {
|
|
123
|
+
description?: string
|
|
124
|
+
schema?: {
|
|
125
|
+
params?: TJSONSchema
|
|
126
|
+
yieldType?: TJSONSchema
|
|
127
|
+
returnType?: TJSONSchema
|
|
128
|
+
}
|
|
129
|
+
validation?: {
|
|
130
|
+
params?: (params: any) => { errors?: any[] }
|
|
131
|
+
yield?: (value: any) => { errors?: any[] }
|
|
132
|
+
}
|
|
133
|
+
validateYields?: boolean
|
|
134
|
+
} & TExtendedConfig
|
|
135
|
+
handler: (ctx: TContext, params?: any) => AsyncGenerator<any, any, unknown>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type TBuilderConfig = {
|
|
139
|
+
/**
|
|
140
|
+
* Skip per-call AJV runtime validation. JSON Schema is still computed at
|
|
141
|
+
* registration time. Intended for trusted internal factories whose callers
|
|
142
|
+
* are already type-checked at build time.
|
|
143
|
+
*/
|
|
144
|
+
noRuntimeValidation?: true
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
(`'http'` and `'http-stream'` registration types are added in later phases when those creators land.)
|
|
149
|
+
|
|
150
|
+
- [ ] **Step 2: Update `src/index.ts` to re-export from types.ts**
|
|
151
|
+
|
|
152
|
+
Replace the type declarations at the top of `src/index.ts` with a re-export. The factory function and creators stay in place for now:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// At the very top of src/index.ts, replace the local TLocalContext/TStreamContext/etc. with:
|
|
156
|
+
export {
|
|
157
|
+
type TNoContextProvided,
|
|
158
|
+
type TLocalContext,
|
|
159
|
+
type TStreamContext,
|
|
160
|
+
type ProcedureKind,
|
|
161
|
+
type TProcedureRegistration,
|
|
162
|
+
type TStreamProcedureRegistration,
|
|
163
|
+
type TBuilderConfig,
|
|
164
|
+
} from './types.js'
|
|
165
|
+
|
|
166
|
+
import { TLocalContext, TStreamContext, TBuilderConfig } from './types.js'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Delete the original `export type T...` declarations from `src/index.ts` (lines around 10-69 in the current file). Keep the `Procedures` function and `Create` / `CreateStream` definitions intact.
|
|
170
|
+
|
|
171
|
+
- [ ] **Step 3: Run the full test suite**
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npm run test
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Expected: all existing tests pass. Type re-exports are transparent.
|
|
178
|
+
|
|
179
|
+
- [ ] **Step 4: Commit**
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
git add src/types.ts src/index.ts
|
|
183
|
+
git commit -m "refactor(core): extract shared types to src/types.ts"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### Task 2: Extract `Create` into `src/create.ts`
|
|
189
|
+
|
|
190
|
+
**Files:**
|
|
191
|
+
- Create: `src/create.ts`
|
|
192
|
+
- Modify: `src/index.ts`
|
|
193
|
+
|
|
194
|
+
- [ ] **Step 1: Create `src/create.ts`**
|
|
195
|
+
|
|
196
|
+
Move the `Create` function out of `src/index.ts` into a standalone factory. The function takes the `procedures` Map and `builder` config (so it can register and check `noRuntimeValidation`):
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { ProcedureError, ProcedureValidationError } from './errors.js'
|
|
200
|
+
import { computeSchema } from './schema/compute-schema.js'
|
|
201
|
+
import { Prettify, TSchemaLib } from './schema/types.js'
|
|
202
|
+
import { captureDefinitionInfo } from './stack-utils.js'
|
|
203
|
+
import {
|
|
204
|
+
TBuilderConfig,
|
|
205
|
+
TLocalContext,
|
|
206
|
+
TProcedureRegistration,
|
|
207
|
+
TStreamProcedureRegistration,
|
|
208
|
+
} from './types.js'
|
|
209
|
+
|
|
210
|
+
export type CreateBuilderArg<TContext, TExtendedConfig> = {
|
|
211
|
+
config?: TBuilderConfig
|
|
212
|
+
onCreate?: (procedure: TProcedureRegistration<TContext, TExtendedConfig> | TStreamProcedureRegistration<TContext, TExtendedConfig>) => void
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function makeCreate<TContext, TExtendedConfig>(
|
|
216
|
+
procedures: Map<string, TProcedureRegistration<TContext, TExtendedConfig> | TStreamProcedureRegistration<TContext, TExtendedConfig>>,
|
|
217
|
+
builder?: CreateBuilderArg<TContext, TExtendedConfig>,
|
|
218
|
+
) {
|
|
219
|
+
return function Create<TName extends string, TParams, TReturnType>(
|
|
220
|
+
name: TName,
|
|
221
|
+
config: {
|
|
222
|
+
description?: string
|
|
223
|
+
schema?: {
|
|
224
|
+
params?: TParams
|
|
225
|
+
returnType?: TReturnType
|
|
226
|
+
}
|
|
227
|
+
} & TExtendedConfig,
|
|
228
|
+
handler: (ctx: Prettify<TContext & TLocalContext>, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>,
|
|
229
|
+
) {
|
|
230
|
+
const definitionInfo = captureDefinitionInfo()
|
|
231
|
+
|
|
232
|
+
if (procedures.has(name)) {
|
|
233
|
+
throw new Error(`Procedure with name ${name} is already registered`)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { jsonSchema, validations } = computeSchema(name, config.schema, definitionInfo)
|
|
237
|
+
|
|
238
|
+
const errorFactory = (message: string, meta?: object) =>
|
|
239
|
+
new ProcedureError(name, message, meta, definitionInfo)
|
|
240
|
+
|
|
241
|
+
const registeredProcedure: TProcedureRegistration<TContext, TExtendedConfig> = {
|
|
242
|
+
name,
|
|
243
|
+
kind: 'rpc',
|
|
244
|
+
config: {
|
|
245
|
+
...config,
|
|
246
|
+
description: config.description,
|
|
247
|
+
schema: jsonSchema,
|
|
248
|
+
validation: {
|
|
249
|
+
params: validations.params,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
handler: async (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => {
|
|
253
|
+
try {
|
|
254
|
+
const skipValidation =
|
|
255
|
+
(ctx as { isPrevalidated?: boolean }).isPrevalidated ||
|
|
256
|
+
builder?.config?.noRuntimeValidation
|
|
257
|
+
|
|
258
|
+
if (validations?.params && !skipValidation) {
|
|
259
|
+
const { errors } = validations.params(params)
|
|
260
|
+
if (errors) {
|
|
261
|
+
throw new ProcedureValidationError(
|
|
262
|
+
name,
|
|
263
|
+
`Validation error for ${name}`,
|
|
264
|
+
errors,
|
|
265
|
+
definitionInfo,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const localCtx: TLocalContext = { error: errorFactory }
|
|
271
|
+
|
|
272
|
+
return await handler({ ...ctx, ...localCtx } as Prettify<TContext & TLocalContext>, params as any)
|
|
273
|
+
} catch (error: any) {
|
|
274
|
+
if (error instanceof ProcedureError) throw error
|
|
275
|
+
const err = new ProcedureError(name, `Error in handler for ${name} - ${error?.message}`, undefined, definitionInfo)
|
|
276
|
+
err.cause = error
|
|
277
|
+
if (error.stack && definitionInfo.definedAt) {
|
|
278
|
+
const { file, line, column } = definitionInfo.definedAt
|
|
279
|
+
err.stack = error.stack + `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
|
|
280
|
+
} else if (error.stack) {
|
|
281
|
+
err.stack = error.stack
|
|
282
|
+
}
|
|
283
|
+
throw err
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
procedures.set(name, registeredProcedure)
|
|
289
|
+
builder?.onCreate?.(registeredProcedure)
|
|
290
|
+
|
|
291
|
+
const info = { name, ...registeredProcedure.config }
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
[name]: registeredProcedure.handler,
|
|
295
|
+
procedure: registeredProcedure.handler,
|
|
296
|
+
info,
|
|
297
|
+
} as {
|
|
298
|
+
[K in TName]: (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>
|
|
299
|
+
} & {
|
|
300
|
+
procedure: (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>
|
|
301
|
+
info: {
|
|
302
|
+
name: TName
|
|
303
|
+
description?: string
|
|
304
|
+
schema: { params?: TParams; returnType?: TReturnType }
|
|
305
|
+
validation?: { params?: (params: any) => { errors?: any[] } }
|
|
306
|
+
} & TExtendedConfig
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Note:** This already drops `input` from the `Create` signature (Phase 2 will enforce the migration error). The conditional `TInput extends Record<string, unknown> ? ... : TSchemaLib<TParams>` is gone — `params` is always `TSchemaLib<TParams>`.
|
|
313
|
+
|
|
314
|
+
- [ ] **Step 2: Update `src/index.ts` to use `makeCreate`**
|
|
315
|
+
|
|
316
|
+
In the `Procedures` function body, replace the inline `Create` definition with:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { makeCreate } from './create.js'
|
|
320
|
+
|
|
321
|
+
// inside Procedures():
|
|
322
|
+
const Create = makeCreate<TContext, TExtendedConfig>(procedures, builder)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Remove the original `function Create<...>` body.
|
|
326
|
+
|
|
327
|
+
- [ ] **Step 3: Run the test suite**
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
npm run test
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Expected: all existing tests that don't use `schema.input` on `Create` pass. Tests using `schema.input` will fail with a TS error or runtime error — that's expected; Phase 2 handles the migration error formally. Note which tests fail.
|
|
334
|
+
|
|
335
|
+
- [ ] **Step 4: Mark failing tests with `it.todo` or temporarily skip**
|
|
336
|
+
|
|
337
|
+
For any tests in `src/index.test.ts` that exercise `schema.input` on `Create`, change `it(...)` to `it.todo(...)` with a comment `// migrated to schema.req in Phase 3+`. We'll restore them in Phase 3 against `CreateHttp`.
|
|
338
|
+
|
|
339
|
+
- [ ] **Step 5: Run tests again, expect green**
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
npm run test
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
- [ ] **Step 6: Commit**
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
git add src/create.ts src/index.ts src/index.test.ts
|
|
349
|
+
git commit -m "refactor(core): extract Create into src/create.ts; drop schema.input from Create"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### Task 3: Extract `CreateStream` into `src/create-stream.ts`
|
|
355
|
+
|
|
356
|
+
**Files:**
|
|
357
|
+
- Create: `src/create-stream.ts`
|
|
358
|
+
- Modify: `src/index.ts`
|
|
359
|
+
|
|
360
|
+
- [ ] **Step 1: Create `src/create-stream.ts`**
|
|
361
|
+
|
|
362
|
+
Mirror Task 2's pattern. Move the `CreateStream` function into its own file as a `makeCreateStream` factory. Drop the `TInput` generic and `input` from the schema shape — `CreateStream` now only accepts `params`/`yieldType`/`returnType`.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError } from './errors.js'
|
|
366
|
+
import { computeSchema } from './schema/compute-schema.js'
|
|
367
|
+
import { Prettify, TSchemaLib } from './schema/types.js'
|
|
368
|
+
import { captureDefinitionInfo } from './stack-utils.js'
|
|
369
|
+
import {
|
|
370
|
+
TBuilderConfig,
|
|
371
|
+
TStreamContext,
|
|
372
|
+
TProcedureRegistration,
|
|
373
|
+
TStreamProcedureRegistration,
|
|
374
|
+
} from './types.js'
|
|
375
|
+
|
|
376
|
+
export function makeCreateStream<TContext, TExtendedConfig>(
|
|
377
|
+
procedures: Map<string, TProcedureRegistration<TContext, TExtendedConfig> | TStreamProcedureRegistration<TContext, TExtendedConfig>>,
|
|
378
|
+
builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
|
|
379
|
+
) {
|
|
380
|
+
return function CreateStream<TName extends string, TParams, TYieldType, TReturnType = void>(
|
|
381
|
+
name: TName,
|
|
382
|
+
config: {
|
|
383
|
+
description?: string
|
|
384
|
+
schema?: {
|
|
385
|
+
params?: TParams
|
|
386
|
+
yieldType?: TYieldType
|
|
387
|
+
returnType?: TReturnType
|
|
388
|
+
}
|
|
389
|
+
validateYields?: boolean
|
|
390
|
+
} & TExtendedConfig,
|
|
391
|
+
handler: (
|
|
392
|
+
ctx: Prettify<TContext & TStreamContext>,
|
|
393
|
+
params: TSchemaLib<TParams>,
|
|
394
|
+
) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>,
|
|
395
|
+
) {
|
|
396
|
+
// [body identical to today's CreateStream in src/index.ts:284-503,
|
|
397
|
+
// but with schema.input removed and `kind: 'rpc-stream'` set on the
|
|
398
|
+
// registration. Copy from existing code, delete the input handling
|
|
399
|
+
// blocks at lines ~366-380 and the TInput conditional return shapes.]
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Reference:** Copy the existing `CreateStream` body from `src/index.ts` lines 284-503. Required edits:
|
|
405
|
+
1. Set `kind: 'rpc-stream'` on the registration (alongside `isStream: true`).
|
|
406
|
+
2. Remove the `TInput` generic and conditional return types.
|
|
407
|
+
3. Remove the `validations.input` loop (around line 366-380 in current).
|
|
408
|
+
4. Remove `input` from the `validation` object spread.
|
|
409
|
+
|
|
410
|
+
- [ ] **Step 2: Wire `makeCreateStream` into `Procedures`**
|
|
411
|
+
|
|
412
|
+
In `src/index.ts`:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { makeCreateStream } from './create-stream.js'
|
|
416
|
+
|
|
417
|
+
// inside Procedures():
|
|
418
|
+
const CreateStream = makeCreateStream<TContext, TExtendedConfig>(procedures, builder)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Remove the inline `function CreateStream<...>` body from `src/index.ts`.
|
|
422
|
+
|
|
423
|
+
- [ ] **Step 3: Mark schema.input stream tests as `it.todo`**
|
|
424
|
+
|
|
425
|
+
Same treatment as Task 2 step 4.
|
|
426
|
+
|
|
427
|
+
- [ ] **Step 4: Run tests**
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
npm run test
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Expected: green except for `it.todo` cases.
|
|
434
|
+
|
|
435
|
+
- [ ] **Step 5: Commit**
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
git add src/create-stream.ts src/index.ts src/index.test.ts
|
|
439
|
+
git commit -m "refactor(core): extract CreateStream into src/create-stream.ts; drop schema.input"
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
### Task 4: Slim `src/index.ts` to glue only
|
|
445
|
+
|
|
446
|
+
**Files:**
|
|
447
|
+
- Modify: `src/index.ts`
|
|
448
|
+
|
|
449
|
+
- [ ] **Step 1: Verify `src/index.ts` is now a thin glue file**
|
|
450
|
+
|
|
451
|
+
The remaining content should be: type re-exports, `Procedures` factory, factory's internal `procedures` Map, `getProcedures` / `getProcedure` / `removeProcedure` / `clear` helpers, and the import wiring for `makeCreate` / `makeCreateStream`. Should be ~100 lines.
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
wc -l src/index.ts
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Expected: under 130 lines.
|
|
458
|
+
|
|
459
|
+
- [ ] **Step 2: Run lint and typecheck**
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
npm run lint
|
|
463
|
+
npm run build
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Expected: green.
|
|
467
|
+
|
|
468
|
+
- [ ] **Step 3: Run tests**
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
npm run test
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Expected: green (excluding `it.todo` migrations).
|
|
475
|
+
|
|
476
|
+
- [ ] **Step 4: Commit (if anything changed)**
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
git add src/index.ts
|
|
480
|
+
git commit -m "refactor(core): finalize index.ts as Procedures factory glue" --allow-empty
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
### Task 5: Add `kind` field to existing registrations and update `getProcedures` consumers
|
|
486
|
+
|
|
487
|
+
**Files:**
|
|
488
|
+
- Modify: `src/types.ts` (already done in Task 1 — verify)
|
|
489
|
+
- Modify: `src/index.test.ts` (test that `kind` is present on returned registrations)
|
|
490
|
+
|
|
491
|
+
- [ ] **Step 1: Add a test asserting `kind` appears on Create/CreateStream registrations**
|
|
492
|
+
|
|
493
|
+
In `src/index.test.ts`:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
import { Procedures } from './index.js'
|
|
497
|
+
import { Type } from 'typebox'
|
|
498
|
+
|
|
499
|
+
describe('Procedure registration kind discriminant', () => {
|
|
500
|
+
it('Create produces kind: rpc', () => {
|
|
501
|
+
const procs = Procedures()
|
|
502
|
+
procs.Create('Foo', { schema: { params: Type.Object({}) } }, async () => undefined)
|
|
503
|
+
const reg = procs.getProcedure('Foo')
|
|
504
|
+
expect(reg?.kind).toBe('rpc')
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('CreateStream produces kind: rpc-stream', () => {
|
|
508
|
+
const procs = Procedures()
|
|
509
|
+
procs.CreateStream('Bar', {
|
|
510
|
+
schema: { params: Type.Object({}), yieldType: Type.Number() },
|
|
511
|
+
}, async function* () {})
|
|
512
|
+
const reg = procs.getProcedure('Bar')
|
|
513
|
+
expect(reg?.kind).toBe('rpc-stream')
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
- [ ] **Step 2: Run test, verify it passes**
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
npx vitest run src/index.test.ts -t "kind discriminant"
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Expected: PASS (the `kind` was set in Tasks 2 and 3).
|
|
525
|
+
|
|
526
|
+
- [ ] **Step 3: Commit**
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
git add src/index.test.ts
|
|
530
|
+
git commit -m "test(core): assert kind discriminant on registrations"
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Phase 2 — Schema cleanup & migration errors
|
|
536
|
+
|
|
537
|
+
Lock down the v8 shape: `compute-schema.ts` accepts `req`/`res` instead of `input`, and `Create` / `CreateStream` reject HTTP fields with informative errors.
|
|
538
|
+
|
|
539
|
+
### Task 6: Update `computeSchema` signature for `req`/`res`
|
|
540
|
+
|
|
541
|
+
**Files:**
|
|
542
|
+
- Modify: `src/schema/compute-schema.ts`
|
|
543
|
+
- Test: `src/schema/compute-schema.test.ts` (create if missing)
|
|
544
|
+
|
|
545
|
+
- [ ] **Step 1: Read the current `compute-schema.ts`**
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
cat src/schema/compute-schema.ts
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Note the current `input` handling (lines 17-84).
|
|
552
|
+
|
|
553
|
+
- [ ] **Step 2: Update the schema input/output type signatures**
|
|
554
|
+
|
|
555
|
+
Replace the `input` field with `req` and add `res`:
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
export function computeSchema<
|
|
559
|
+
TParamsSchemaType,
|
|
560
|
+
TReturnTypeSchemaType,
|
|
561
|
+
TYieldTypeSchemaType = unknown,
|
|
562
|
+
>(
|
|
563
|
+
name: string,
|
|
564
|
+
schema?: {
|
|
565
|
+
params?: TParamsSchemaType
|
|
566
|
+
returnType?: TReturnTypeSchemaType
|
|
567
|
+
yieldType?: TYieldTypeSchemaType
|
|
568
|
+
req?: Record<string, unknown> // was: input
|
|
569
|
+
res?: { body?: unknown; headers?: unknown } // new
|
|
570
|
+
},
|
|
571
|
+
definitionInfo?: DefinitionInfo,
|
|
572
|
+
): {
|
|
573
|
+
jsonSchema: {
|
|
574
|
+
params?: TJSONSchema
|
|
575
|
+
returnType?: TJSONSchema
|
|
576
|
+
yieldType?: TJSONSchema
|
|
577
|
+
req?: Record<string, TJSONSchema>
|
|
578
|
+
res?: { body?: TJSONSchema; headers?: TJSONSchema }
|
|
579
|
+
}
|
|
580
|
+
validations: {
|
|
581
|
+
params?: (params?: any) => { errors?: TSchemaValidationError[] }
|
|
582
|
+
yield?: (value?: any) => { errors?: TSchemaValidationError[] }
|
|
583
|
+
req?: Record<string, (value?: any) => { errors?: TSchemaValidationError[] }>
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Update the mutual-exclusivity guard:
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
if (schema?.params && schema?.req) {
|
|
592
|
+
throw new ProcedureRegistrationError(
|
|
593
|
+
name,
|
|
594
|
+
`schema.params and schema.req are mutually exclusive for procedure "${name}". Use schema.params for RPC procedures or schema.req for HTTP procedures.`,
|
|
595
|
+
definitionInfo,
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Adapt the `schemaParser` call to pass `req` where it previously passed `input`. The internal mechanics are identical — `req` is just a renamed `input`. (`res` validation is documentation-only; not parsed.)
|
|
601
|
+
|
|
602
|
+
- [ ] **Step 3: Update `src/schema/parser.ts` to accept `req` channels**
|
|
603
|
+
|
|
604
|
+
In `parser.ts`, find the block that processes `input` and rename to `req`. The validator factory pattern is unchanged — each channel gets its own AJV-compiled validator.
|
|
605
|
+
|
|
606
|
+
- [ ] **Step 4: Run schema tests**
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
npx vitest run src/schema/
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
Expected: tests pass (no callers yet pass `req`/`res` outside of internal renames).
|
|
613
|
+
|
|
614
|
+
- [ ] **Step 5: Commit**
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
git add src/schema/compute-schema.ts src/schema/parser.ts
|
|
618
|
+
git commit -m "refactor(schema): rename input→req in compute-schema; add res slot"
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
### Task 7: Add migration error when legacy `schema.input` is passed
|
|
624
|
+
|
|
625
|
+
**Files:**
|
|
626
|
+
- Modify: `src/schema/compute-schema.ts`
|
|
627
|
+
- Test: `src/migration.test.ts` (create)
|
|
628
|
+
|
|
629
|
+
- [ ] **Step 1: Write failing migration tests**
|
|
630
|
+
|
|
631
|
+
Create `src/migration.test.ts`:
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
import { describe, it, expect } from 'vitest'
|
|
635
|
+
import { Procedures } from './index.js'
|
|
636
|
+
import { ProcedureRegistrationError } from './errors.js'
|
|
637
|
+
import { Type } from 'typebox'
|
|
638
|
+
|
|
639
|
+
describe('v7 → v8 migration errors', () => {
|
|
640
|
+
it('rejects schema.input on Create with v8 message', () => {
|
|
641
|
+
const procs = Procedures()
|
|
642
|
+
expect(() =>
|
|
643
|
+
procs.Create('Legacy', {
|
|
644
|
+
schema: {
|
|
645
|
+
// @ts-expect-error v8 removed schema.input
|
|
646
|
+
input: { body: Type.Object({}) },
|
|
647
|
+
},
|
|
648
|
+
}, async () => undefined)
|
|
649
|
+
).toThrow(ProcedureRegistrationError)
|
|
650
|
+
expect(() =>
|
|
651
|
+
procs.Create('Legacy2', { schema: { input: { body: Type.Object({}) } } } as any, async () => undefined)
|
|
652
|
+
).toThrow(/schema.input was removed in v8/)
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it('rejects schema.input on CreateStream with v8 message', () => {
|
|
656
|
+
const procs = Procedures()
|
|
657
|
+
expect(() =>
|
|
658
|
+
procs.CreateStream('Legacy', {
|
|
659
|
+
schema: { input: { body: Type.Object({}) }, yieldType: Type.Number() } as any,
|
|
660
|
+
}, async function* () {})
|
|
661
|
+
).toThrow(/schema.input was removed in v8/)
|
|
662
|
+
})
|
|
663
|
+
})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
- [ ] **Step 2: Run, verify they fail**
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
npx vitest run src/migration.test.ts
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Expected: FAIL (no migration error implemented yet).
|
|
673
|
+
|
|
674
|
+
- [ ] **Step 3: Add migration check in `compute-schema.ts`**
|
|
675
|
+
|
|
676
|
+
At the top of `computeSchema`, before the existing mutual-exclusivity guard:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
if (schema && 'input' in schema && (schema as any).input !== undefined) {
|
|
680
|
+
throw new ProcedureRegistrationError(
|
|
681
|
+
name,
|
|
682
|
+
`schema.input was removed in v8. Use CreateHttp / CreateHttpStream for per-channel HTTP validation. Procedure: "${name}".`,
|
|
683
|
+
definitionInfo,
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
- [ ] **Step 4: Run tests, verify they pass**
|
|
689
|
+
|
|
690
|
+
```bash
|
|
691
|
+
npx vitest run src/migration.test.ts
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Expected: PASS.
|
|
695
|
+
|
|
696
|
+
- [ ] **Step 5: Commit**
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
git add src/schema/compute-schema.ts src/migration.test.ts
|
|
700
|
+
git commit -m "feat(migration): reject schema.input with v8 migration error"
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
### Task 8: Reject HTTP fields on `Create` / `CreateStream`
|
|
706
|
+
|
|
707
|
+
**Files:**
|
|
708
|
+
- Modify: `src/create.ts`, `src/create-stream.ts`
|
|
709
|
+
- Modify: `src/migration.test.ts`
|
|
710
|
+
|
|
711
|
+
- [ ] **Step 1: Add failing migration tests**
|
|
712
|
+
|
|
713
|
+
Append to `src/migration.test.ts`:
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
describe('HTTP fields rejected on RPC creators', () => {
|
|
717
|
+
const HTTP_FIELDS = ['path', 'method', 'req', 'res', 'successStatus'] as const
|
|
718
|
+
|
|
719
|
+
for (const field of HTTP_FIELDS) {
|
|
720
|
+
it(`Create rejects ${field}`, () => {
|
|
721
|
+
const procs = Procedures()
|
|
722
|
+
expect(() =>
|
|
723
|
+
procs.Create('Foo', { [field]: 'x', schema: { params: Type.Object({}) } } as any, async () => undefined)
|
|
724
|
+
).toThrow(/HTTP fields require CreateHttp/)
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
it(`CreateStream rejects ${field}`, () => {
|
|
728
|
+
const procs = Procedures()
|
|
729
|
+
expect(() =>
|
|
730
|
+
procs.CreateStream('Bar', {
|
|
731
|
+
[field]: 'x',
|
|
732
|
+
schema: { params: Type.Object({}), yieldType: Type.Number() },
|
|
733
|
+
} as any, async function* () {})
|
|
734
|
+
).toThrow(/HTTP fields require CreateHttp/)
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
})
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
- [ ] **Step 2: Run, verify they fail**
|
|
741
|
+
|
|
742
|
+
```bash
|
|
743
|
+
npx vitest run src/migration.test.ts -t "HTTP fields"
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Expected: FAIL.
|
|
747
|
+
|
|
748
|
+
- [ ] **Step 3: Add HTTP-field guard helper to `src/create.ts`**
|
|
749
|
+
|
|
750
|
+
At the top of `makeCreate`'s returned `Create` function (after `definitionInfo = captureDefinitionInfo()`):
|
|
751
|
+
|
|
752
|
+
```ts
|
|
753
|
+
import { ProcedureRegistrationError } from './errors.js'
|
|
754
|
+
|
|
755
|
+
const HTTP_FIELDS = ['path', 'method', 'req', 'res', 'successStatus'] as const
|
|
756
|
+
const presentHttpFields = HTTP_FIELDS.filter((f) => (config as any)[f] !== undefined)
|
|
757
|
+
if (presentHttpFields.length > 0) {
|
|
758
|
+
throw new ProcedureRegistrationError(
|
|
759
|
+
name,
|
|
760
|
+
`HTTP fields require CreateHttp / CreateHttpStream. Procedure "${name}" has [${presentHttpFields.join(', ')}] which are not valid on Create.`,
|
|
761
|
+
definitionInfo,
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
- [ ] **Step 4: Mirror the guard in `src/create-stream.ts`**
|
|
767
|
+
|
|
768
|
+
Same block, adjusted message ("not valid on CreateStream").
|
|
769
|
+
|
|
770
|
+
- [ ] **Step 5: Run tests**
|
|
771
|
+
|
|
772
|
+
```bash
|
|
773
|
+
npx vitest run src/migration.test.ts
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
Expected: PASS.
|
|
777
|
+
|
|
778
|
+
- [ ] **Step 6: Commit**
|
|
779
|
+
|
|
780
|
+
```bash
|
|
781
|
+
git add src/create.ts src/create-stream.ts src/migration.test.ts
|
|
782
|
+
git commit -m "feat(migration): reject HTTP fields on Create/CreateStream with v8 message"
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
### Task 9: Move per-creator tests out of `src/index.test.ts`
|
|
788
|
+
|
|
789
|
+
**Files:**
|
|
790
|
+
- Create: `src/create.test.ts`, `src/create-stream.test.ts`
|
|
791
|
+
- Modify: `src/index.test.ts`
|
|
792
|
+
|
|
793
|
+
- [ ] **Step 1: Move `Create`-specific tests from `src/index.test.ts` to `src/create.test.ts`**
|
|
794
|
+
|
|
795
|
+
Identify tests in `src/index.test.ts` that exercise `Create` specifically (validation, error wrapping, `info` shape, etc.). Move them to a new file `src/create.test.ts`. Update imports.
|
|
796
|
+
|
|
797
|
+
- [ ] **Step 2: Move `CreateStream`-specific tests to `src/create-stream.test.ts`**
|
|
798
|
+
|
|
799
|
+
Same pattern.
|
|
800
|
+
|
|
801
|
+
- [ ] **Step 3: Keep `Procedures`-factory-level tests in `src/index.test.ts`**
|
|
802
|
+
|
|
803
|
+
Tests covering: `getProcedures`, `getProcedure`, `removeProcedure`, `clear`, duplicate-name rejection, `onCreate` hook, factory return shape.
|
|
804
|
+
|
|
805
|
+
- [ ] **Step 4: Run all three test files**
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
npx vitest run src/create.test.ts src/create-stream.test.ts src/index.test.ts
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
Expected: green.
|
|
812
|
+
|
|
813
|
+
- [ ] **Step 5: Commit**
|
|
814
|
+
|
|
815
|
+
```bash
|
|
816
|
+
git add src/create.test.ts src/create-stream.test.ts src/index.test.ts
|
|
817
|
+
git commit -m "test(core): split index.test.ts into per-creator files"
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
|
|
822
|
+
## Phase 3 — `CreateHttp` (req only, no res yet)
|
|
823
|
+
|
|
824
|
+
Add the new HTTP unary creator with structured request channels. Response shape (`res.body`/`res.headers`) lands in Phase 4. This phase keeps the surface minimal — handlers return whatever they want; we'll narrow in Phase 4.
|
|
825
|
+
|
|
826
|
+
### Task 10: Define `CreateHttp` types in `src/types.ts`
|
|
827
|
+
|
|
828
|
+
**Files:**
|
|
829
|
+
- Modify: `src/types.ts`
|
|
830
|
+
|
|
831
|
+
- [ ] **Step 1: Add HTTP method type and `CreateHttp` config / handler types**
|
|
832
|
+
|
|
833
|
+
Append to `src/types.ts`:
|
|
834
|
+
|
|
835
|
+
```ts
|
|
836
|
+
export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
|
|
837
|
+
|
|
838
|
+
export type TCreateHttpConfig<TReq, TRes, TErrorKey extends string = string> = {
|
|
839
|
+
path: string
|
|
840
|
+
method: HttpMethod
|
|
841
|
+
successStatus?: number
|
|
842
|
+
scope?: string
|
|
843
|
+
errors?: TErrorKey[]
|
|
844
|
+
description?: string
|
|
845
|
+
schema: {
|
|
846
|
+
req?: TReq
|
|
847
|
+
res?: TRes
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export type THttpProcedureRegistration<TContext = unknown> = {
|
|
852
|
+
name: string
|
|
853
|
+
kind: 'http'
|
|
854
|
+
config: {
|
|
855
|
+
path: string
|
|
856
|
+
method: HttpMethod
|
|
857
|
+
successStatus?: number
|
|
858
|
+
scope?: string
|
|
859
|
+
errors?: string[]
|
|
860
|
+
description?: string
|
|
861
|
+
schema?: {
|
|
862
|
+
req?: Record<string, TJSONSchema>
|
|
863
|
+
res?: { body?: TJSONSchema; headers?: TJSONSchema }
|
|
864
|
+
}
|
|
865
|
+
validation?: {
|
|
866
|
+
req?: Record<string, (value: any) => { errors?: any[] }>
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
handler: (ctx: TContext, req?: any) => Promise<any>
|
|
870
|
+
}
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
- [ ] **Step 2: Run lint to ensure imports resolve**
|
|
874
|
+
|
|
875
|
+
```bash
|
|
876
|
+
npm run lint
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
- [ ] **Step 3: Commit**
|
|
880
|
+
|
|
881
|
+
```bash
|
|
882
|
+
git add src/types.ts
|
|
883
|
+
git commit -m "feat(types): add HttpMethod and CreateHttp registration types"
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
### Task 11: Implement `makeCreateHttp` in `src/create-http.ts`
|
|
889
|
+
|
|
890
|
+
**Files:**
|
|
891
|
+
- Create: `src/create-http.ts`
|
|
892
|
+
|
|
893
|
+
- [ ] **Step 1: Write the file with the basic shape (req only, body return)**
|
|
894
|
+
|
|
895
|
+
```ts
|
|
896
|
+
import { ProcedureError, ProcedureRegistrationError, ProcedureValidationError } from './errors.js'
|
|
897
|
+
import { computeSchema } from './schema/compute-schema.js'
|
|
898
|
+
import { Prettify, TSchemaLib } from './schema/types.js'
|
|
899
|
+
import { captureDefinitionInfo } from './stack-utils.js'
|
|
900
|
+
import {
|
|
901
|
+
HttpMethod,
|
|
902
|
+
TBuilderConfig,
|
|
903
|
+
TLocalContext,
|
|
904
|
+
THttpProcedureRegistration,
|
|
905
|
+
TProcedureRegistration,
|
|
906
|
+
TStreamProcedureRegistration,
|
|
907
|
+
} from './types.js'
|
|
908
|
+
|
|
909
|
+
const PATH_PARAM_RE = /:([a-zA-Z_][a-zA-Z0-9_]*)/g
|
|
910
|
+
|
|
911
|
+
function extractPathParamNames(path: string): string[] {
|
|
912
|
+
const matches = path.match(PATH_PARAM_RE)
|
|
913
|
+
return matches ? matches.map((m) => m.slice(1)) : []
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function checkPathParamConsistency(
|
|
917
|
+
procedureName: string,
|
|
918
|
+
path: string,
|
|
919
|
+
pathParamsSchema: Record<string, unknown> | undefined,
|
|
920
|
+
definitionInfo: ReturnType<typeof captureDefinitionInfo>,
|
|
921
|
+
) {
|
|
922
|
+
const pathParamNames = extractPathParamNames(path)
|
|
923
|
+
const hasPathParams = pathParamNames.length > 0
|
|
924
|
+
const hasSchema = pathParamsSchema !== undefined
|
|
925
|
+
|
|
926
|
+
if (hasPathParams && !hasSchema) {
|
|
927
|
+
throw new ProcedureRegistrationError(
|
|
928
|
+
procedureName,
|
|
929
|
+
`Path "${path}" has path parameters [${pathParamNames.join(', ')}] but schema.req.pathParams is not defined.`,
|
|
930
|
+
definitionInfo,
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
if (!hasPathParams && hasSchema) {
|
|
934
|
+
throw new ProcedureRegistrationError(
|
|
935
|
+
procedureName,
|
|
936
|
+
`schema.req.pathParams is defined but path "${path}" has no path parameters.`,
|
|
937
|
+
definitionInfo,
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
if (hasPathParams && hasSchema) {
|
|
941
|
+
const schemaProperties = (pathParamsSchema as { properties?: Record<string, unknown> }).properties
|
|
942
|
+
if (schemaProperties) {
|
|
943
|
+
const schemaKeys = Object.keys(schemaProperties)
|
|
944
|
+
const missing = pathParamNames.filter((p) => !schemaKeys.includes(p))
|
|
945
|
+
const extra = schemaKeys.filter((k) => !pathParamNames.includes(k))
|
|
946
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
947
|
+
const parts: string[] = []
|
|
948
|
+
if (missing.length > 0) parts.push(`path has [${missing.join(', ')}] missing from schema`)
|
|
949
|
+
if (extra.length > 0) parts.push(`schema has [${extra.join(', ')}] not in path`)
|
|
950
|
+
throw new ProcedureRegistrationError(
|
|
951
|
+
procedureName,
|
|
952
|
+
`Path param mismatch for "${procedureName}": ${parts.join('; ')}.`,
|
|
953
|
+
definitionInfo,
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
export function makeCreateHttp<TContext>(
|
|
961
|
+
procedures: Map<string, TProcedureRegistration<TContext, any> | TStreamProcedureRegistration<TContext, any> | THttpProcedureRegistration<TContext>>,
|
|
962
|
+
builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
|
|
963
|
+
) {
|
|
964
|
+
return function CreateHttp<TName extends string, TReq extends Record<string, unknown> | undefined, TErrorKey extends string = string>(
|
|
965
|
+
name: TName,
|
|
966
|
+
config: {
|
|
967
|
+
path: string
|
|
968
|
+
method: HttpMethod
|
|
969
|
+
successStatus?: number
|
|
970
|
+
scope?: string
|
|
971
|
+
errors?: TErrorKey[]
|
|
972
|
+
description?: string
|
|
973
|
+
schema: {
|
|
974
|
+
req?: TReq
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
handler: (
|
|
978
|
+
ctx: Prettify<TContext & TLocalContext>,
|
|
979
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
980
|
+
) => Promise<unknown>,
|
|
981
|
+
) {
|
|
982
|
+
const definitionInfo = captureDefinitionInfo()
|
|
983
|
+
|
|
984
|
+
if (procedures.has(name)) {
|
|
985
|
+
throw new Error(`Procedure with name ${name} is already registered`)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if ((config.schema as any)?.params) {
|
|
989
|
+
throw new ProcedureRegistrationError(
|
|
990
|
+
name,
|
|
991
|
+
`Use schema.req.body (or schema.req.query) instead of schema.params on CreateHttp. Procedure: "${name}".`,
|
|
992
|
+
definitionInfo,
|
|
993
|
+
)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const { jsonSchema, validations } = computeSchema(name, {
|
|
997
|
+
req: config.schema.req as Record<string, unknown> | undefined,
|
|
998
|
+
}, definitionInfo)
|
|
999
|
+
|
|
1000
|
+
const pathParamsSchema = (jsonSchema.req as Record<string, unknown> | undefined)?.pathParams
|
|
1001
|
+
checkPathParamConsistency(name, config.path, pathParamsSchema as Record<string, unknown> | undefined, definitionInfo)
|
|
1002
|
+
|
|
1003
|
+
const errorFactory = (message: string, meta?: object) =>
|
|
1004
|
+
new ProcedureError(name, message, meta, definitionInfo)
|
|
1005
|
+
|
|
1006
|
+
const registeredProcedure: THttpProcedureRegistration<TContext> = {
|
|
1007
|
+
name,
|
|
1008
|
+
kind: 'http',
|
|
1009
|
+
config: {
|
|
1010
|
+
path: config.path,
|
|
1011
|
+
method: config.method,
|
|
1012
|
+
successStatus: config.successStatus,
|
|
1013
|
+
scope: config.scope,
|
|
1014
|
+
errors: config.errors as string[] | undefined,
|
|
1015
|
+
description: config.description,
|
|
1016
|
+
schema: jsonSchema as any,
|
|
1017
|
+
validation: { req: validations.req },
|
|
1018
|
+
},
|
|
1019
|
+
handler: async (ctx: TContext, req: any) => {
|
|
1020
|
+
try {
|
|
1021
|
+
const skipValidation =
|
|
1022
|
+
(ctx as { isPrevalidated?: boolean }).isPrevalidated ||
|
|
1023
|
+
builder?.config?.noRuntimeValidation
|
|
1024
|
+
|
|
1025
|
+
if (validations?.req && !skipValidation) {
|
|
1026
|
+
for (const [channel, validator] of Object.entries(validations.req)) {
|
|
1027
|
+
const channelValue = (req as Record<string, unknown>)?.[channel]
|
|
1028
|
+
const { errors } = validator(channelValue)
|
|
1029
|
+
if (errors) {
|
|
1030
|
+
throw new ProcedureValidationError(
|
|
1031
|
+
name,
|
|
1032
|
+
`Validation error for ${name} in req.${channel}`,
|
|
1033
|
+
errors,
|
|
1034
|
+
definitionInfo,
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const localCtx: TLocalContext = { error: errorFactory }
|
|
1041
|
+
return await handler({ ...ctx, ...localCtx } as any, req)
|
|
1042
|
+
} catch (error: any) {
|
|
1043
|
+
if (error instanceof ProcedureError) throw error
|
|
1044
|
+
const err = new ProcedureError(name, `Error in handler for ${name} - ${error?.message}`, undefined, definitionInfo)
|
|
1045
|
+
err.cause = error
|
|
1046
|
+
if (error.stack && definitionInfo.definedAt) {
|
|
1047
|
+
const { file, line, column } = definitionInfo.definedAt
|
|
1048
|
+
err.stack = error.stack + `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
|
|
1049
|
+
} else if (error.stack) {
|
|
1050
|
+
err.stack = error.stack
|
|
1051
|
+
}
|
|
1052
|
+
throw err
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
procedures.set(name, registeredProcedure as any)
|
|
1058
|
+
builder?.onCreate?.(registeredProcedure)
|
|
1059
|
+
|
|
1060
|
+
const info = { name, ...registeredProcedure.config }
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
[name]: registeredProcedure.handler,
|
|
1064
|
+
procedure: registeredProcedure.handler,
|
|
1065
|
+
info,
|
|
1066
|
+
} as {
|
|
1067
|
+
[K in TName]: typeof registeredProcedure.handler
|
|
1068
|
+
} & {
|
|
1069
|
+
procedure: typeof registeredProcedure.handler
|
|
1070
|
+
info: typeof info
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
- [ ] **Step 2: Wire `makeCreateHttp` into `Procedures` factory**
|
|
1077
|
+
|
|
1078
|
+
In `src/index.ts`:
|
|
1079
|
+
|
|
1080
|
+
```ts
|
|
1081
|
+
import { makeCreateHttp } from './create-http.js'
|
|
1082
|
+
|
|
1083
|
+
// inside Procedures():
|
|
1084
|
+
const CreateHttp = makeCreateHttp<TContext>(procedures as any, builder)
|
|
1085
|
+
|
|
1086
|
+
return {
|
|
1087
|
+
// existing fields ...
|
|
1088
|
+
Create,
|
|
1089
|
+
CreateStream,
|
|
1090
|
+
CreateHttp,
|
|
1091
|
+
}
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
Also update the `procedures` Map's type signature to accept `THttpProcedureRegistration`.
|
|
1095
|
+
|
|
1096
|
+
- [ ] **Step 3: Run lint and build**
|
|
1097
|
+
|
|
1098
|
+
```bash
|
|
1099
|
+
npm run lint
|
|
1100
|
+
npm run build
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
Expected: green.
|
|
1104
|
+
|
|
1105
|
+
- [ ] **Step 4: Commit**
|
|
1106
|
+
|
|
1107
|
+
```bash
|
|
1108
|
+
git add src/create-http.ts src/index.ts
|
|
1109
|
+
git commit -m "feat(core): add CreateHttp with req channels and path-param validation"
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
### Task 12: Test `CreateHttp` registration and req validation
|
|
1115
|
+
|
|
1116
|
+
**Files:**
|
|
1117
|
+
- Create: `src/create-http.test.ts`
|
|
1118
|
+
|
|
1119
|
+
- [ ] **Step 1: Write tests covering registration shape**
|
|
1120
|
+
|
|
1121
|
+
```ts
|
|
1122
|
+
import { describe, it, expect } from 'vitest'
|
|
1123
|
+
import { Procedures } from './index.js'
|
|
1124
|
+
import { ProcedureRegistrationError, ProcedureValidationError } from './errors.js'
|
|
1125
|
+
import { Type } from 'typebox'
|
|
1126
|
+
|
|
1127
|
+
describe('CreateHttp registration', () => {
|
|
1128
|
+
it('registers with kind: http', () => {
|
|
1129
|
+
const procs = Procedures()
|
|
1130
|
+
procs.CreateHttp('GetUser', {
|
|
1131
|
+
path: '/users/:id',
|
|
1132
|
+
method: 'get',
|
|
1133
|
+
schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
|
|
1134
|
+
}, async (_, { pathParams }) => ({ id: pathParams.id }))
|
|
1135
|
+
|
|
1136
|
+
const reg = procs.getProcedure('GetUser')
|
|
1137
|
+
expect(reg?.kind).toBe('http')
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
it('rejects schema.params with v8 message', () => {
|
|
1141
|
+
const procs = Procedures()
|
|
1142
|
+
expect(() =>
|
|
1143
|
+
procs.CreateHttp('Bad', {
|
|
1144
|
+
path: '/x', method: 'get',
|
|
1145
|
+
schema: { params: Type.Object({}) } as any,
|
|
1146
|
+
}, async () => undefined)
|
|
1147
|
+
).toThrow(/Use schema.req.body \(or schema.req.query\) instead of schema.params/)
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
it('rejects path with params but no pathParams schema', () => {
|
|
1151
|
+
const procs = Procedures()
|
|
1152
|
+
expect(() =>
|
|
1153
|
+
procs.CreateHttp('Bad', {
|
|
1154
|
+
path: '/users/:id', method: 'get',
|
|
1155
|
+
schema: { req: {} },
|
|
1156
|
+
}, async () => undefined)
|
|
1157
|
+
).toThrow(/path parameters \[id\] but schema.req.pathParams is not defined/)
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
it('rejects pathParams schema with no path params', () => {
|
|
1161
|
+
const procs = Procedures()
|
|
1162
|
+
expect(() =>
|
|
1163
|
+
procs.CreateHttp('Bad', {
|
|
1164
|
+
path: '/users', method: 'get',
|
|
1165
|
+
schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
|
|
1166
|
+
}, async () => undefined)
|
|
1167
|
+
).toThrow(/has no path parameters/)
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
it('rejects pathParams key mismatch', () => {
|
|
1171
|
+
const procs = Procedures()
|
|
1172
|
+
expect(() =>
|
|
1173
|
+
procs.CreateHttp('Bad', {
|
|
1174
|
+
path: '/users/:id', method: 'get',
|
|
1175
|
+
schema: { req: { pathParams: Type.Object({ wrongKey: Type.String() }) } },
|
|
1176
|
+
}, async () => undefined)
|
|
1177
|
+
).toThrow(/Path param mismatch/)
|
|
1178
|
+
})
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
describe('CreateHttp req channel validation', () => {
|
|
1182
|
+
it('validates each channel independently with channel-tagged error', async () => {
|
|
1183
|
+
const procs = Procedures()
|
|
1184
|
+
const { GetUser } = procs.CreateHttp('GetUser', {
|
|
1185
|
+
path: '/users/:id', method: 'get',
|
|
1186
|
+
schema: {
|
|
1187
|
+
req: {
|
|
1188
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
1189
|
+
query: Type.Object({ limit: Type.Number() }),
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
}, async (_, { pathParams, query }) => ({ id: pathParams.id, limit: query.limit }))
|
|
1193
|
+
|
|
1194
|
+
await expect(GetUser({} as any, {
|
|
1195
|
+
pathParams: { id: 'abc' },
|
|
1196
|
+
query: { limit: 'not-a-number' as any },
|
|
1197
|
+
})).rejects.toThrow(ProcedureValidationError)
|
|
1198
|
+
await expect(GetUser({} as any, {
|
|
1199
|
+
pathParams: { id: 'abc' },
|
|
1200
|
+
query: { limit: 'not-a-number' as any },
|
|
1201
|
+
})).rejects.toThrow(/req\.query/)
|
|
1202
|
+
})
|
|
1203
|
+
})
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
- [ ] **Step 2: Run tests**
|
|
1207
|
+
|
|
1208
|
+
```bash
|
|
1209
|
+
npx vitest run src/create-http.test.ts
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
Expected: PASS for all five registration tests and the validation test.
|
|
1213
|
+
|
|
1214
|
+
- [ ] **Step 3: Commit**
|
|
1215
|
+
|
|
1216
|
+
```bash
|
|
1217
|
+
git add src/create-http.test.ts
|
|
1218
|
+
git commit -m "test(core): cover CreateHttp registration and req validation"
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
---
|
|
1222
|
+
|
|
1223
|
+
## Phase 4 — `CreateHttp` response shape (`res.body`, `res.headers`, conditional return)
|
|
1224
|
+
|
|
1225
|
+
### Task 13: Add `res.body` to `CreateHttp` config and types
|
|
1226
|
+
|
|
1227
|
+
**Files:**
|
|
1228
|
+
- Modify: `src/types.ts`
|
|
1229
|
+
- Modify: `src/create-http.ts`
|
|
1230
|
+
- Test: `src/create-http.test.ts`
|
|
1231
|
+
|
|
1232
|
+
- [ ] **Step 1: Extend the config type to include `res.body`**
|
|
1233
|
+
|
|
1234
|
+
In `src/types.ts`, update `TCreateHttpConfig`:
|
|
1235
|
+
|
|
1236
|
+
```ts
|
|
1237
|
+
export type TCreateHttpConfig<TReq, TRes, TErrorKey extends string = string> = {
|
|
1238
|
+
path: string
|
|
1239
|
+
method: HttpMethod
|
|
1240
|
+
successStatus?: number
|
|
1241
|
+
scope?: string
|
|
1242
|
+
errors?: TErrorKey[]
|
|
1243
|
+
description?: string
|
|
1244
|
+
schema: {
|
|
1245
|
+
req?: TReq
|
|
1246
|
+
res?: TRes // { body?, headers? } — narrowed at the creator call site
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
- [ ] **Step 2: Update `makeCreateHttp` signature to thread `TRes`**
|
|
1252
|
+
|
|
1253
|
+
In `src/create-http.ts`:
|
|
1254
|
+
|
|
1255
|
+
```ts
|
|
1256
|
+
return function CreateHttp<
|
|
1257
|
+
TName extends string,
|
|
1258
|
+
TReq extends Record<string, unknown> | undefined,
|
|
1259
|
+
TRes extends { body?: unknown; headers?: unknown } | undefined = undefined,
|
|
1260
|
+
TErrorKey extends string = string,
|
|
1261
|
+
>(
|
|
1262
|
+
name: TName,
|
|
1263
|
+
config: TCreateHttpConfig<TReq, TRes, TErrorKey>,
|
|
1264
|
+
handler: (
|
|
1265
|
+
ctx: Prettify<TContext & TLocalContext>,
|
|
1266
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
1267
|
+
) => Promise<HttpReturn<TRes>>,
|
|
1268
|
+
) { /* ... */ }
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
Add the `HttpReturn` helper type at the top of the file:
|
|
1272
|
+
|
|
1273
|
+
```ts
|
|
1274
|
+
type Infer<T> = TSchemaLib<T>
|
|
1275
|
+
|
|
1276
|
+
export type HttpReturn<TRes> =
|
|
1277
|
+
TRes extends { body: infer B; headers: infer H } ? { body: Infer<B>; headers: Infer<H> }
|
|
1278
|
+
: TRes extends { headers: infer H } ? { headers: Infer<H> }
|
|
1279
|
+
: TRes extends { body: infer B } ? Infer<B>
|
|
1280
|
+
: void
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
- [ ] **Step 3: Pass `res` through to `computeSchema`**
|
|
1284
|
+
|
|
1285
|
+
In the body of `makeCreateHttp`'s creator function:
|
|
1286
|
+
|
|
1287
|
+
```ts
|
|
1288
|
+
const { jsonSchema, validations } = computeSchema(name, {
|
|
1289
|
+
req: config.schema.req as Record<string, unknown> | undefined,
|
|
1290
|
+
res: config.schema.res as { body?: unknown; headers?: unknown } | undefined,
|
|
1291
|
+
}, definitionInfo)
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
- [ ] **Step 4: Add a test for body-only return**
|
|
1295
|
+
|
|
1296
|
+
In `src/create-http.test.ts`:
|
|
1297
|
+
|
|
1298
|
+
```ts
|
|
1299
|
+
describe('CreateHttp res.body', () => {
|
|
1300
|
+
it('handler returns body bare when only res.body declared', async () => {
|
|
1301
|
+
const procs = Procedures()
|
|
1302
|
+
const { GetUser } = procs.CreateHttp('GetUser', {
|
|
1303
|
+
path: '/users/:id', method: 'get',
|
|
1304
|
+
schema: {
|
|
1305
|
+
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
1306
|
+
res: { body: Type.Object({ id: Type.String(), name: Type.String() }) },
|
|
1307
|
+
},
|
|
1308
|
+
}, async (_, { pathParams }) => ({ id: pathParams.id, name: 'Alice' }))
|
|
1309
|
+
|
|
1310
|
+
const result = await GetUser({} as any, { pathParams: { id: '1' } })
|
|
1311
|
+
expect(result).toEqual({ id: '1', name: 'Alice' })
|
|
1312
|
+
})
|
|
1313
|
+
})
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
- [ ] **Step 5: Run tests**
|
|
1317
|
+
|
|
1318
|
+
```bash
|
|
1319
|
+
npx vitest run src/create-http.test.ts -t "res.body"
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
Expected: PASS.
|
|
1323
|
+
|
|
1324
|
+
- [ ] **Step 6: Commit**
|
|
1325
|
+
|
|
1326
|
+
```bash
|
|
1327
|
+
git add src/types.ts src/create-http.ts src/create-http.test.ts
|
|
1328
|
+
git commit -m "feat(core): add CreateHttp res.body with bare-body return shape"
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
---
|
|
1332
|
+
|
|
1333
|
+
### Task 14: Add `res.headers` and conditional `{ body, headers }` return
|
|
1334
|
+
|
|
1335
|
+
**Files:**
|
|
1336
|
+
- Modify: `src/create-http.ts`
|
|
1337
|
+
- Test: `src/create-http.test.ts`
|
|
1338
|
+
|
|
1339
|
+
- [ ] **Step 1: Write failing tests for the four return shapes**
|
|
1340
|
+
|
|
1341
|
+
```ts
|
|
1342
|
+
describe('CreateHttp conditional return shape', () => {
|
|
1343
|
+
it('res: { body, headers } → returns { body, headers }', async () => {
|
|
1344
|
+
const procs = Procedures()
|
|
1345
|
+
const { GetUser } = procs.CreateHttp('GetUser', {
|
|
1346
|
+
path: '/users/:id', method: 'get',
|
|
1347
|
+
schema: {
|
|
1348
|
+
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
1349
|
+
res: {
|
|
1350
|
+
body: Type.Object({ id: Type.String() }),
|
|
1351
|
+
headers: Type.Object({ 'x-rate-limit': Type.String() }),
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
}, async (_, { pathParams }) => ({
|
|
1355
|
+
body: { id: pathParams.id },
|
|
1356
|
+
headers: { 'x-rate-limit': '99' },
|
|
1357
|
+
}))
|
|
1358
|
+
|
|
1359
|
+
const result = await GetUser({} as any, { pathParams: { id: '1' } })
|
|
1360
|
+
expect(result).toEqual({ body: { id: '1' }, headers: { 'x-rate-limit': '99' } })
|
|
1361
|
+
})
|
|
1362
|
+
|
|
1363
|
+
it('res: { headers } only → returns { headers }', async () => {
|
|
1364
|
+
const procs = Procedures()
|
|
1365
|
+
const { Healthcheck } = procs.CreateHttp('Healthcheck', {
|
|
1366
|
+
path: '/health', method: 'get',
|
|
1367
|
+
schema: {
|
|
1368
|
+
res: { headers: Type.Object({ 'x-version': Type.String() }) },
|
|
1369
|
+
},
|
|
1370
|
+
}, async () => ({ headers: { 'x-version': '8.0.0' } }))
|
|
1371
|
+
|
|
1372
|
+
const result = await Healthcheck({} as any, undefined)
|
|
1373
|
+
expect(result).toEqual({ headers: { 'x-version': '8.0.0' } })
|
|
1374
|
+
})
|
|
1375
|
+
|
|
1376
|
+
it('no res → returns void', async () => {
|
|
1377
|
+
const procs = Procedures()
|
|
1378
|
+
const { Ping } = procs.CreateHttp('Ping', {
|
|
1379
|
+
path: '/ping', method: 'post',
|
|
1380
|
+
schema: { req: { body: Type.Object({}) } },
|
|
1381
|
+
}, async () => undefined)
|
|
1382
|
+
|
|
1383
|
+
const result = await Ping({} as any, { body: {} })
|
|
1384
|
+
expect(result).toBeUndefined()
|
|
1385
|
+
})
|
|
1386
|
+
})
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
- [ ] **Step 2: Run, verify they pass**
|
|
1390
|
+
|
|
1391
|
+
The handler return shape is enforced at the **type level** by `HttpReturn<TRes>` (from Task 13). Runtime doesn't validate `res` body/headers (per spec out-of-scope). So if tests are typed correctly, runtime just passes through whatever the handler returns.
|
|
1392
|
+
|
|
1393
|
+
```bash
|
|
1394
|
+
npx vitest run src/create-http.test.ts -t "conditional return"
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
Expected: PASS (no implementation change needed beyond Task 13's `HttpReturn` type — runtime is pass-through).
|
|
1398
|
+
|
|
1399
|
+
- [ ] **Step 3: Add a type-level test (using `expectTypeOf`)**
|
|
1400
|
+
|
|
1401
|
+
```ts
|
|
1402
|
+
import { expectTypeOf } from 'vitest'
|
|
1403
|
+
|
|
1404
|
+
describe('CreateHttp HttpReturn type inference', () => {
|
|
1405
|
+
it('infers correct return shapes', () => {
|
|
1406
|
+
const procs = Procedures()
|
|
1407
|
+
const { GetUser } = procs.CreateHttp('GetUser', {
|
|
1408
|
+
path: '/users/:id', method: 'get',
|
|
1409
|
+
schema: {
|
|
1410
|
+
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
1411
|
+
res: {
|
|
1412
|
+
body: Type.Object({ id: Type.String() }),
|
|
1413
|
+
headers: Type.Object({ 'x-rate-limit': Type.String() }),
|
|
1414
|
+
},
|
|
1415
|
+
},
|
|
1416
|
+
}, async () => ({ body: { id: '1' }, headers: { 'x-rate-limit': '99' } }))
|
|
1417
|
+
|
|
1418
|
+
type ReturnT = Awaited<ReturnType<typeof GetUser>>
|
|
1419
|
+
expectTypeOf<ReturnT>().toEqualTypeOf<{ body: { id: string }; headers: { 'x-rate-limit': string } }>()
|
|
1420
|
+
})
|
|
1421
|
+
})
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
- [ ] **Step 4: Run tests**
|
|
1425
|
+
|
|
1426
|
+
```bash
|
|
1427
|
+
npx vitest run src/create-http.test.ts
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
Expected: PASS.
|
|
1431
|
+
|
|
1432
|
+
- [ ] **Step 5: Commit**
|
|
1433
|
+
|
|
1434
|
+
```bash
|
|
1435
|
+
git add src/create-http.ts src/create-http.test.ts
|
|
1436
|
+
git commit -m "feat(core): conditional return shape on CreateHttp (body, headers, both, void)"
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
---
|
|
1440
|
+
|
|
1441
|
+
## Phase 5 — `CreateHttpStream`
|
|
1442
|
+
|
|
1443
|
+
### Task 15: Define `CreateHttpStream` types
|
|
1444
|
+
|
|
1445
|
+
**Files:**
|
|
1446
|
+
- Modify: `src/types.ts`
|
|
1447
|
+
|
|
1448
|
+
- [ ] **Step 1: Add the streaming registration type and config**
|
|
1449
|
+
|
|
1450
|
+
```ts
|
|
1451
|
+
export type THttpStreamProcedureRegistration<TContext = unknown> = {
|
|
1452
|
+
name: string
|
|
1453
|
+
kind: 'http-stream'
|
|
1454
|
+
config: {
|
|
1455
|
+
path: string
|
|
1456
|
+
method: HttpMethod
|
|
1457
|
+
scope?: string
|
|
1458
|
+
errors?: string[]
|
|
1459
|
+
description?: string
|
|
1460
|
+
schema?: {
|
|
1461
|
+
req?: Record<string, TJSONSchema>
|
|
1462
|
+
res?: { headers?: TJSONSchema }
|
|
1463
|
+
yield?: TJSONSchema
|
|
1464
|
+
returnType?: TJSONSchema
|
|
1465
|
+
}
|
|
1466
|
+
validation?: {
|
|
1467
|
+
req?: Record<string, (value: any) => { errors?: any[] }>
|
|
1468
|
+
yield?: (value: any) => { errors?: any[] }
|
|
1469
|
+
}
|
|
1470
|
+
validateYields?: boolean
|
|
1471
|
+
}
|
|
1472
|
+
handler: (ctx: TContext, req?: any) => AsyncGenerator<any, any, unknown> | Promise<{ headers: Record<string, string>; stream: AsyncGenerator<any, any, unknown> }>
|
|
1473
|
+
}
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
- [ ] **Step 2: Commit**
|
|
1477
|
+
|
|
1478
|
+
```bash
|
|
1479
|
+
git add src/types.ts
|
|
1480
|
+
git commit -m "feat(types): add CreateHttpStream registration type"
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
---
|
|
1484
|
+
|
|
1485
|
+
### Task 16: Implement `makeCreateHttpStream` (no res.headers branch — plain async generator)
|
|
1486
|
+
|
|
1487
|
+
**Files:**
|
|
1488
|
+
- Create: `src/create-http-stream.ts`
|
|
1489
|
+
|
|
1490
|
+
- [ ] **Step 1: Write the file**
|
|
1491
|
+
|
|
1492
|
+
Use the existing `CreateStream` body from `src/create-stream.ts` as the template. Required adaptations:
|
|
1493
|
+
1. Accept `path`/`method`/`scope`/`errors` config fields.
|
|
1494
|
+
2. Use `req` channels (mirror `create-http.ts` validation loop).
|
|
1495
|
+
3. Set `kind: 'http-stream'` on registration.
|
|
1496
|
+
4. Reject `schema.params` with v8 message (mirror `create-http.ts`).
|
|
1497
|
+
5. Run path-param consistency check (call the helper exported from `create-http.ts` — extract it or duplicate).
|
|
1498
|
+
|
|
1499
|
+
```ts
|
|
1500
|
+
import { ProcedureError, ProcedureRegistrationError, ProcedureValidationError, ProcedureYieldValidationError } from './errors.js'
|
|
1501
|
+
import { computeSchema } from './schema/compute-schema.js'
|
|
1502
|
+
import { Prettify, TSchemaLib } from './schema/types.js'
|
|
1503
|
+
import { captureDefinitionInfo } from './stack-utils.js'
|
|
1504
|
+
import {
|
|
1505
|
+
HttpMethod, TBuilderConfig, TStreamContext,
|
|
1506
|
+
THttpStreamProcedureRegistration, TProcedureRegistration, TStreamProcedureRegistration, THttpProcedureRegistration,
|
|
1507
|
+
} from './types.js'
|
|
1508
|
+
import { checkPathParamConsistency } from './create-http.js' // export it from create-http.ts
|
|
1509
|
+
|
|
1510
|
+
export function makeCreateHttpStream<TContext>(
|
|
1511
|
+
procedures: Map<string, TProcedureRegistration<TContext, any> | TStreamProcedureRegistration<TContext, any> | THttpProcedureRegistration<TContext> | THttpStreamProcedureRegistration<TContext>>,
|
|
1512
|
+
builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
|
|
1513
|
+
) {
|
|
1514
|
+
return function CreateHttpStream<TName extends string, TReq extends Record<string, unknown> | undefined, TYieldType, TReturnType = void, TErrorKey extends string = string>(
|
|
1515
|
+
name: TName,
|
|
1516
|
+
config: {
|
|
1517
|
+
path: string
|
|
1518
|
+
method: HttpMethod
|
|
1519
|
+
scope?: string
|
|
1520
|
+
errors?: TErrorKey[]
|
|
1521
|
+
description?: string
|
|
1522
|
+
schema: {
|
|
1523
|
+
req?: TReq
|
|
1524
|
+
yield?: TYieldType
|
|
1525
|
+
returnType?: TReturnType
|
|
1526
|
+
// res: { headers? } added in Task 18
|
|
1527
|
+
}
|
|
1528
|
+
validateYields?: boolean
|
|
1529
|
+
},
|
|
1530
|
+
handler: (
|
|
1531
|
+
ctx: Prettify<TContext & TStreamContext>,
|
|
1532
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
1533
|
+
) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>,
|
|
1534
|
+
) {
|
|
1535
|
+
const definitionInfo = captureDefinitionInfo()
|
|
1536
|
+
|
|
1537
|
+
if (procedures.has(name)) throw new Error(`Procedure with name ${name} is already registered`)
|
|
1538
|
+
if ((config.schema as any)?.params) {
|
|
1539
|
+
throw new ProcedureRegistrationError(name, `Use schema.req.body (or schema.req.query) instead of schema.params on CreateHttpStream. Procedure: "${name}".`, definitionInfo)
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const { jsonSchema, validations } = computeSchema(name, {
|
|
1543
|
+
req: config.schema.req as Record<string, unknown> | undefined,
|
|
1544
|
+
yieldType: config.schema.yield,
|
|
1545
|
+
returnType: config.schema.returnType,
|
|
1546
|
+
}, definitionInfo)
|
|
1547
|
+
|
|
1548
|
+
const pathParamsSchema = (jsonSchema.req as Record<string, unknown> | undefined)?.pathParams
|
|
1549
|
+
checkPathParamConsistency(name, config.path, pathParamsSchema as Record<string, unknown> | undefined, definitionInfo)
|
|
1550
|
+
|
|
1551
|
+
const errorFactory = (message: string, meta?: object) => new ProcedureError(name, message, meta, definitionInfo)
|
|
1552
|
+
const validateYields = config.validateYields ?? false
|
|
1553
|
+
|
|
1554
|
+
const wrappedHandler = async function* (ctx: TContext, req: any) {
|
|
1555
|
+
const abortController = new AbortController()
|
|
1556
|
+
const skipValidation = (ctx as { isPrevalidated?: boolean }).isPrevalidated || builder?.config?.noRuntimeValidation
|
|
1557
|
+
|
|
1558
|
+
if (validations?.req && !skipValidation) {
|
|
1559
|
+
for (const [channel, validator] of Object.entries(validations.req)) {
|
|
1560
|
+
const channelValue = (req as Record<string, unknown>)?.[channel]
|
|
1561
|
+
const { errors } = validator(channelValue)
|
|
1562
|
+
if (errors) {
|
|
1563
|
+
throw new ProcedureValidationError(name, `Validation error for ${name} in req.${channel}`, errors, definitionInfo)
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const incomingSignal = (ctx as { signal?: AbortSignal }).signal
|
|
1569
|
+
const signal = incomingSignal ? AbortSignal.any([incomingSignal, abortController.signal]) : abortController.signal
|
|
1570
|
+
const streamCtx: TStreamContext = { error: errorFactory, signal }
|
|
1571
|
+
|
|
1572
|
+
const userGenerator = handler({ ...ctx, ...streamCtx } as any, req)
|
|
1573
|
+
const userIterator = userGenerator[Symbol.asyncIterator]()
|
|
1574
|
+
|
|
1575
|
+
try {
|
|
1576
|
+
let userIterResult = await userIterator.next()
|
|
1577
|
+
while (!userIterResult.done) {
|
|
1578
|
+
const value = userIterResult.value
|
|
1579
|
+
if (validateYields && validations.yield) {
|
|
1580
|
+
const { errors } = validations.yield(value)
|
|
1581
|
+
if (errors) throw new ProcedureYieldValidationError(name, `Yield validation error for ${name}`, errors, definitionInfo)
|
|
1582
|
+
}
|
|
1583
|
+
yield value
|
|
1584
|
+
userIterResult = await userIterator.next()
|
|
1585
|
+
}
|
|
1586
|
+
return userIterResult.value
|
|
1587
|
+
} catch (error: any) {
|
|
1588
|
+
if (definitionInfo.definedAt && error && typeof error.stack === 'string') {
|
|
1589
|
+
const { file, line, column } = definitionInfo.definedAt
|
|
1590
|
+
error.stack = `${error.stack}\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
|
|
1591
|
+
}
|
|
1592
|
+
throw error
|
|
1593
|
+
} finally {
|
|
1594
|
+
try { await userIterator.return?.(undefined) } catch { /* swallow cleanup */ }
|
|
1595
|
+
abortController.abort('stream-completed')
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const registeredProcedure: THttpStreamProcedureRegistration<TContext> = {
|
|
1600
|
+
name,
|
|
1601
|
+
kind: 'http-stream',
|
|
1602
|
+
config: {
|
|
1603
|
+
path: config.path,
|
|
1604
|
+
method: config.method,
|
|
1605
|
+
scope: config.scope,
|
|
1606
|
+
errors: config.errors as string[] | undefined,
|
|
1607
|
+
description: config.description,
|
|
1608
|
+
schema: jsonSchema as any,
|
|
1609
|
+
validation: { req: validations.req, yield: validations.yield },
|
|
1610
|
+
validateYields,
|
|
1611
|
+
},
|
|
1612
|
+
handler: wrappedHandler as any,
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
procedures.set(name, registeredProcedure as any)
|
|
1616
|
+
builder?.onCreate?.(registeredProcedure)
|
|
1617
|
+
|
|
1618
|
+
const info = { name, kind: 'http-stream' as const, ...registeredProcedure.config }
|
|
1619
|
+
|
|
1620
|
+
return {
|
|
1621
|
+
[name]: registeredProcedure.handler,
|
|
1622
|
+
procedure: registeredProcedure.handler,
|
|
1623
|
+
info,
|
|
1624
|
+
} as {
|
|
1625
|
+
[K in TName]: typeof registeredProcedure.handler
|
|
1626
|
+
} & {
|
|
1627
|
+
procedure: typeof registeredProcedure.handler
|
|
1628
|
+
info: typeof info
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
- [ ] **Step 2: Export `checkPathParamConsistency` from `create-http.ts`**
|
|
1635
|
+
|
|
1636
|
+
Add `export` keyword to the helper.
|
|
1637
|
+
|
|
1638
|
+
- [ ] **Step 3: Wire `CreateHttpStream` into `Procedures` factory**
|
|
1639
|
+
|
|
1640
|
+
In `src/index.ts`:
|
|
1641
|
+
|
|
1642
|
+
```ts
|
|
1643
|
+
import { makeCreateHttpStream } from './create-http-stream.js'
|
|
1644
|
+
|
|
1645
|
+
// inside Procedures():
|
|
1646
|
+
const CreateHttpStream = makeCreateHttpStream<TContext>(procedures as any, builder)
|
|
1647
|
+
|
|
1648
|
+
return { /* ... */ Create, CreateStream, CreateHttp, CreateHttpStream }
|
|
1649
|
+
```
|
|
1650
|
+
|
|
1651
|
+
- [ ] **Step 4: Build**
|
|
1652
|
+
|
|
1653
|
+
```bash
|
|
1654
|
+
npm run build
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
Expected: green.
|
|
1658
|
+
|
|
1659
|
+
- [ ] **Step 5: Commit**
|
|
1660
|
+
|
|
1661
|
+
```bash
|
|
1662
|
+
git add src/create-http-stream.ts src/create-http.ts src/index.ts
|
|
1663
|
+
git commit -m "feat(core): add CreateHttpStream (plain async generator, no res.headers yet)"
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
---
|
|
1667
|
+
|
|
1668
|
+
### Task 17: Test `CreateHttpStream` plain-generator path
|
|
1669
|
+
|
|
1670
|
+
**Files:**
|
|
1671
|
+
- Create: `src/create-http-stream.test.ts`
|
|
1672
|
+
|
|
1673
|
+
- [ ] **Step 1: Write tests covering the basic shape**
|
|
1674
|
+
|
|
1675
|
+
```ts
|
|
1676
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
1677
|
+
import { Procedures } from './index.js'
|
|
1678
|
+
import { Type } from 'typebox'
|
|
1679
|
+
import { ProcedureValidationError } from './errors.js'
|
|
1680
|
+
|
|
1681
|
+
describe('CreateHttpStream basic generator', () => {
|
|
1682
|
+
it('registers with kind: http-stream', () => {
|
|
1683
|
+
const procs = Procedures()
|
|
1684
|
+
procs.CreateHttpStream('Tail', {
|
|
1685
|
+
path: '/streams/logs', method: 'get',
|
|
1686
|
+
schema: {
|
|
1687
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
1688
|
+
yield: Type.Object({ line: Type.String() }),
|
|
1689
|
+
},
|
|
1690
|
+
}, async function* () { yield { line: 'a' } })
|
|
1691
|
+
|
|
1692
|
+
expect(procs.getProcedure('Tail')?.kind).toBe('http-stream')
|
|
1693
|
+
})
|
|
1694
|
+
|
|
1695
|
+
it('yields and returns', async () => {
|
|
1696
|
+
const procs = Procedures()
|
|
1697
|
+
const { Tail } = procs.CreateHttpStream('Tail', {
|
|
1698
|
+
path: '/streams/logs', method: 'get',
|
|
1699
|
+
schema: {
|
|
1700
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
1701
|
+
yield: Type.Object({ line: Type.String() }),
|
|
1702
|
+
returnType: Type.Object({ totalLines: Type.Number() }),
|
|
1703
|
+
},
|
|
1704
|
+
}, async function* (_, { query }) {
|
|
1705
|
+
yield { line: `from-${query.source}-1` }
|
|
1706
|
+
yield { line: `from-${query.source}-2` }
|
|
1707
|
+
return { totalLines: 2 }
|
|
1708
|
+
})
|
|
1709
|
+
|
|
1710
|
+
const lines: string[] = []
|
|
1711
|
+
let returnVal: unknown
|
|
1712
|
+
const gen = Tail({} as any, { query: { source: 'app' } })
|
|
1713
|
+
let result = await gen.next()
|
|
1714
|
+
while (!result.done) {
|
|
1715
|
+
lines.push(result.value.line)
|
|
1716
|
+
result = await gen.next()
|
|
1717
|
+
}
|
|
1718
|
+
returnVal = result.value
|
|
1719
|
+
expect(lines).toEqual(['from-app-1', 'from-app-2'])
|
|
1720
|
+
expect(returnVal).toEqual({ totalLines: 2 })
|
|
1721
|
+
})
|
|
1722
|
+
|
|
1723
|
+
it('rejects req.query validation error before opening stream', async () => {
|
|
1724
|
+
const procs = Procedures()
|
|
1725
|
+
const { Tail } = procs.CreateHttpStream('Tail', {
|
|
1726
|
+
path: '/streams/logs', method: 'get',
|
|
1727
|
+
schema: {
|
|
1728
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
1729
|
+
yield: Type.Object({ line: Type.String() }),
|
|
1730
|
+
},
|
|
1731
|
+
}, async function* () { yield { line: 'should not run' } })
|
|
1732
|
+
|
|
1733
|
+
const gen = Tail({} as any, { query: {} as any })
|
|
1734
|
+
await expect(gen.next()).rejects.toThrow(ProcedureValidationError)
|
|
1735
|
+
await expect(gen.next()).rejects.toThrow(/req\.query/)
|
|
1736
|
+
})
|
|
1737
|
+
})
|
|
1738
|
+
```
|
|
1739
|
+
|
|
1740
|
+
- [ ] **Step 2: Run tests**
|
|
1741
|
+
|
|
1742
|
+
```bash
|
|
1743
|
+
npx vitest run src/create-http-stream.test.ts
|
|
1744
|
+
```
|
|
1745
|
+
|
|
1746
|
+
Expected: PASS.
|
|
1747
|
+
|
|
1748
|
+
- [ ] **Step 3: Commit**
|
|
1749
|
+
|
|
1750
|
+
```bash
|
|
1751
|
+
git add src/create-http-stream.test.ts
|
|
1752
|
+
git commit -m "test(core): cover CreateHttpStream basic generator path"
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
---
|
|
1756
|
+
|
|
1757
|
+
### Task 18: Add `res.headers` async-preamble shape to `CreateHttpStream`
|
|
1758
|
+
|
|
1759
|
+
**Files:**
|
|
1760
|
+
- Modify: `src/create-http-stream.ts`
|
|
1761
|
+
- Modify: `src/types.ts`
|
|
1762
|
+
- Test: `src/create-http-stream.test.ts`
|
|
1763
|
+
|
|
1764
|
+
- [ ] **Step 1: Extend the config and handler types**
|
|
1765
|
+
|
|
1766
|
+
In `src/create-http-stream.ts`, widen the config and handler signature:
|
|
1767
|
+
|
|
1768
|
+
```ts
|
|
1769
|
+
return function CreateHttpStream<
|
|
1770
|
+
TName extends string,
|
|
1771
|
+
TReq extends Record<string, unknown> | undefined,
|
|
1772
|
+
TYieldType,
|
|
1773
|
+
TReturnType = void,
|
|
1774
|
+
TResHeaders = undefined,
|
|
1775
|
+
TErrorKey extends string = string,
|
|
1776
|
+
>(
|
|
1777
|
+
name: TName,
|
|
1778
|
+
config: {
|
|
1779
|
+
path: string
|
|
1780
|
+
method: HttpMethod
|
|
1781
|
+
scope?: string
|
|
1782
|
+
errors?: TErrorKey[]
|
|
1783
|
+
description?: string
|
|
1784
|
+
schema: {
|
|
1785
|
+
req?: TReq
|
|
1786
|
+
yield?: TYieldType
|
|
1787
|
+
returnType?: TReturnType
|
|
1788
|
+
res?: TResHeaders extends undefined ? undefined : { headers: TResHeaders }
|
|
1789
|
+
}
|
|
1790
|
+
validateYields?: boolean
|
|
1791
|
+
},
|
|
1792
|
+
handler: TResHeaders extends undefined
|
|
1793
|
+
? (
|
|
1794
|
+
ctx: Prettify<TContext & TStreamContext>,
|
|
1795
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
1796
|
+
) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
|
|
1797
|
+
: (
|
|
1798
|
+
ctx: Prettify<TContext & TStreamContext>,
|
|
1799
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
1800
|
+
) => Promise<{ headers: TSchemaLib<TResHeaders>; stream: AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown> }>,
|
|
1801
|
+
) { /* ... */ }
|
|
1802
|
+
```
|
|
1803
|
+
|
|
1804
|
+
- [ ] **Step 2: Update the wrapped handler to detect the async-preamble shape at runtime**
|
|
1805
|
+
|
|
1806
|
+
The runtime needs to introspect what the user handler returns. If it's a generator (has `Symbol.asyncIterator`), use today's path. If it's a Promise, await it for `{ headers, stream }` and use the `stream` field.
|
|
1807
|
+
|
|
1808
|
+
Replace the `wrappedHandler` body in `makeCreateHttpStream`:
|
|
1809
|
+
|
|
1810
|
+
```ts
|
|
1811
|
+
const wrappedHandler = async function* (ctx: TContext, req: any) {
|
|
1812
|
+
const abortController = new AbortController()
|
|
1813
|
+
const skipValidation = (ctx as { isPrevalidated?: boolean }).isPrevalidated || builder?.config?.noRuntimeValidation
|
|
1814
|
+
|
|
1815
|
+
if (validations?.req && !skipValidation) {
|
|
1816
|
+
for (const [channel, validator] of Object.entries(validations.req)) {
|
|
1817
|
+
const channelValue = (req as Record<string, unknown>)?.[channel]
|
|
1818
|
+
const { errors } = validator(channelValue)
|
|
1819
|
+
if (errors) throw new ProcedureValidationError(name, `Validation error for ${name} in req.${channel}`, errors, definitionInfo)
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const incomingSignal = (ctx as { signal?: AbortSignal }).signal
|
|
1824
|
+
const signal = incomingSignal ? AbortSignal.any([incomingSignal, abortController.signal]) : abortController.signal
|
|
1825
|
+
const streamCtx: TStreamContext = { error: errorFactory, signal }
|
|
1826
|
+
|
|
1827
|
+
const handlerResult: any = (handler as any)({ ...ctx, ...streamCtx }, req)
|
|
1828
|
+
|
|
1829
|
+
// Detect async-preamble shape: handler returned a Promise
|
|
1830
|
+
let userGenerator: AsyncGenerator<any, any, unknown>
|
|
1831
|
+
let initialHeaders: Record<string, string> | undefined
|
|
1832
|
+
|
|
1833
|
+
if (typeof handlerResult?.[Symbol.asyncIterator] === 'function') {
|
|
1834
|
+
// Plain async generator
|
|
1835
|
+
userGenerator = handlerResult
|
|
1836
|
+
} else if (typeof handlerResult?.then === 'function') {
|
|
1837
|
+
// Promise → await for { headers, stream }
|
|
1838
|
+
const resolved = await handlerResult
|
|
1839
|
+
if (!resolved || typeof resolved !== 'object' || !('stream' in resolved)) {
|
|
1840
|
+
throw new ProcedureError(name, `CreateHttpStream handler returned a Promise that did not resolve to { headers, stream }. Got: ${JSON.stringify(resolved)}`, undefined, definitionInfo)
|
|
1841
|
+
}
|
|
1842
|
+
initialHeaders = resolved.headers
|
|
1843
|
+
userGenerator = resolved.stream
|
|
1844
|
+
} else {
|
|
1845
|
+
throw new ProcedureError(name, `CreateHttpStream handler must return an AsyncGenerator or Promise<{ headers, stream }>.`, undefined, definitionInfo)
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Surface initial headers via a synthetic first yield with a known sentinel — but
|
|
1849
|
+
// that's not transport-clean. Instead we attach headers to the wrappedHandler's
|
|
1850
|
+
// generator object so the Hono builder can read them. AsyncGenerators don't
|
|
1851
|
+
// support arbitrary properties cleanly through the typed surface, so we expose
|
|
1852
|
+
// them via the registration's `getInitialHeaders` accessor rather than the
|
|
1853
|
+
// generator itself.
|
|
1854
|
+
;(wrappedHandler as any)._lastInitialHeaders = initialHeaders
|
|
1855
|
+
|
|
1856
|
+
const userIterator = userGenerator[Symbol.asyncIterator]()
|
|
1857
|
+
try {
|
|
1858
|
+
let userIterResult = await userIterator.next()
|
|
1859
|
+
while (!userIterResult.done) {
|
|
1860
|
+
const value = userIterResult.value
|
|
1861
|
+
if (validateYields && validations.yield) {
|
|
1862
|
+
const { errors } = validations.yield(value)
|
|
1863
|
+
if (errors) throw new ProcedureYieldValidationError(name, `Yield validation error for ${name}`, errors, definitionInfo)
|
|
1864
|
+
}
|
|
1865
|
+
yield value
|
|
1866
|
+
userIterResult = await userIterator.next()
|
|
1867
|
+
}
|
|
1868
|
+
return userIterResult.value
|
|
1869
|
+
} catch (error: any) {
|
|
1870
|
+
if (definitionInfo.definedAt && error && typeof error.stack === 'string') {
|
|
1871
|
+
const { file, line, column } = definitionInfo.definedAt
|
|
1872
|
+
error.stack = `${error.stack}\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
|
|
1873
|
+
}
|
|
1874
|
+
throw error
|
|
1875
|
+
} finally {
|
|
1876
|
+
try { await userIterator.return?.(undefined) } catch { /* swallow */ }
|
|
1877
|
+
abortController.abort('stream-completed')
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
**Note on the initial-headers handoff:** stashing on `wrappedHandler._lastInitialHeaders` is fragile under concurrent calls. Better: make the wrappedHandler return a custom iterable that exposes `.initialHeaders` when iterated. Or — simpler — yield a sentinel first value `{ __initialHeaders: {...} }` that the Hono builder strips. Choose the sentinel approach for thread safety:
|
|
1883
|
+
|
|
1884
|
+
Replace the marked block with:
|
|
1885
|
+
|
|
1886
|
+
```ts
|
|
1887
|
+
// Yield headers sentinel as the first value if async-preamble was used.
|
|
1888
|
+
// The Hono builder strips this sentinel and applies headers before opening SSE.
|
|
1889
|
+
if (initialHeaders) {
|
|
1890
|
+
yield { __initialHeaders: initialHeaders } as any
|
|
1891
|
+
}
|
|
1892
|
+
```
|
|
1893
|
+
|
|
1894
|
+
And document the sentinel shape in `src/types.ts`:
|
|
1895
|
+
|
|
1896
|
+
```ts
|
|
1897
|
+
export const HTTP_STREAM_HEADERS_SENTINEL = '__initialHeaders' as const
|
|
1898
|
+
```
|
|
1899
|
+
|
|
1900
|
+
- [ ] **Step 3: Write failing tests for the async-preamble shape**
|
|
1901
|
+
|
|
1902
|
+
```ts
|
|
1903
|
+
describe('CreateHttpStream async-preamble shape (res.headers)', () => {
|
|
1904
|
+
it('handler returning Promise<{ headers, stream }> yields headers sentinel first', async () => {
|
|
1905
|
+
const procs = Procedures()
|
|
1906
|
+
const { Tail } = procs.CreateHttpStream('Tail', {
|
|
1907
|
+
path: '/streams/logs', method: 'get',
|
|
1908
|
+
schema: {
|
|
1909
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
1910
|
+
yield: Type.Object({ line: Type.String() }),
|
|
1911
|
+
res: { headers: Type.Object({ 'x-stream-id': Type.String() }) },
|
|
1912
|
+
},
|
|
1913
|
+
}, async (_, { query }) => ({
|
|
1914
|
+
headers: { 'x-stream-id': `id-${query.source}` },
|
|
1915
|
+
stream: (async function* () { yield { line: 'a' } })(),
|
|
1916
|
+
}))
|
|
1917
|
+
|
|
1918
|
+
const gen = Tail({} as any, { query: { source: 'app' } })
|
|
1919
|
+
const first = await gen.next()
|
|
1920
|
+
expect(first.value).toEqual({ __initialHeaders: { 'x-stream-id': 'id-app' } })
|
|
1921
|
+
const second = await gen.next()
|
|
1922
|
+
expect(second.value).toEqual({ line: 'a' })
|
|
1923
|
+
})
|
|
1924
|
+
|
|
1925
|
+
it('throws when handler returns malformed shape', async () => {
|
|
1926
|
+
const procs = Procedures()
|
|
1927
|
+
const { Bad } = procs.CreateHttpStream('Bad', {
|
|
1928
|
+
path: '/streams', method: 'get',
|
|
1929
|
+
schema: {
|
|
1930
|
+
yield: Type.Number(),
|
|
1931
|
+
res: { headers: Type.Object({}) },
|
|
1932
|
+
},
|
|
1933
|
+
}, (async () => 'not-a-shape') as any)
|
|
1934
|
+
|
|
1935
|
+
const gen = Bad({} as any, undefined)
|
|
1936
|
+
await expect(gen.next()).rejects.toThrow(/did not resolve to/)
|
|
1937
|
+
})
|
|
1938
|
+
})
|
|
1939
|
+
```
|
|
1940
|
+
|
|
1941
|
+
- [ ] **Step 4: Run tests**
|
|
1942
|
+
|
|
1943
|
+
```bash
|
|
1944
|
+
npx vitest run src/create-http-stream.test.ts
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
Expected: PASS.
|
|
1948
|
+
|
|
1949
|
+
- [ ] **Step 5: Commit**
|
|
1950
|
+
|
|
1951
|
+
```bash
|
|
1952
|
+
git add src/create-http-stream.ts src/types.ts src/create-http-stream.test.ts
|
|
1953
|
+
git commit -m "feat(core): add CreateHttpStream async-preamble shape with headers sentinel"
|
|
1954
|
+
```
|
|
1955
|
+
|
|
1956
|
+
---
|
|
1957
|
+
|
|
1958
|
+
## Phase 6 — Hono builder unification
|
|
1959
|
+
|
|
1960
|
+
### Task 19: Strip `APIConfig` generic from `HonoAPIFactoryItem`
|
|
1961
|
+
|
|
1962
|
+
**Files:**
|
|
1963
|
+
- Modify: `src/implementations/http/hono-api/types.ts`
|
|
1964
|
+
|
|
1965
|
+
- [ ] **Step 1: Replace `HonoAPIFactoryItem` definition**
|
|
1966
|
+
|
|
1967
|
+
```ts
|
|
1968
|
+
import { ExtractContext, APIHttpRouteDoc } from '../../types.js'
|
|
1969
|
+
import { Procedures } from '../../../index.js'
|
|
1970
|
+
import { THttpProcedureRegistration, THttpStreamProcedureRegistration } from '../../../types.js'
|
|
1971
|
+
import { Context } from 'hono'
|
|
1972
|
+
|
|
1973
|
+
export type HonoAPIFactoryItem<TFactory = ReturnType<typeof Procedures<any>>> = {
|
|
1974
|
+
factory: TFactory
|
|
1975
|
+
factoryContext: ExtractContext<TFactory> | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
1976
|
+
extendProcedureDoc?: (params: {
|
|
1977
|
+
base: APIHttpRouteDoc
|
|
1978
|
+
procedure: THttpProcedureRegistration<any> | THttpStreamProcedureRegistration<any>
|
|
1979
|
+
}) => Record<string, any>
|
|
1980
|
+
}
|
|
1981
|
+
```
|
|
1982
|
+
|
|
1983
|
+
- [ ] **Step 2: Build to surface compile errors**
|
|
1984
|
+
|
|
1985
|
+
```bash
|
|
1986
|
+
npm run build
|
|
1987
|
+
```
|
|
1988
|
+
|
|
1989
|
+
Expected: errors in `src/implementations/http/hono-api/index.ts` because the factory is no longer generic over `APIConfig`. Those will be fixed in Task 20.
|
|
1990
|
+
|
|
1991
|
+
- [ ] **Step 3: Commit (without build green — Task 20 finishes the chain)**
|
|
1992
|
+
|
|
1993
|
+
```bash
|
|
1994
|
+
git add src/implementations/http/hono-api/types.ts
|
|
1995
|
+
git commit -m "refactor(hono-api): drop APIConfig generic from HonoAPIFactoryItem"
|
|
1996
|
+
```
|
|
1997
|
+
|
|
1998
|
+
---
|
|
1999
|
+
|
|
2000
|
+
### Task 20: Update `HonoAPIAppBuilder` to read `CreateHttp` registrations and apply response headers
|
|
2001
|
+
|
|
2002
|
+
**Files:**
|
|
2003
|
+
- Modify: `src/implementations/http/hono-api/index.ts`
|
|
2004
|
+
|
|
2005
|
+
- [ ] **Step 1: Update `register` signature**
|
|
2006
|
+
|
|
2007
|
+
```ts
|
|
2008
|
+
register<TFactory extends ProceduresFactory>(
|
|
2009
|
+
factory: TFactory,
|
|
2010
|
+
factoryContext: ExtractContext<TFactory> | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
|
|
2011
|
+
extendProcedureDoc?: (params: {
|
|
2012
|
+
base: APIHttpRouteDoc
|
|
2013
|
+
procedure: THttpProcedureRegistration<any> | THttpStreamProcedureRegistration<any>
|
|
2014
|
+
}) => Record<string, any>,
|
|
2015
|
+
): this
|
|
2016
|
+
```
|
|
2017
|
+
|
|
2018
|
+
- [ ] **Step 2: In `build()`, filter procedures by `kind === 'http'` and capture skipped procedures**
|
|
2019
|
+
|
|
2020
|
+
```ts
|
|
2021
|
+
build(): Hono {
|
|
2022
|
+
const queryParser = this.config?.queryParser ?? parseQueryNative
|
|
2023
|
+
const skipped: { name: string; reason: string }[] = []
|
|
2024
|
+
|
|
2025
|
+
this.factories.forEach(({ factory, factoryContext, extendProcedureDoc }) => {
|
|
2026
|
+
factory.getProcedures().forEach((procedure: any) => {
|
|
2027
|
+
if (procedure.kind === 'http') {
|
|
2028
|
+
// existing http registration flow
|
|
2029
|
+
this.registerHttpRoute(procedure, factoryContext, extendProcedureDoc, queryParser)
|
|
2030
|
+
} else if (procedure.kind === 'http-stream') {
|
|
2031
|
+
// Phase 6 Task 22 wires this up
|
|
2032
|
+
this.registerHttpStreamRoute(procedure, factoryContext, extendProcedureDoc, queryParser)
|
|
2033
|
+
} else {
|
|
2034
|
+
skipped.push({
|
|
2035
|
+
name: procedure.name,
|
|
2036
|
+
reason: `Procedure has kind "${procedure.kind}"; HonoAPIAppBuilder serves only "http" and "http-stream".`,
|
|
2037
|
+
})
|
|
2038
|
+
}
|
|
2039
|
+
})
|
|
2040
|
+
})
|
|
2041
|
+
|
|
2042
|
+
this._skippedProcedures = skipped
|
|
2043
|
+
return this._app
|
|
2044
|
+
}
|
|
2045
|
+
```
|
|
2046
|
+
|
|
2047
|
+
Add the `_skippedProcedures` private field and a `get skippedProcedures()` accessor.
|
|
2048
|
+
|
|
2049
|
+
- [ ] **Step 3: Refactor existing route handler logic into `registerHttpRoute`**
|
|
2050
|
+
|
|
2051
|
+
Move the existing per-procedure body (path resolution, doc build, route handler, mount) into `private registerHttpRoute(procedure, factoryContext, extendProcedureDoc, queryParser)`. Adapt it to read `procedure.config` directly (no more `APIConfig` generic — fields are first-class).
|
|
2052
|
+
|
|
2053
|
+
- [ ] **Step 4: Update `extractInputParams` to read `req` instead of `input`**
|
|
2054
|
+
|
|
2055
|
+
The function body is unchanged in logic — just rename the variable from `inputSchema` to `reqSchema` and update the parameter type. Channel extraction (`pathParams`/`query`/`body`/`headers`) is identical to today.
|
|
2056
|
+
|
|
2057
|
+
- [ ] **Step 5: Apply response headers when handler returns `{ body, headers }`**
|
|
2058
|
+
|
|
2059
|
+
In the route handler:
|
|
2060
|
+
|
|
2061
|
+
```ts
|
|
2062
|
+
const result = await procedure.handler({ ...context, signal: c.req.raw.signal }, params)
|
|
2063
|
+
|
|
2064
|
+
if (this.config?.onSuccess) this.config.onSuccess(procedure, c)
|
|
2065
|
+
if (successStatus === 204) {
|
|
2066
|
+
// Apply headers if present
|
|
2067
|
+
if (result && typeof result === 'object' && 'headers' in result && result.headers) {
|
|
2068
|
+
for (const [k, v] of Object.entries(result.headers as Record<string, string>)) {
|
|
2069
|
+
c.header(k, v)
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return c.body(null, 204)
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Detect { body, headers } shape vs bare body
|
|
2076
|
+
let body: unknown = result
|
|
2077
|
+
let headers: Record<string, string> | undefined
|
|
2078
|
+
if (result && typeof result === 'object' && 'body' in result && 'headers' in result) {
|
|
2079
|
+
body = (result as any).body
|
|
2080
|
+
headers = (result as any).headers
|
|
2081
|
+
} else if (result && typeof result === 'object' && 'headers' in result && !('body' in result)) {
|
|
2082
|
+
// headers-only case
|
|
2083
|
+
for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
|
|
2084
|
+
c.header(k, v)
|
|
2085
|
+
}
|
|
2086
|
+
return c.body(null, successStatus as any)
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
if (headers) {
|
|
2090
|
+
for (const [k, v] of Object.entries(headers)) c.header(k, v)
|
|
2091
|
+
}
|
|
2092
|
+
return c.json(body, successStatus as any)
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
- [ ] **Step 6: Build to confirm compile errors are resolved**
|
|
2096
|
+
|
|
2097
|
+
```bash
|
|
2098
|
+
npm run build
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
Expected: green.
|
|
2102
|
+
|
|
2103
|
+
- [ ] **Step 7: Run hono-api tests (will likely fail — fixed in next task)**
|
|
2104
|
+
|
|
2105
|
+
```bash
|
|
2106
|
+
npx vitest run src/implementations/http/hono-api/
|
|
2107
|
+
```
|
|
2108
|
+
|
|
2109
|
+
Note any failing tests; they'll need updates (Task 23).
|
|
2110
|
+
|
|
2111
|
+
- [ ] **Step 8: Commit**
|
|
2112
|
+
|
|
2113
|
+
```bash
|
|
2114
|
+
git add src/implementations/http/hono-api/index.ts
|
|
2115
|
+
git commit -m "refactor(hono-api): read CreateHttp registrations directly; apply response headers"
|
|
2116
|
+
```
|
|
2117
|
+
|
|
2118
|
+
---
|
|
2119
|
+
|
|
2120
|
+
### Task 21: Add `'http-stream'` branch to `HonoAPIAppBuilder`
|
|
2121
|
+
|
|
2122
|
+
**Files:**
|
|
2123
|
+
- Modify: `src/implementations/http/hono-api/index.ts`
|
|
2124
|
+
|
|
2125
|
+
- [ ] **Step 1: Implement `registerHttpStreamRoute`**
|
|
2126
|
+
|
|
2127
|
+
```ts
|
|
2128
|
+
import { streamSSE } from 'hono/streaming'
|
|
2129
|
+
import { HTTP_STREAM_HEADERS_SENTINEL } from '../../../types.js'
|
|
2130
|
+
|
|
2131
|
+
private registerHttpStreamRoute(
|
|
2132
|
+
procedure: THttpStreamProcedureRegistration<any>,
|
|
2133
|
+
factoryContext: any,
|
|
2134
|
+
extendProcedureDoc: any,
|
|
2135
|
+
queryParser: QueryParser,
|
|
2136
|
+
) {
|
|
2137
|
+
const fullPath = this.resolveFullPath(procedure.config.path)
|
|
2138
|
+
const route = this.buildHttpStreamRouteDoc(procedure, fullPath, extendProcedureDoc)
|
|
2139
|
+
this._docs.push(route)
|
|
2140
|
+
|
|
2141
|
+
const method = procedure.config.method.toUpperCase()
|
|
2142
|
+
const reqSchema = procedure.config.schema?.req
|
|
2143
|
+
|
|
2144
|
+
this._app.on(method, fullPath, async (c: Context) => {
|
|
2145
|
+
try {
|
|
2146
|
+
const context = typeof factoryContext === 'function' ? await factoryContext(c) : factoryContext
|
|
2147
|
+
const params = reqSchema
|
|
2148
|
+
? await this.extractInputParams(c, procedure.config.method, reqSchema, queryParser)
|
|
2149
|
+
: undefined
|
|
2150
|
+
|
|
2151
|
+
// Pre-stream phase: open the generator, await first value (which may be sentinel)
|
|
2152
|
+
const gen = procedure.handler({ ...context, signal: c.req.raw.signal, isPrevalidated: false }, params)
|
|
2153
|
+
const first = await gen.next()
|
|
2154
|
+
|
|
2155
|
+
// If first value is the headers sentinel, apply headers and consume next
|
|
2156
|
+
let initialYield: any
|
|
2157
|
+
if (first.value && typeof first.value === 'object' && HTTP_STREAM_HEADERS_SENTINEL in first.value) {
|
|
2158
|
+
for (const [k, v] of Object.entries((first.value as any)[HTTP_STREAM_HEADERS_SENTINEL] as Record<string, string>)) {
|
|
2159
|
+
c.header(k, v)
|
|
2160
|
+
}
|
|
2161
|
+
initialYield = await gen.next()
|
|
2162
|
+
} else {
|
|
2163
|
+
initialYield = first
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// Open SSE response and stream remaining yields
|
|
2167
|
+
return streamSSE(c, async (stream) => {
|
|
2168
|
+
let it = initialYield
|
|
2169
|
+
while (!it.done) {
|
|
2170
|
+
await stream.writeSSE({ data: JSON.stringify(it.value) })
|
|
2171
|
+
it = await gen.next()
|
|
2172
|
+
}
|
|
2173
|
+
if (it.value !== undefined) {
|
|
2174
|
+
await stream.writeSSE({ event: 'return', data: JSON.stringify(it.value) })
|
|
2175
|
+
}
|
|
2176
|
+
})
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
// Pre-stream error path — same dispatch as registerHttpRoute
|
|
2179
|
+
return this.handleError(error, procedure, c)
|
|
2180
|
+
}
|
|
2181
|
+
})
|
|
2182
|
+
}
|
|
2183
|
+
```
|
|
2184
|
+
|
|
2185
|
+
Extract the error-dispatch block from `registerHttpRoute` into a shared `private handleError(error, procedure, c)` method so both branches use the same dispatch flow (taxonomy → onError → hard default + onRequestError observer).
|
|
2186
|
+
|
|
2187
|
+
- [ ] **Step 2: Add `buildHttpStreamRouteDoc` helper**
|
|
2188
|
+
|
|
2189
|
+
```ts
|
|
2190
|
+
private buildHttpStreamRouteDoc(
|
|
2191
|
+
procedure: THttpStreamProcedureRegistration<any>,
|
|
2192
|
+
fullPath: string,
|
|
2193
|
+
extendProcedureDoc?: HonoAPIFactoryItem['extendProcedureDoc'],
|
|
2194
|
+
): HttpStreamRouteDoc {
|
|
2195
|
+
const config = procedure.config
|
|
2196
|
+
const reqSchema = config.schema?.req
|
|
2197
|
+
const resSchema = config.schema?.res
|
|
2198
|
+
const jsonSchema: HttpStreamRouteDoc['jsonSchema'] = {}
|
|
2199
|
+
if (reqSchema) jsonSchema.req = reqSchema
|
|
2200
|
+
if (resSchema) jsonSchema.res = resSchema
|
|
2201
|
+
if (config.schema?.yield) jsonSchema.yield = config.schema.yield
|
|
2202
|
+
if (config.schema?.returnType) jsonSchema.returnType = config.schema.returnType
|
|
2203
|
+
|
|
2204
|
+
const base: HttpStreamRouteDoc = {
|
|
2205
|
+
kind: 'http-stream',
|
|
2206
|
+
name: procedure.name,
|
|
2207
|
+
scope: config.scope,
|
|
2208
|
+
path: config.path,
|
|
2209
|
+
method: config.method,
|
|
2210
|
+
fullPath,
|
|
2211
|
+
streamMode: 'sse',
|
|
2212
|
+
jsonSchema,
|
|
2213
|
+
}
|
|
2214
|
+
if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
|
|
2215
|
+
|
|
2216
|
+
let extendedDoc: object = {}
|
|
2217
|
+
if (extendProcedureDoc) extendedDoc = extendProcedureDoc({ base: base as any, procedure })
|
|
2218
|
+
return { ...extendedDoc, ...base }
|
|
2219
|
+
}
|
|
2220
|
+
```
|
|
2221
|
+
|
|
2222
|
+
(`HttpStreamRouteDoc` type is added in Phase 7 / Task 24. For now use `any` if needed and revisit.)
|
|
2223
|
+
|
|
2224
|
+
- [ ] **Step 3: Build**
|
|
2225
|
+
|
|
2226
|
+
```bash
|
|
2227
|
+
npm run build
|
|
2228
|
+
```
|
|
2229
|
+
|
|
2230
|
+
Expected: green (or near-green pending the type added in Phase 7).
|
|
2231
|
+
|
|
2232
|
+
- [ ] **Step 4: Commit**
|
|
2233
|
+
|
|
2234
|
+
```bash
|
|
2235
|
+
git add src/implementations/http/hono-api/index.ts
|
|
2236
|
+
git commit -m "feat(hono-api): add http-stream branch with sentinel-based headers handoff"
|
|
2237
|
+
```
|
|
2238
|
+
|
|
2239
|
+
---
|
|
2240
|
+
|
|
2241
|
+
### Task 22: Test `HonoAPIAppBuilder` for both `'http'` and `'http-stream'` kinds + response headers
|
|
2242
|
+
|
|
2243
|
+
**Files:**
|
|
2244
|
+
- Modify: `src/implementations/http/hono-api/index.test.ts`
|
|
2245
|
+
|
|
2246
|
+
- [ ] **Step 1: Update existing tests to use `CreateHttp` instead of `Create` + `APIConfig`**
|
|
2247
|
+
|
|
2248
|
+
Find tests that registered procedures via `Procedures<Ctx, APIConfig>().Create('Foo', { path, method, schema: { input } }, ...)`. Migrate them to `Procedures<Ctx>().CreateHttp('Foo', { path, method, schema: { req } }, ...)`.
|
|
2249
|
+
|
|
2250
|
+
- [ ] **Step 2: Add new tests for response headers**
|
|
2251
|
+
|
|
2252
|
+
```ts
|
|
2253
|
+
it('applies res.headers to the response', async () => {
|
|
2254
|
+
const procs = Procedures()
|
|
2255
|
+
procs.CreateHttp('GetUser', {
|
|
2256
|
+
path: '/users/:id', method: 'get',
|
|
2257
|
+
schema: {
|
|
2258
|
+
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
2259
|
+
res: {
|
|
2260
|
+
body: Type.Object({ id: Type.String() }),
|
|
2261
|
+
headers: Type.Object({ 'x-rate-limit': Type.String() }),
|
|
2262
|
+
},
|
|
2263
|
+
},
|
|
2264
|
+
}, async (_, { pathParams }) => ({
|
|
2265
|
+
body: { id: pathParams.id },
|
|
2266
|
+
headers: { 'x-rate-limit': '99' },
|
|
2267
|
+
}))
|
|
2268
|
+
|
|
2269
|
+
const app = new HonoAPIAppBuilder().register(procs, {}).build()
|
|
2270
|
+
const res = await app.request('/users/abc')
|
|
2271
|
+
expect(res.status).toBe(200)
|
|
2272
|
+
expect(res.headers.get('x-rate-limit')).toBe('99')
|
|
2273
|
+
expect(await res.json()).toEqual({ id: 'abc' })
|
|
2274
|
+
})
|
|
2275
|
+
|
|
2276
|
+
it('handles headers-only response (204)', async () => {
|
|
2277
|
+
const procs = Procedures()
|
|
2278
|
+
procs.CreateHttp('Touch', {
|
|
2279
|
+
path: '/touch/:id', method: 'delete',
|
|
2280
|
+
schema: {
|
|
2281
|
+
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
2282
|
+
res: { headers: Type.Object({ 'x-touched-at': Type.String() }) },
|
|
2283
|
+
},
|
|
2284
|
+
}, async () => ({ headers: { 'x-touched-at': '2026-05-08' } }))
|
|
2285
|
+
|
|
2286
|
+
const app = new HonoAPIAppBuilder().register(procs, {}).build()
|
|
2287
|
+
const res = await app.request('/touch/abc', { method: 'DELETE' })
|
|
2288
|
+
expect(res.status).toBe(204)
|
|
2289
|
+
expect(res.headers.get('x-touched-at')).toBe('2026-05-08')
|
|
2290
|
+
})
|
|
2291
|
+
```
|
|
2292
|
+
|
|
2293
|
+
- [ ] **Step 3: Add tests for the http-stream branch**
|
|
2294
|
+
|
|
2295
|
+
```ts
|
|
2296
|
+
it('serves CreateHttpStream as SSE', async () => {
|
|
2297
|
+
const procs = Procedures()
|
|
2298
|
+
procs.CreateHttpStream('TailLogs', {
|
|
2299
|
+
path: '/streams/logs', method: 'get',
|
|
2300
|
+
schema: {
|
|
2301
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
2302
|
+
yield: Type.Object({ line: Type.String() }),
|
|
2303
|
+
returnType: Type.Object({ totalLines: Type.Number() }),
|
|
2304
|
+
},
|
|
2305
|
+
}, async function* (_, { query }) {
|
|
2306
|
+
yield { line: `from-${query.source}-1` }
|
|
2307
|
+
yield { line: `from-${query.source}-2` }
|
|
2308
|
+
return { totalLines: 2 }
|
|
2309
|
+
})
|
|
2310
|
+
|
|
2311
|
+
const app = new HonoAPIAppBuilder().register(procs, {}).build()
|
|
2312
|
+
const res = await app.request('/streams/logs?source=app')
|
|
2313
|
+
const text = await res.text()
|
|
2314
|
+
expect(text).toContain('data: {"line":"from-app-1"}')
|
|
2315
|
+
expect(text).toContain('event: return\ndata: {"totalLines":2}')
|
|
2316
|
+
})
|
|
2317
|
+
|
|
2318
|
+
it('applies http-stream initial response headers from async preamble', async () => {
|
|
2319
|
+
const procs = Procedures()
|
|
2320
|
+
procs.CreateHttpStream('TailLogs', {
|
|
2321
|
+
path: '/streams/logs', method: 'get',
|
|
2322
|
+
schema: {
|
|
2323
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
2324
|
+
yield: Type.Object({ line: Type.String() }),
|
|
2325
|
+
res: { headers: Type.Object({ 'x-stream-id': Type.String() }) },
|
|
2326
|
+
},
|
|
2327
|
+
}, async (_, { query }) => ({
|
|
2328
|
+
headers: { 'x-stream-id': `sid-${query.source}` },
|
|
2329
|
+
stream: (async function* () { yield { line: 'first' } })(),
|
|
2330
|
+
}))
|
|
2331
|
+
|
|
2332
|
+
const app = new HonoAPIAppBuilder().register(procs, {}).build()
|
|
2333
|
+
const res = await app.request('/streams/logs?source=app')
|
|
2334
|
+
expect(res.headers.get('x-stream-id')).toBe('sid-app')
|
|
2335
|
+
expect(await res.text()).toContain('data: {"line":"first"}')
|
|
2336
|
+
})
|
|
2337
|
+
```
|
|
2338
|
+
|
|
2339
|
+
- [ ] **Step 4: Run tests**
|
|
2340
|
+
|
|
2341
|
+
```bash
|
|
2342
|
+
npx vitest run src/implementations/http/hono-api/
|
|
2343
|
+
```
|
|
2344
|
+
|
|
2345
|
+
Expected: PASS.
|
|
2346
|
+
|
|
2347
|
+
- [ ] **Step 5: Commit**
|
|
2348
|
+
|
|
2349
|
+
```bash
|
|
2350
|
+
git add src/implementations/http/hono-api/index.test.ts
|
|
2351
|
+
git commit -m "test(hono-api): cover CreateHttp response headers and http-stream branch"
|
|
2352
|
+
```
|
|
2353
|
+
|
|
2354
|
+
---
|
|
2355
|
+
|
|
2356
|
+
### Task 23: Add `kind`-based filtering to `HonoRPCAppBuilder`, `HonoStreamAppBuilder`
|
|
2357
|
+
|
|
2358
|
+
**Files:**
|
|
2359
|
+
- Modify: `src/implementations/http/hono-rpc/index.ts`
|
|
2360
|
+
- Modify: `src/implementations/http/hono-stream/index.ts`
|
|
2361
|
+
|
|
2362
|
+
- [ ] **Step 1: For each of the two builders, change `getProcedures().forEach` filtering**
|
|
2363
|
+
|
|
2364
|
+
In `hono-rpc/index.ts` `build()`:
|
|
2365
|
+
|
|
2366
|
+
```ts
|
|
2367
|
+
factory.getProcedures().forEach((procedure: any) => {
|
|
2368
|
+
if (procedure.kind !== 'rpc') {
|
|
2369
|
+
skipped.push({
|
|
2370
|
+
name: procedure.name,
|
|
2371
|
+
reason: `Procedure has kind "${procedure.kind}"; HonoRPCAppBuilder serves only "rpc".`,
|
|
2372
|
+
})
|
|
2373
|
+
return
|
|
2374
|
+
}
|
|
2375
|
+
// existing rpc registration flow
|
|
2376
|
+
})
|
|
2377
|
+
```
|
|
2378
|
+
|
|
2379
|
+
- [ ] **Step 2: Same pattern for `hono-stream` (kind === 'rpc-stream')**
|
|
2380
|
+
|
|
2381
|
+
- [ ] **Step 3: Add `_skippedProcedures` and accessor on each builder**
|
|
2382
|
+
|
|
2383
|
+
Mirror the `HonoAPIAppBuilder._skippedProcedures` pattern from Task 20.
|
|
2384
|
+
|
|
2385
|
+
- [ ] **Step 4: Update each builder's tests to assert skipped procedures**
|
|
2386
|
+
|
|
2387
|
+
For each test file, add:
|
|
2388
|
+
|
|
2389
|
+
```ts
|
|
2390
|
+
it('records http-kind procedures in skippedProcedures', () => {
|
|
2391
|
+
const procs = Procedures()
|
|
2392
|
+
procs.CreateHttp('Skipped', {
|
|
2393
|
+
path: '/x', method: 'get',
|
|
2394
|
+
schema: { req: { query: Type.Object({}) } },
|
|
2395
|
+
}, async () => undefined)
|
|
2396
|
+
|
|
2397
|
+
const builder = new HonoRPCAppBuilder().register(procs, {})
|
|
2398
|
+
builder.build()
|
|
2399
|
+
expect(builder.skippedProcedures).toContainEqual({
|
|
2400
|
+
name: 'Skipped',
|
|
2401
|
+
reason: expect.stringContaining('HonoRPCAppBuilder serves only "rpc"'),
|
|
2402
|
+
})
|
|
2403
|
+
})
|
|
2404
|
+
```
|
|
2405
|
+
|
|
2406
|
+
- [ ] **Step 5: Run all builder tests**
|
|
2407
|
+
|
|
2408
|
+
```bash
|
|
2409
|
+
npx vitest run src/implementations/http/
|
|
2410
|
+
```
|
|
2411
|
+
|
|
2412
|
+
Expected: PASS.
|
|
2413
|
+
|
|
2414
|
+
- [ ] **Step 6: Commit**
|
|
2415
|
+
|
|
2416
|
+
```bash
|
|
2417
|
+
git add src/implementations/http/hono-rpc/ src/implementations/http/hono-stream/
|
|
2418
|
+
git commit -m "refactor(builders): kind-based filtering with skippedProcedures across rpc/stream builders"
|
|
2419
|
+
```
|
|
2420
|
+
|
|
2421
|
+
---
|
|
2422
|
+
|
|
2423
|
+
### Task 24: Update Astro adapter for kind-aware updates
|
|
2424
|
+
|
|
2425
|
+
**Files:**
|
|
2426
|
+
- Modify: `src/implementations/http/astro/*`
|
|
2427
|
+
- Modify: `src/implementations/http/astro/*.test.ts`
|
|
2428
|
+
|
|
2429
|
+
- [ ] **Step 1: Inspect the Astro adapter to identify how it consumes procedures**
|
|
2430
|
+
|
|
2431
|
+
```bash
|
|
2432
|
+
ls src/implementations/http/astro/
|
|
2433
|
+
cat src/implementations/http/astro/*.ts | head -200
|
|
2434
|
+
```
|
|
2435
|
+
|
|
2436
|
+
- [ ] **Step 2: Apply the same pattern as `HonoAPIAppBuilder`**
|
|
2437
|
+
|
|
2438
|
+
Filter by `kind === 'http'` (and `'http-stream'` if Astro supports streaming). Stash skipped procedures. Read `path` / `method` / `req` / `res` from `CreateHttp` registrations directly. Apply response headers from handler return.
|
|
2439
|
+
|
|
2440
|
+
- [ ] **Step 3: Update Astro tests**
|
|
2441
|
+
|
|
2442
|
+
Migrate any tests using `Create` + `schema.input` to `CreateHttp` + `schema.req`.
|
|
2443
|
+
|
|
2444
|
+
- [ ] **Step 4: Run Astro tests**
|
|
2445
|
+
|
|
2446
|
+
```bash
|
|
2447
|
+
npx vitest run src/implementations/http/astro/
|
|
2448
|
+
```
|
|
2449
|
+
|
|
2450
|
+
Expected: PASS.
|
|
2451
|
+
|
|
2452
|
+
- [ ] **Step 5: Commit**
|
|
2453
|
+
|
|
2454
|
+
```bash
|
|
2455
|
+
git add src/implementations/http/astro/
|
|
2456
|
+
git commit -m "refactor(astro): kind-aware procedure filtering and response headers"
|
|
2457
|
+
```
|
|
2458
|
+
|
|
2459
|
+
---
|
|
2460
|
+
|
|
2461
|
+
## Phase 7 — Doc envelope updates
|
|
2462
|
+
|
|
2463
|
+
### Task 25: Restructure `APIHttpRouteDoc` and add `HttpStreamRouteDoc`
|
|
2464
|
+
|
|
2465
|
+
**Files:**
|
|
2466
|
+
- Modify: `src/implementations/types.ts`
|
|
2467
|
+
|
|
2468
|
+
- [ ] **Step 1: Replace `APIHttpRouteDoc` with regrouped shape**
|
|
2469
|
+
|
|
2470
|
+
```ts
|
|
2471
|
+
export interface APIHttpRouteDoc {
|
|
2472
|
+
kind: 'api'
|
|
2473
|
+
name: string
|
|
2474
|
+
scope?: string
|
|
2475
|
+
path: string
|
|
2476
|
+
method: HttpMethod
|
|
2477
|
+
fullPath: string
|
|
2478
|
+
successStatus?: number
|
|
2479
|
+
errors?: string[]
|
|
2480
|
+
jsonSchema: {
|
|
2481
|
+
req?: { pathParams?: Record<string, unknown>; query?: Record<string, unknown>; body?: Record<string, unknown>; headers?: Record<string, unknown> }
|
|
2482
|
+
res?: { body?: Record<string, unknown>; headers?: Record<string, unknown> }
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
```
|
|
2486
|
+
|
|
2487
|
+
- [ ] **Step 2: Add `HttpStreamRouteDoc`**
|
|
2488
|
+
|
|
2489
|
+
```ts
|
|
2490
|
+
export interface HttpStreamRouteDoc {
|
|
2491
|
+
kind: 'http-stream'
|
|
2492
|
+
name: string
|
|
2493
|
+
scope?: string
|
|
2494
|
+
path: string
|
|
2495
|
+
method: HttpMethod
|
|
2496
|
+
fullPath: string
|
|
2497
|
+
streamMode: StreamMode
|
|
2498
|
+
errors?: string[]
|
|
2499
|
+
jsonSchema: {
|
|
2500
|
+
req?: { pathParams?: Record<string, unknown>; query?: Record<string, unknown>; body?: Record<string, unknown>; headers?: Record<string, unknown> }
|
|
2501
|
+
res?: { headers?: Record<string, unknown> }
|
|
2502
|
+
yield?: Record<string, unknown>
|
|
2503
|
+
returnType?: Record<string, unknown>
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
```
|
|
2507
|
+
|
|
2508
|
+
- [ ] **Step 3: Extend `AnyHttpRouteDoc` union**
|
|
2509
|
+
|
|
2510
|
+
```ts
|
|
2511
|
+
export type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc | HttpStreamRouteDoc
|
|
2512
|
+
```
|
|
2513
|
+
|
|
2514
|
+
- [ ] **Step 4: Remove `APIInput` and `APIConfig` (or simplify)**
|
|
2515
|
+
|
|
2516
|
+
`APIConfig` is no longer needed as a `TExtendedConfig` shape since HTTP fields live on `CreateHttp` directly. But it may still be referenced from external types — search for usages first:
|
|
2517
|
+
|
|
2518
|
+
```bash
|
|
2519
|
+
grep -rn "APIConfig\|APIInput" src/ --include="*.ts"
|
|
2520
|
+
```
|
|
2521
|
+
|
|
2522
|
+
If any internal code still references `APIConfig`, either inline its fields or leave the type as a deprecation-marked alias. Prefer removal — the migration error in Phase 2 catches v7 callers.
|
|
2523
|
+
|
|
2524
|
+
- [ ] **Step 5: Build**
|
|
2525
|
+
|
|
2526
|
+
```bash
|
|
2527
|
+
npm run build
|
|
2528
|
+
```
|
|
2529
|
+
|
|
2530
|
+
Expected: surfaces compile errors in `hono-api/index.ts` (the `buildApiHttpRouteDoc` and `buildHttpStreamRouteDoc` need to populate the new shape) and in codegen files (Phase 8 work). Fix the hono-api build errors now:
|
|
2531
|
+
|
|
2532
|
+
In `buildApiHttpRouteDoc` (Task 20):
|
|
2533
|
+
|
|
2534
|
+
```ts
|
|
2535
|
+
const reqSchema = procedure.config.schema?.req
|
|
2536
|
+
const resSchema = procedure.config.schema?.res
|
|
2537
|
+
const jsonSchema: APIHttpRouteDoc['jsonSchema'] = {}
|
|
2538
|
+
if (reqSchema) jsonSchema.req = reqSchema as any
|
|
2539
|
+
if (resSchema) jsonSchema.res = resSchema as any
|
|
2540
|
+
```
|
|
2541
|
+
|
|
2542
|
+
- [ ] **Step 6: Run hono-api tests**
|
|
2543
|
+
|
|
2544
|
+
```bash
|
|
2545
|
+
npx vitest run src/implementations/http/hono-api/
|
|
2546
|
+
```
|
|
2547
|
+
|
|
2548
|
+
Expected: PASS.
|
|
2549
|
+
|
|
2550
|
+
- [ ] **Step 7: Commit**
|
|
2551
|
+
|
|
2552
|
+
```bash
|
|
2553
|
+
git add src/implementations/types.ts src/implementations/http/hono-api/index.ts
|
|
2554
|
+
git commit -m "feat(envelope): regroup APIHttpRouteDoc under req/res; add HttpStreamRouteDoc"
|
|
2555
|
+
```
|
|
2556
|
+
|
|
2557
|
+
---
|
|
2558
|
+
|
|
2559
|
+
### Task 26: Update `DocRegistry` for `'http-stream'` kind
|
|
2560
|
+
|
|
2561
|
+
**Files:**
|
|
2562
|
+
- Modify: `src/implementations/http/doc-registry.ts`
|
|
2563
|
+
- Modify: `src/implementations/http/doc-registry.test.ts`
|
|
2564
|
+
|
|
2565
|
+
- [ ] **Step 1: Search for kind-discriminating logic**
|
|
2566
|
+
|
|
2567
|
+
```bash
|
|
2568
|
+
grep -n "kind\|RPCHttpRouteDoc\|APIHttpRouteDoc\|StreamHttpRouteDoc" src/implementations/http/doc-registry.ts
|
|
2569
|
+
```
|
|
2570
|
+
|
|
2571
|
+
- [ ] **Step 2: Add `'http-stream'` to any switch/filter logic**
|
|
2572
|
+
|
|
2573
|
+
If `doc-registry.ts` has explicit kind-handling, add the new case. If it just iterates `AnyHttpRouteDoc[]`, the type widening in Task 25 is sufficient.
|
|
2574
|
+
|
|
2575
|
+
- [ ] **Step 3: Add a test that aggregates docs from multiple builders including http-stream**
|
|
2576
|
+
|
|
2577
|
+
```ts
|
|
2578
|
+
it('aggregates http-stream routes from HonoAPIAppBuilder', () => {
|
|
2579
|
+
const procs = Procedures()
|
|
2580
|
+
procs.CreateHttpStream('Tail', {
|
|
2581
|
+
path: '/streams/logs', method: 'get',
|
|
2582
|
+
schema: { req: { query: Type.Object({ source: Type.String() }) }, yield: Type.Object({ line: Type.String() }) },
|
|
2583
|
+
}, async function* () {})
|
|
2584
|
+
|
|
2585
|
+
const builder = new HonoAPIAppBuilder().register(procs, {})
|
|
2586
|
+
builder.build()
|
|
2587
|
+
|
|
2588
|
+
const registry = DocRegistry.from(builder)
|
|
2589
|
+
const envelope = registry.toJSON()
|
|
2590
|
+
const route = envelope.routes.find((r) => r.name === 'Tail')
|
|
2591
|
+
expect(route?.kind).toBe('http-stream')
|
|
2592
|
+
})
|
|
2593
|
+
```
|
|
2594
|
+
|
|
2595
|
+
- [ ] **Step 4: Run doc-registry tests**
|
|
2596
|
+
|
|
2597
|
+
```bash
|
|
2598
|
+
npx vitest run src/implementations/http/doc-registry.test.ts
|
|
2599
|
+
```
|
|
2600
|
+
|
|
2601
|
+
Expected: PASS.
|
|
2602
|
+
|
|
2603
|
+
- [ ] **Step 5: Commit**
|
|
2604
|
+
|
|
2605
|
+
```bash
|
|
2606
|
+
git add src/implementations/http/doc-registry.ts src/implementations/http/doc-registry.test.ts
|
|
2607
|
+
git commit -m "feat(doc-registry): aggregate http-stream routes alongside http"
|
|
2608
|
+
```
|
|
2609
|
+
|
|
2610
|
+
---
|
|
2611
|
+
|
|
2612
|
+
## Phase 8 — Codegen TS target
|
|
2613
|
+
|
|
2614
|
+
### Task 27: Update `group-routes.ts` and `route-slots.ts` for new kind and req/res grouping
|
|
2615
|
+
|
|
2616
|
+
**Files:**
|
|
2617
|
+
- Modify: `src/codegen/group-routes.ts`
|
|
2618
|
+
- Modify: `src/codegen/targets/_shared/route-slots.ts`
|
|
2619
|
+
|
|
2620
|
+
- [ ] **Step 1: Read current `route-slots.ts`**
|
|
2621
|
+
|
|
2622
|
+
```bash
|
|
2623
|
+
cat src/codegen/targets/_shared/route-slots.ts
|
|
2624
|
+
```
|
|
2625
|
+
|
|
2626
|
+
- [ ] **Step 2: Add `responseHeaders` slot to `RouteSlot` definition**
|
|
2627
|
+
|
|
2628
|
+
```ts
|
|
2629
|
+
export type RouteSlot = {
|
|
2630
|
+
pathParams?: Record<string, unknown>
|
|
2631
|
+
query?: Record<string, unknown>
|
|
2632
|
+
body?: Record<string, unknown>
|
|
2633
|
+
headers?: Record<string, unknown> // request headers
|
|
2634
|
+
response?: Record<string, unknown> // response body (for back-compat / unary)
|
|
2635
|
+
responseHeaders?: Record<string, unknown> // new
|
|
2636
|
+
yield?: Record<string, unknown>
|
|
2637
|
+
returnType?: Record<string, unknown>
|
|
2638
|
+
}
|
|
2639
|
+
```
|
|
2640
|
+
|
|
2641
|
+
- [ ] **Step 3: Update `extractRouteSlots` to read from new envelope shape**
|
|
2642
|
+
|
|
2643
|
+
```ts
|
|
2644
|
+
export function extractRouteSlots(route: AnyHttpRouteDoc): RouteSlot {
|
|
2645
|
+
const slots: RouteSlot = {}
|
|
2646
|
+
if (route.kind === 'api' || route.kind === 'http-stream') {
|
|
2647
|
+
const req = route.jsonSchema.req
|
|
2648
|
+
if (req?.pathParams) slots.pathParams = req.pathParams
|
|
2649
|
+
if (req?.query) slots.query = req.query
|
|
2650
|
+
if (req?.body) slots.body = req.body
|
|
2651
|
+
if (req?.headers) slots.headers = req.headers
|
|
2652
|
+
const res = route.jsonSchema.res
|
|
2653
|
+
if (res?.body) slots.response = res.body
|
|
2654
|
+
if (res?.headers) slots.responseHeaders = res.headers
|
|
2655
|
+
}
|
|
2656
|
+
if (route.kind === 'http-stream') {
|
|
2657
|
+
if (route.jsonSchema.yield) slots.yield = route.jsonSchema.yield
|
|
2658
|
+
if (route.jsonSchema.returnType) slots.returnType = route.jsonSchema.returnType
|
|
2659
|
+
}
|
|
2660
|
+
// ... existing rpc and stream cases
|
|
2661
|
+
return slots
|
|
2662
|
+
}
|
|
2663
|
+
```
|
|
2664
|
+
|
|
2665
|
+
- [ ] **Step 4: Update `group-routes.ts` to recognize `'http-stream'` kind**
|
|
2666
|
+
|
|
2667
|
+
If `group-routes.ts` has any kind-specific switching, add the case. Streams group by scope same as `'http'`.
|
|
2668
|
+
|
|
2669
|
+
- [ ] **Step 5: Build**
|
|
2670
|
+
|
|
2671
|
+
```bash
|
|
2672
|
+
npm run build
|
|
2673
|
+
```
|
|
2674
|
+
|
|
2675
|
+
Expected: green (or surfaces emit-scope.ts errors fixed in Task 28).
|
|
2676
|
+
|
|
2677
|
+
- [ ] **Step 6: Commit**
|
|
2678
|
+
|
|
2679
|
+
```bash
|
|
2680
|
+
git add src/codegen/group-routes.ts src/codegen/targets/_shared/route-slots.ts
|
|
2681
|
+
git commit -m "feat(codegen): add responseHeaders slot and http-stream kind handling"
|
|
2682
|
+
```
|
|
2683
|
+
|
|
2684
|
+
---
|
|
2685
|
+
|
|
2686
|
+
### Task 28: Update `emit-scope.ts` for `Req` / `Response` namespace naming and conditional return
|
|
2687
|
+
|
|
2688
|
+
**Files:**
|
|
2689
|
+
- Modify: `src/codegen/emit-scope.ts`
|
|
2690
|
+
- Modify: `src/codegen/targets/ts/run.ts` (if emit logic is split there)
|
|
2691
|
+
|
|
2692
|
+
- [ ] **Step 1: Read existing emission for `'api'` routes**
|
|
2693
|
+
|
|
2694
|
+
```bash
|
|
2695
|
+
grep -n "emit\(API\|Api\|Stream\|Rpc\)Route\|Params\|Response" src/codegen/emit-scope.ts | head -40
|
|
2696
|
+
```
|
|
2697
|
+
|
|
2698
|
+
- [ ] **Step 2: Restructure type namespace emission**
|
|
2699
|
+
|
|
2700
|
+
Where today's code emits flat `<Route>Params`, `<Route>Body`, `<Route>Response`, change to nested under `Req` and `Response`:
|
|
2701
|
+
|
|
2702
|
+
Namespace mode (the default):
|
|
2703
|
+
```
|
|
2704
|
+
<Route>.Req.PathParams
|
|
2705
|
+
<Route>.Req.Query
|
|
2706
|
+
<Route>.Req.Body
|
|
2707
|
+
<Route>.Req.Headers
|
|
2708
|
+
<Route>.Response.Body
|
|
2709
|
+
<Route>.Response.Headers
|
|
2710
|
+
<Route>.Errors
|
|
2711
|
+
```
|
|
2712
|
+
|
|
2713
|
+
Flat mode:
|
|
2714
|
+
```
|
|
2715
|
+
<Route>ReqPathParams
|
|
2716
|
+
<Route>ReqQuery
|
|
2717
|
+
<Route>ReqBody
|
|
2718
|
+
<Route>ReqHeaders
|
|
2719
|
+
<Route>ResponseBody
|
|
2720
|
+
<Route>ResponseHeaders
|
|
2721
|
+
```
|
|
2722
|
+
|
|
2723
|
+
- [ ] **Step 3: Update generated callable signature with conditional return**
|
|
2724
|
+
|
|
2725
|
+
When `slots.responseHeaders` is present, emit:
|
|
2726
|
+
|
|
2727
|
+
```ts
|
|
2728
|
+
declare function getUser(req: GetUser.Req, opts?): Promise<{ body: GetUser.Response.Body; headers: GetUser.Response.Headers }>
|
|
2729
|
+
```
|
|
2730
|
+
|
|
2731
|
+
When only `slots.response` (body) is present:
|
|
2732
|
+
```ts
|
|
2733
|
+
declare function getUser(req: GetUser.Req, opts?): Promise<GetUser.Response.Body>
|
|
2734
|
+
```
|
|
2735
|
+
|
|
2736
|
+
When only `slots.responseHeaders`:
|
|
2737
|
+
```ts
|
|
2738
|
+
declare function getUser(req: GetUser.Req, opts?): Promise<{ headers: GetUser.Response.Headers }>
|
|
2739
|
+
```
|
|
2740
|
+
|
|
2741
|
+
When neither:
|
|
2742
|
+
```ts
|
|
2743
|
+
declare function getUser(req: GetUser.Req, opts?): Promise<void>
|
|
2744
|
+
```
|
|
2745
|
+
|
|
2746
|
+
- [ ] **Step 4: Add `'http-stream'` callable emission**
|
|
2747
|
+
|
|
2748
|
+
Mirror the existing `'stream'` emission but with `Req` namespace and optional `Response.Headers`. Returns `TypedStream<Yield, Return>` where `TypedStream` has optional `headers` field.
|
|
2749
|
+
|
|
2750
|
+
- [ ] **Step 5: Update `.safe()` sibling to widen with the conditional return**
|
|
2751
|
+
|
|
2752
|
+
If `slots.responseHeaders`, the typed result becomes `Result<{ body, headers }, ETyped>` not `Result<Body, ETyped>`.
|
|
2753
|
+
|
|
2754
|
+
- [ ] **Step 6: Regenerate the fixture envelope**
|
|
2755
|
+
|
|
2756
|
+
```bash
|
|
2757
|
+
# Find the test that constructs the fixture and run it manually to dump current output
|
|
2758
|
+
grep -rn "users-envelope.json" src/codegen/
|
|
2759
|
+
```
|
|
2760
|
+
|
|
2761
|
+
Update `src/codegen/__fixtures__/users-envelope.json` with the new `req`/`res` grouping and `'http-stream'` kind. Easiest path: write a small fixture-generation script that registers procs via `CreateHttp`/`CreateHttpStream`, builds the doc registry, and writes the JSON. Or hand-edit the JSON.
|
|
2762
|
+
|
|
2763
|
+
- [ ] **Step 7: Run codegen tests**
|
|
2764
|
+
|
|
2765
|
+
```bash
|
|
2766
|
+
npx vitest run src/codegen/
|
|
2767
|
+
```
|
|
2768
|
+
|
|
2769
|
+
Expected: snapshot mismatches at first. Inspect each snapshot diff carefully — confirm the new shape is correct, then update snapshots:
|
|
2770
|
+
|
|
2771
|
+
```bash
|
|
2772
|
+
npx vitest run src/codegen/ -u
|
|
2773
|
+
```
|
|
2774
|
+
|
|
2775
|
+
- [ ] **Step 8: Commit**
|
|
2776
|
+
|
|
2777
|
+
```bash
|
|
2778
|
+
git add src/codegen/emit-scope.ts src/codegen/__fixtures__/users-envelope.json src/codegen/**/*.snap*
|
|
2779
|
+
git commit -m "feat(codegen-ts): emit Req/Response namespaces and conditional return shapes"
|
|
2780
|
+
```
|
|
2781
|
+
|
|
2782
|
+
---
|
|
2783
|
+
|
|
2784
|
+
### Task 29: Update self-contained `_client.ts` and `_types.ts` emission
|
|
2785
|
+
|
|
2786
|
+
**Files:**
|
|
2787
|
+
- Modify: `src/codegen/targets/ts/run.ts` (or wherever `_client.ts` is bundled)
|
|
2788
|
+
- Modify: `src/client/*.ts` source files (which get bundled)
|
|
2789
|
+
|
|
2790
|
+
- [ ] **Step 1: Find where `_client.ts` is assembled**
|
|
2791
|
+
|
|
2792
|
+
```bash
|
|
2793
|
+
grep -rn "_client.ts\|selfContained" src/codegen/
|
|
2794
|
+
```
|
|
2795
|
+
|
|
2796
|
+
- [ ] **Step 2: Ensure new client runtime files (added in Phase 10) are bundled**
|
|
2797
|
+
|
|
2798
|
+
Phase 10 will modify `src/client/call.ts`, `src/client/stream.ts`, `src/client/types.ts`, `src/client/fetch-adapter.ts` to surface response headers. The self-contained bundler reads these files — verify the bundling logic picks up changes automatically (typically a `readFileSync` over the client source files).
|
|
2799
|
+
|
|
2800
|
+
- [ ] **Step 3: Run codegen tests for self-contained mode**
|
|
2801
|
+
|
|
2802
|
+
```bash
|
|
2803
|
+
npx vitest run src/codegen/ -t "self-contained"
|
|
2804
|
+
```
|
|
2805
|
+
|
|
2806
|
+
Expected: tests pass after Phase 10 lands. For now, may pass or have type-only mismatches deferred to Phase 10.
|
|
2807
|
+
|
|
2808
|
+
- [ ] **Step 4: Commit any changes**
|
|
2809
|
+
|
|
2810
|
+
```bash
|
|
2811
|
+
git add -p
|
|
2812
|
+
git commit -m "feat(codegen-ts): wire self-contained _client.ts to surface response headers"
|
|
2813
|
+
```
|
|
2814
|
+
|
|
2815
|
+
---
|
|
2816
|
+
|
|
2817
|
+
## Phase 9 — Codegen Kotlin & Swift targets
|
|
2818
|
+
|
|
2819
|
+
### Task 30: Kotlin target — emit `Response.Headers` data class
|
|
2820
|
+
|
|
2821
|
+
**Files:**
|
|
2822
|
+
- Modify: `src/codegen/targets/kotlin/run.ts`
|
|
2823
|
+
- Modify: `src/codegen/targets/kotlin/run.test.ts`
|
|
2824
|
+
|
|
2825
|
+
- [ ] **Step 1: Read the current Kotlin emit logic**
|
|
2826
|
+
|
|
2827
|
+
```bash
|
|
2828
|
+
cat src/codegen/targets/kotlin/run.ts | head -80
|
|
2829
|
+
```
|
|
2830
|
+
|
|
2831
|
+
Identify where `Response` data class is emitted for `'api'` routes.
|
|
2832
|
+
|
|
2833
|
+
- [ ] **Step 2: Update Kotlin emission for new envelope shape**
|
|
2834
|
+
|
|
2835
|
+
Read `req` channel schemas instead of flat `pathParams` / `query` / etc. Adapt the nested object emission accordingly:
|
|
2836
|
+
|
|
2837
|
+
```kotlin
|
|
2838
|
+
object Users {
|
|
2839
|
+
object GetUser {
|
|
2840
|
+
const val method = "GET"
|
|
2841
|
+
fun path(p: PathParams): String = "/users/${p.id}"
|
|
2842
|
+
@Serializable data class PathParams(val id: String)
|
|
2843
|
+
@Serializable data class Query(val include: String? = null)
|
|
2844
|
+
object Response {
|
|
2845
|
+
@Serializable data class Body(val id: String, val name: String)
|
|
2846
|
+
@Serializable data class Headers(@SerialName("x-rate-limit") val xRateLimit: String) // only when res.headers declared
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
```
|
|
2851
|
+
|
|
2852
|
+
- [ ] **Step 3: Add `'http-stream'` kind emission for Kotlin**
|
|
2853
|
+
|
|
2854
|
+
Streams in Kotlin emit the same `path`/`method`/types/`Req` constructs. Yield/return types are emitted as `Yield` and `Return` data classes. The runtime/HTTP layer is consumer-owned (per spec posture). No SSE adapter emission.
|
|
2855
|
+
|
|
2856
|
+
- [ ] **Step 4: Update Kotlin snapshot tests**
|
|
2857
|
+
|
|
2858
|
+
```bash
|
|
2859
|
+
npx vitest run src/codegen/targets/kotlin/run.test.ts -u
|
|
2860
|
+
```
|
|
2861
|
+
|
|
2862
|
+
Inspect each snapshot diff carefully before accepting.
|
|
2863
|
+
|
|
2864
|
+
- [ ] **Step 5: Commit**
|
|
2865
|
+
|
|
2866
|
+
```bash
|
|
2867
|
+
git add src/codegen/targets/kotlin/
|
|
2868
|
+
git commit -m "feat(codegen-kotlin): emit Req/Response namespaces and Response.Headers data class"
|
|
2869
|
+
```
|
|
2870
|
+
|
|
2871
|
+
---
|
|
2872
|
+
|
|
2873
|
+
### Task 31: Swift target — emit `Response.Headers` struct
|
|
2874
|
+
|
|
2875
|
+
**Files:**
|
|
2876
|
+
- Modify: `src/codegen/targets/swift/run.ts`
|
|
2877
|
+
- Modify: `src/codegen/targets/swift/run.test.ts`
|
|
2878
|
+
|
|
2879
|
+
- [ ] **Step 1: Mirror Task 30's pattern for Swift**
|
|
2880
|
+
|
|
2881
|
+
Emit nested caseless-enum namespaces with `Req.PathParams`, `Req.Query`, `Req.Body`, `Req.Headers`, `Response.Body`, `Response.Headers` structs. Conditional `Response.Headers` only when declared.
|
|
2882
|
+
|
|
2883
|
+
```swift
|
|
2884
|
+
enum Users {
|
|
2885
|
+
enum GetUser {
|
|
2886
|
+
static let method = "GET"
|
|
2887
|
+
static func path(_ p: PathParams) -> String { return "/users/\(p.id)" }
|
|
2888
|
+
struct PathParams: Codable { let id: String }
|
|
2889
|
+
struct Query: Codable { let include: String? }
|
|
2890
|
+
enum Response {
|
|
2891
|
+
struct Body: Codable { let id: String; let name: String }
|
|
2892
|
+
struct Headers: Codable { let xRateLimit: String; enum CodingKeys: String, CodingKey { case xRateLimit = "x-rate-limit" } }
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
```
|
|
2897
|
+
|
|
2898
|
+
- [ ] **Step 2: Add `'http-stream'` kind emission for Swift**
|
|
2899
|
+
|
|
2900
|
+
Same posture as Kotlin — types only, no HTTP/SSE runtime.
|
|
2901
|
+
|
|
2902
|
+
- [ ] **Step 3: Update Swift snapshot tests**
|
|
2903
|
+
|
|
2904
|
+
```bash
|
|
2905
|
+
npx vitest run src/codegen/targets/swift/run.test.ts -u
|
|
2906
|
+
```
|
|
2907
|
+
|
|
2908
|
+
- [ ] **Step 4: Commit**
|
|
2909
|
+
|
|
2910
|
+
```bash
|
|
2911
|
+
git add src/codegen/targets/swift/
|
|
2912
|
+
git commit -m "feat(codegen-swift): emit Req/Response namespaces and Response.Headers struct"
|
|
2913
|
+
```
|
|
2914
|
+
|
|
2915
|
+
---
|
|
2916
|
+
|
|
2917
|
+
## Phase 10 — Client runtime updates
|
|
2918
|
+
|
|
2919
|
+
### Task 32: Add `headers` to `AdapterStreamResponse` and `TypedStream`
|
|
2920
|
+
|
|
2921
|
+
**Files:**
|
|
2922
|
+
- Modify: `src/client/types.ts`
|
|
2923
|
+
|
|
2924
|
+
- [ ] **Step 1: Extend `AdapterStreamResponse` with `headers: Headers`**
|
|
2925
|
+
|
|
2926
|
+
Find the `AdapterStreamResponse` interface in `src/client/types.ts` and add:
|
|
2927
|
+
|
|
2928
|
+
```ts
|
|
2929
|
+
export interface AdapterStreamResponse {
|
|
2930
|
+
// existing fields...
|
|
2931
|
+
headers: Headers // always populated by the adapter
|
|
2932
|
+
}
|
|
2933
|
+
```
|
|
2934
|
+
|
|
2935
|
+
- [ ] **Step 2: Add optional `.headers` to `TypedStream`**
|
|
2936
|
+
|
|
2937
|
+
```ts
|
|
2938
|
+
export interface TypedStream<TYield, TReturn> {
|
|
2939
|
+
[Symbol.asyncIterator](): AsyncIterator<TYield>
|
|
2940
|
+
result: Promise<TReturn>
|
|
2941
|
+
headers?: Headers // populated for routes declaring res.headers
|
|
2942
|
+
}
|
|
2943
|
+
```
|
|
2944
|
+
|
|
2945
|
+
- [ ] **Step 3: Build**
|
|
2946
|
+
|
|
2947
|
+
```bash
|
|
2948
|
+
npm run build
|
|
2949
|
+
```
|
|
2950
|
+
|
|
2951
|
+
Expected: errors in `fetch-adapter.ts` (now must populate `.headers`) and `stream.ts` (now must wire `.headers` onto `TypedStream`). Fixed in next tasks.
|
|
2952
|
+
|
|
2953
|
+
- [ ] **Step 4: Commit**
|
|
2954
|
+
|
|
2955
|
+
```bash
|
|
2956
|
+
git add src/client/types.ts
|
|
2957
|
+
git commit -m "feat(client): add headers to AdapterStreamResponse and TypedStream"
|
|
2958
|
+
```
|
|
2959
|
+
|
|
2960
|
+
---
|
|
2961
|
+
|
|
2962
|
+
### Task 33: Update `fetch-adapter.ts` to populate `AdapterStreamResponse.headers`
|
|
2963
|
+
|
|
2964
|
+
**Files:**
|
|
2965
|
+
- Modify: `src/client/fetch-adapter.ts`
|
|
2966
|
+
- Modify: `src/client/fetch-adapter.test.ts`
|
|
2967
|
+
|
|
2968
|
+
- [ ] **Step 1: In `fetch-adapter.ts` `stream()` function, set `headers: response.headers` on the returned `AdapterStreamResponse`**
|
|
2969
|
+
|
|
2970
|
+
```ts
|
|
2971
|
+
return {
|
|
2972
|
+
// existing fields...
|
|
2973
|
+
headers: response.headers,
|
|
2974
|
+
}
|
|
2975
|
+
```
|
|
2976
|
+
|
|
2977
|
+
- [ ] **Step 2: Add a test asserting headers are propagated**
|
|
2978
|
+
|
|
2979
|
+
```ts
|
|
2980
|
+
it('propagates response headers to AdapterStreamResponse.headers', async () => {
|
|
2981
|
+
const fetchSpy = vi.fn().mockResolvedValue(
|
|
2982
|
+
new Response('data: x\n\n', {
|
|
2983
|
+
status: 200,
|
|
2984
|
+
headers: { 'x-stream-id': 'abc', 'content-type': 'text/event-stream' },
|
|
2985
|
+
})
|
|
2986
|
+
)
|
|
2987
|
+
const adapter = createFetchAdapter({ fetch: fetchSpy })
|
|
2988
|
+
const result = await adapter.stream({ url: 'https://x', method: 'GET', headers: {} })
|
|
2989
|
+
expect(result.headers.get('x-stream-id')).toBe('abc')
|
|
2990
|
+
})
|
|
2991
|
+
```
|
|
2992
|
+
|
|
2993
|
+
- [ ] **Step 3: Run tests**
|
|
2994
|
+
|
|
2995
|
+
```bash
|
|
2996
|
+
npx vitest run src/client/fetch-adapter.test.ts
|
|
2997
|
+
```
|
|
2998
|
+
|
|
2999
|
+
Expected: PASS.
|
|
3000
|
+
|
|
3001
|
+
- [ ] **Step 4: Commit**
|
|
3002
|
+
|
|
3003
|
+
```bash
|
|
3004
|
+
git add src/client/fetch-adapter.ts src/client/fetch-adapter.test.ts
|
|
3005
|
+
git commit -m "feat(client): populate AdapterStreamResponse.headers in fetch adapter"
|
|
3006
|
+
```
|
|
3007
|
+
|
|
3008
|
+
---
|
|
3009
|
+
|
|
3010
|
+
### Task 34: Update `executeCall` to surface response headers in conditional return
|
|
3011
|
+
|
|
3012
|
+
**Files:**
|
|
3013
|
+
- Modify: `src/client/call.ts`
|
|
3014
|
+
- Modify: `src/client/call.test.ts`
|
|
3015
|
+
|
|
3016
|
+
- [ ] **Step 1: Inspect current `executeCall` body**
|
|
3017
|
+
|
|
3018
|
+
```bash
|
|
3019
|
+
cat src/client/call.ts
|
|
3020
|
+
```
|
|
3021
|
+
|
|
3022
|
+
Identify where the response body is parsed and returned.
|
|
3023
|
+
|
|
3024
|
+
- [ ] **Step 2: Read response headers from the descriptor's metadata**
|
|
3025
|
+
|
|
3026
|
+
The descriptor (in `src/client/types.ts`) carries the route's declared shape. Add a `responseHeadersDeclared?: boolean` field on the descriptor (set by codegen — see Task 28). When `true`, `executeCall` returns `{ body, headers }` instead of bare body.
|
|
3027
|
+
|
|
3028
|
+
```ts
|
|
3029
|
+
// Inside executeCall after parsing the response:
|
|
3030
|
+
if (descriptor.responseHeadersDeclared) {
|
|
3031
|
+
return { body: parsedBody, headers: response.headers }
|
|
3032
|
+
}
|
|
3033
|
+
return parsedBody
|
|
3034
|
+
```
|
|
3035
|
+
|
|
3036
|
+
(`response.headers` here is the platform `Response.headers` — already a `Headers` object.)
|
|
3037
|
+
|
|
3038
|
+
- [ ] **Step 3: Update codegen to emit `responseHeadersDeclared: true` when `slots.responseHeaders` is present**
|
|
3039
|
+
|
|
3040
|
+
Back in `emit-scope.ts` (Task 28), include the flag on the descriptor literal. Inspect a generated file to confirm:
|
|
3041
|
+
|
|
3042
|
+
```ts
|
|
3043
|
+
const descriptor: RouteDescriptor = {
|
|
3044
|
+
kind: 'api',
|
|
3045
|
+
// ...
|
|
3046
|
+
responseHeadersDeclared: true, // only when res.headers in envelope
|
|
3047
|
+
}
|
|
3048
|
+
```
|
|
3049
|
+
|
|
3050
|
+
- [ ] **Step 4: Add tests for both shapes**
|
|
3051
|
+
|
|
3052
|
+
```ts
|
|
3053
|
+
it('returns bare body when response headers not declared', async () => {
|
|
3054
|
+
// ...
|
|
3055
|
+
expect(result).toEqual({ id: 'abc' })
|
|
3056
|
+
})
|
|
3057
|
+
|
|
3058
|
+
it('returns { body, headers } when response headers declared', async () => {
|
|
3059
|
+
// ...
|
|
3060
|
+
expect(result).toEqual({ body: { id: 'abc' }, headers: expect.any(Headers) })
|
|
3061
|
+
expect(result.headers.get('x-rate-limit')).toBe('99')
|
|
3062
|
+
})
|
|
3063
|
+
```
|
|
3064
|
+
|
|
3065
|
+
- [ ] **Step 5: Run tests**
|
|
3066
|
+
|
|
3067
|
+
```bash
|
|
3068
|
+
npx vitest run src/client/call.test.ts
|
|
3069
|
+
```
|
|
3070
|
+
|
|
3071
|
+
Expected: PASS.
|
|
3072
|
+
|
|
3073
|
+
- [ ] **Step 6: Commit**
|
|
3074
|
+
|
|
3075
|
+
```bash
|
|
3076
|
+
git add src/client/call.ts src/client/call.test.ts src/client/types.ts src/codegen/emit-scope.ts
|
|
3077
|
+
git commit -m "feat(client): surface response headers via descriptor.responseHeadersDeclared flag"
|
|
3078
|
+
```
|
|
3079
|
+
|
|
3080
|
+
---
|
|
3081
|
+
|
|
3082
|
+
### Task 35: Update `executeStream` to wire initial headers onto `TypedStream`
|
|
3083
|
+
|
|
3084
|
+
**Files:**
|
|
3085
|
+
- Modify: `src/client/stream.ts`
|
|
3086
|
+
- Modify: `src/client/stream.test.ts`
|
|
3087
|
+
|
|
3088
|
+
- [ ] **Step 1: When constructing the `TypedStream`, set `.headers` from `adapterResponse.headers`**
|
|
3089
|
+
|
|
3090
|
+
```ts
|
|
3091
|
+
// Inside executeStream, when constructing the TypedStream:
|
|
3092
|
+
const typedStream: TypedStream<TYield, TReturn> = {
|
|
3093
|
+
[Symbol.asyncIterator]() { /* ... */ },
|
|
3094
|
+
result: resultPromise,
|
|
3095
|
+
headers: descriptor.responseHeadersDeclared ? adapterResponse.headers : undefined,
|
|
3096
|
+
}
|
|
3097
|
+
```
|
|
3098
|
+
|
|
3099
|
+
- [ ] **Step 2: Add a test**
|
|
3100
|
+
|
|
3101
|
+
```ts
|
|
3102
|
+
it('TypedStream.headers is populated when route declares res.headers', async () => {
|
|
3103
|
+
// mock adapter to return headers
|
|
3104
|
+
// construct stream
|
|
3105
|
+
// expect stream.headers.get('x-stream-id') === '...'
|
|
3106
|
+
})
|
|
3107
|
+
```
|
|
3108
|
+
|
|
3109
|
+
- [ ] **Step 3: Run tests**
|
|
3110
|
+
|
|
3111
|
+
```bash
|
|
3112
|
+
npx vitest run src/client/stream.test.ts
|
|
3113
|
+
```
|
|
3114
|
+
|
|
3115
|
+
Expected: PASS.
|
|
3116
|
+
|
|
3117
|
+
- [ ] **Step 4: Commit**
|
|
3118
|
+
|
|
3119
|
+
```bash
|
|
3120
|
+
git add src/client/stream.ts src/client/stream.test.ts
|
|
3121
|
+
git commit -m "feat(client): wire initial response headers onto TypedStream.headers"
|
|
3122
|
+
```
|
|
3123
|
+
|
|
3124
|
+
---
|
|
3125
|
+
|
|
3126
|
+
## Phase 11 — Migration polish
|
|
3127
|
+
|
|
3128
|
+
### Task 36: Update CLAUDE.md architecture section
|
|
3129
|
+
|
|
3130
|
+
**Files:**
|
|
3131
|
+
- Modify: `CLAUDE.md`
|
|
3132
|
+
|
|
3133
|
+
- [ ] **Step 1: Update the "Schema System" and "Important Patterns" sections to reflect v8**
|
|
3134
|
+
|
|
3135
|
+
Replace the `schema.input` / `APIInput` references with the four-creator surface and `req`/`res` shape.
|
|
3136
|
+
|
|
3137
|
+
- [ ] **Step 2: Add a "Procedure kinds" section near "Architecture"**
|
|
3138
|
+
|
|
3139
|
+
Brief paragraph explaining the four creators and their `kind` discriminants.
|
|
3140
|
+
|
|
3141
|
+
- [ ] **Step 3: Commit**
|
|
3142
|
+
|
|
3143
|
+
```bash
|
|
3144
|
+
git add CLAUDE.md
|
|
3145
|
+
git commit -m "docs(claude): update architecture for v8 four-creator surface"
|
|
3146
|
+
```
|
|
3147
|
+
|
|
3148
|
+
---
|
|
3149
|
+
|
|
3150
|
+
### Task 37: Update `agent_config/` skills, patterns, and templates
|
|
3151
|
+
|
|
3152
|
+
**Files:**
|
|
3153
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures/{patterns.md,anti-patterns.md,api-reference.md}`
|
|
3154
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures-scaffold/templates/*`
|
|
3155
|
+
- Modify: `agent_config/copilot/copilot-instructions.md`
|
|
3156
|
+
- Modify: `agent_config/cursor/cursorrules`
|
|
3157
|
+
|
|
3158
|
+
- [ ] **Step 1: Replace HTTP examples in `patterns.md` to use `CreateHttp`**
|
|
3159
|
+
|
|
3160
|
+
Find all examples using `Procedures<Ctx, APIConfig>().Create(...)` with `schema.input`. Replace with `Procedures<Ctx>().CreateHttp(...)` with `schema.req`.
|
|
3161
|
+
|
|
3162
|
+
- [ ] **Step 2: Add an `anti-patterns.md` entry**
|
|
3163
|
+
|
|
3164
|
+
```markdown
|
|
3165
|
+
## Don't use Create for HTTP routes
|
|
3166
|
+
|
|
3167
|
+
Wrong:
|
|
3168
|
+
\`\`\`ts
|
|
3169
|
+
Procedures<Ctx, APIConfig>().Create('GetUser', { path: '/users/:id', ... })
|
|
3170
|
+
\`\`\`
|
|
3171
|
+
|
|
3172
|
+
Right:
|
|
3173
|
+
\`\`\`ts
|
|
3174
|
+
Procedures<Ctx>().CreateHttp('GetUser', { path: '/users/:id', schema: { req: ... } })
|
|
3175
|
+
\`\`\`
|
|
3176
|
+
|
|
3177
|
+
`Create` is the RPC primitive. HTTP routes belong on `CreateHttp` / `CreateHttpStream`.
|
|
3178
|
+
```
|
|
3179
|
+
|
|
3180
|
+
- [ ] **Step 3: Update scaffold templates**
|
|
3181
|
+
|
|
3182
|
+
Replace `procedure-template.ts`, `hono-rpc-template.ts`, etc. as needed. Add new `create-http.ts` and `create-http-stream.ts` templates.
|
|
3183
|
+
|
|
3184
|
+
- [ ] **Step 4: Mirror updates to Copilot and Cursor files**
|
|
3185
|
+
|
|
3186
|
+
Both files are condensed equivalents — copy the relevant sections.
|
|
3187
|
+
|
|
3188
|
+
- [ ] **Step 5: Run agent_config setup dry-run to verify**
|
|
3189
|
+
|
|
3190
|
+
```bash
|
|
3191
|
+
node agent_config/bin/setup.mjs --dry-run
|
|
3192
|
+
```
|
|
3193
|
+
|
|
3194
|
+
Expected: shows what would be installed; no errors.
|
|
3195
|
+
|
|
3196
|
+
- [ ] **Step 6: Commit**
|
|
3197
|
+
|
|
3198
|
+
```bash
|
|
3199
|
+
git add agent_config/
|
|
3200
|
+
git commit -m "docs(agent_config): update skills/templates for v8 CreateHttp surface"
|
|
3201
|
+
```
|
|
3202
|
+
|
|
3203
|
+
---
|
|
3204
|
+
|
|
3205
|
+
### Task 38: Update CHANGELOG and README
|
|
3206
|
+
|
|
3207
|
+
**Files:**
|
|
3208
|
+
- Modify: `CHANGELOG.md`
|
|
3209
|
+
- Modify: `README.md`
|
|
3210
|
+
|
|
3211
|
+
- [ ] **Step 1: Add v8.0.0 entry to CHANGELOG**
|
|
3212
|
+
|
|
3213
|
+
```markdown
|
|
3214
|
+
## 8.0.0 — 2026-05-08
|
|
3215
|
+
|
|
3216
|
+
### Breaking changes
|
|
3217
|
+
|
|
3218
|
+
- **`schema.input` removed** from `Create` / `CreateStream`. Use `CreateHttp` / `CreateHttpStream` for per-channel HTTP validation.
|
|
3219
|
+
- **HTTP routes use `CreateHttp`**, not `Create` + `APIConfig`. HTTP fields (`path`, `method`, `successStatus`, `scope`, `errors`) are first-class on `CreateHttp` config.
|
|
3220
|
+
- **`schema.input.X` → `schema.req.X`** (channels: `pathParams`, `query`, `body`, `headers`).
|
|
3221
|
+
- **`APIInput` and `APIConfig` removed.**
|
|
3222
|
+
- **`APIHttpRouteDoc.jsonSchema` regrouped under `req` / `res`** — extendProcedureDoc callbacks reading `base.jsonSchema.body` need to read `base.jsonSchema.req.body`.
|
|
3223
|
+
- **`Procedures<Ctx, APIConfig>()` → `Procedures<Ctx>()`** for HTTP-only factories.
|
|
3224
|
+
- **Generated client flat-mode aliases renamed:** `<Route>Params` → `<Route>ReqPathParams` (and similar). Namespace-mode users get `Users.GetUser.Req.PathParams` automatically.
|
|
3225
|
+
|
|
3226
|
+
### New
|
|
3227
|
+
|
|
3228
|
+
- **`CreateHttp` / `CreateHttpStream`** — first-class HTTP creators with structured `req` channels and typed `res: { body?, headers? }`.
|
|
3229
|
+
- **Conditional return shape** — `CreateHttp` handler returns body bare unless `res.headers` declared, in which case `{ body, headers }`.
|
|
3230
|
+
- **`TypedStream.headers`** — initial response headers surfaced on streams that declare `res.headers`.
|
|
3231
|
+
- **`kind` discriminant** on every registration (`'rpc' | 'rpc-stream' | 'http' | 'http-stream'`) drives builder routing and `skippedProcedures` warnings.
|
|
3232
|
+
|
|
3233
|
+
### Migration
|
|
3234
|
+
|
|
3235
|
+
See full table at [docs/superpowers/specs/2026-05-08-create-http-design.md](docs/superpowers/specs/2026-05-08-create-http-design.md#migration). Quick checklist:
|
|
3236
|
+
|
|
3237
|
+
1. Replace `Procedures<Ctx, APIConfig>()` with `Procedures<Ctx>()` for HTTP factories.
|
|
3238
|
+
2. Replace `Create('Foo', { path, method, schema: { input } }, h)` with `CreateHttp('Foo', { path, method, schema: { req } }, h)`.
|
|
3239
|
+
3. Replace `schema.input.X` with `schema.req.X`.
|
|
3240
|
+
4. Re-run `npx ts-procedures-codegen` to regenerate clients.
|
|
3241
|
+
5. Update flat-mode-alias call sites if applicable.
|
|
3242
|
+
```
|
|
3243
|
+
|
|
3244
|
+
- [ ] **Step 2: Update README.md headline examples**
|
|
3245
|
+
|
|
3246
|
+
Replace any `Create` + `schema.input` example with `CreateHttp` + `schema.req`. Add a new section showcasing response headers.
|
|
3247
|
+
|
|
3248
|
+
- [ ] **Step 3: Commit**
|
|
3249
|
+
|
|
3250
|
+
```bash
|
|
3251
|
+
git add CHANGELOG.md README.md
|
|
3252
|
+
git commit -m "docs: add v8.0.0 changelog entry and README migration"
|
|
3253
|
+
```
|
|
3254
|
+
|
|
3255
|
+
---
|
|
3256
|
+
|
|
3257
|
+
### Task 39: Bump package version to 8.0.0
|
|
3258
|
+
|
|
3259
|
+
**Files:**
|
|
3260
|
+
- Modify: `package.json`
|
|
3261
|
+
|
|
3262
|
+
- [ ] **Step 1: Update version**
|
|
3263
|
+
|
|
3264
|
+
```bash
|
|
3265
|
+
npm version 8.0.0 --no-git-tag-version
|
|
3266
|
+
```
|
|
3267
|
+
|
|
3268
|
+
- [ ] **Step 2: Verify**
|
|
3269
|
+
|
|
3270
|
+
```bash
|
|
3271
|
+
grep '"version"' package.json
|
|
3272
|
+
```
|
|
3273
|
+
|
|
3274
|
+
Expected: `"version": "8.0.0"`.
|
|
3275
|
+
|
|
3276
|
+
- [ ] **Step 3: Commit**
|
|
3277
|
+
|
|
3278
|
+
```bash
|
|
3279
|
+
git add package.json
|
|
3280
|
+
git commit -m "chore: bump version to 8.0.0"
|
|
3281
|
+
```
|
|
3282
|
+
|
|
3283
|
+
---
|
|
3284
|
+
|
|
3285
|
+
### Task 40: Final integration check — build, lint, full test suite
|
|
3286
|
+
|
|
3287
|
+
**Files:** none
|
|
3288
|
+
|
|
3289
|
+
- [ ] **Step 1: Run full build**
|
|
3290
|
+
|
|
3291
|
+
```bash
|
|
3292
|
+
npm run build
|
|
3293
|
+
```
|
|
3294
|
+
|
|
3295
|
+
Expected: green, no warnings.
|
|
3296
|
+
|
|
3297
|
+
- [ ] **Step 2: Run lint**
|
|
3298
|
+
|
|
3299
|
+
```bash
|
|
3300
|
+
npm run lint
|
|
3301
|
+
```
|
|
3302
|
+
|
|
3303
|
+
Expected: green.
|
|
3304
|
+
|
|
3305
|
+
- [ ] **Step 3: Run full test suite**
|
|
3306
|
+
|
|
3307
|
+
```bash
|
|
3308
|
+
npm run test
|
|
3309
|
+
```
|
|
3310
|
+
|
|
3311
|
+
Expected: all tests green. Any `it.todo` from Phase 1 (legacy `schema.input` tests) should now either be deleted (if obsolete) or migrated to `CreateHttp` / `CreateHttpStream` equivalents.
|
|
3312
|
+
|
|
3313
|
+
- [ ] **Step 4: Address any failures by fixing forward (no skip / no `.todo`)**
|
|
3314
|
+
|
|
3315
|
+
- [ ] **Step 5: Commit any final fixes**
|
|
3316
|
+
|
|
3317
|
+
```bash
|
|
3318
|
+
git add -p
|
|
3319
|
+
git commit -m "test: final cleanup of v7 schema.input migrations"
|
|
3320
|
+
```
|
|
3321
|
+
|
|
3322
|
+
- [ ] **Step 6: Verify clean working tree**
|
|
3323
|
+
|
|
3324
|
+
```bash
|
|
3325
|
+
git status
|
|
3326
|
+
```
|
|
3327
|
+
|
|
3328
|
+
Expected: `nothing to commit, working tree clean`.
|
|
3329
|
+
|
|
3330
|
+
---
|
|
3331
|
+
|
|
3332
|
+
## Self-review notes
|
|
3333
|
+
|
|
3334
|
+
Spec coverage check (against `docs/superpowers/specs/2026-05-08-create-http-design.md`):
|
|
3335
|
+
|
|
3336
|
+
- ✅ Four-creator surface (Tasks 1-18)
|
|
3337
|
+
- ✅ `schema.input` removed from Create/CreateStream (Tasks 7-8)
|
|
3338
|
+
- ✅ HTTP fields baked into CreateHttp (Tasks 10-11)
|
|
3339
|
+
- ✅ Conditional return shape (Task 14)
|
|
3340
|
+
- ✅ Async-preamble for CreateHttpStream initial headers (Task 18)
|
|
3341
|
+
- ✅ `kind` discriminant + builder filtering (Tasks 5, 23)
|
|
3342
|
+
- ✅ Path-param consistency moved to core (Task 11)
|
|
3343
|
+
- ✅ HonoAPIAppBuilder unification serving both http and http-stream (Tasks 19-22)
|
|
3344
|
+
- ✅ APIHttpRouteDoc regrouped to req/res (Task 25)
|
|
3345
|
+
- ✅ HttpStreamRouteDoc added (Task 25)
|
|
3346
|
+
- ✅ Codegen TS namespacing (Req/Response) and conditional return (Task 28)
|
|
3347
|
+
- ✅ Codegen Kotlin/Swift Response.Headers (Tasks 30-31)
|
|
3348
|
+
- ✅ Client runtime: TypedStream.headers, descriptor.responseHeadersDeclared (Tasks 32-35)
|
|
3349
|
+
- ✅ Migration messages on legacy shapes (Tasks 7-8)
|
|
3350
|
+
- ✅ CHANGELOG, README, agent_config (Tasks 37-38)
|
|
3351
|
+
- ✅ Version bump (Task 39)
|
|
3352
|
+
- ✅ Astro adapter (Task 24)
|
|
3353
|
+
- ✅ skippedProcedures across all builders (Tasks 20, 23)
|
|
3354
|
+
|
|
3355
|
+
No placeholders. Type names consistent across tasks (`THttpProcedureRegistration`, `THttpStreamProcedureRegistration`, `HttpReturn<TRes>`, `HTTP_STREAM_HEADERS_SENTINEL`, `responseHeadersDeclared`).
|