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,3365 @@
|
|
|
1
|
+
# HonoAppBuilder Convergence 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:** Replace the three Hono builders (`HonoRPCAppBuilder`, `HonoAPIAppBuilder`, `HonoStreamAppBuilder`) with a single `HonoAppBuilder` that accepts any `Procedures<>()` factory and dispatches each procedure to the correct handler by `kind`.
|
|
6
|
+
|
|
7
|
+
**Architecture:** New `src/implementations/http/hono/` directory containing the public builder + per-kind handler/doc modules. Shared error-dispatch primitives extracted to `src/implementations/http/error-dispatch.ts` (one level up so it can be reused by future framework adapters). Old `hono-rpc/`, `hono-api/`, `hono-stream/` directories deleted entirely.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Hono 4.x, Vitest 4.x, AJV (validation, unchanged), TypeBox (schema authoring, unchanged).
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md`
|
|
12
|
+
|
|
13
|
+
**Pre-existing infrastructure this plan depends on (do not modify):**
|
|
14
|
+
- `src/implementations/http/error-taxonomy.ts` — `defineErrorTaxonomy`, `resolveErrorResponse`, `defaultErrorTaxonomy`, `UnknownErrorConfig`, `taxonomyToErrorDocs`, `PROCEDURE_REGISTRATION_ERROR_DOC`
|
|
15
|
+
- `src/implementations/http/doc-registry.ts` — `DocRegistry` class
|
|
16
|
+
- `src/implementations/types.ts` — `RPCConfig`, `APIConfig`, `RPCHttpRouteDoc`, `APIHttpRouteDoc`, `StreamHttpRouteDoc`, `HttpStreamRouteDoc`, `AnyHttpRouteDoc`, `DocSource`, `DocEnvelope`, `ProceduresFactory`, `ExtractContext`, `ExtractConfig`, `HttpMethod`, `StreamMode`, `APIInput`
|
|
17
|
+
- `src/types.ts` — `TProcedureRegistration`, `TStreamProcedureRegistration`, `THttpProcedureRegistration`, `THttpStreamProcedureRegistration`, `ProcedureKind`
|
|
18
|
+
- `src/index.ts` — `Procedures` factory
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## File Structure
|
|
23
|
+
|
|
24
|
+
**Created:**
|
|
25
|
+
- `src/implementations/http/error-dispatch.ts` — shared `dispatchPreStreamError` + `dispatchMidStreamError`
|
|
26
|
+
- `src/implementations/http/error-dispatch.test.ts`
|
|
27
|
+
- `src/implementations/http/hono/index.ts` — `HonoAppBuilder` class (public surface)
|
|
28
|
+
- `src/implementations/http/hono/index.test.ts` — public-surface tests
|
|
29
|
+
- `src/implementations/http/hono/types.ts` — `HonoAppBuilderConfig`, `HonoFactoryItem`, `OnRequestErrorContext`, `AnyProcedureRegistration`
|
|
30
|
+
- `src/implementations/http/hono/path.ts` — `makeRoutePath`, `resolveFullPath`
|
|
31
|
+
- `src/implementations/http/hono/path.test.ts`
|
|
32
|
+
- `src/implementations/http/hono/handlers/rpc.ts` — `installRpcRoute`
|
|
33
|
+
- `src/implementations/http/hono/handlers/rpc.test.ts`
|
|
34
|
+
- `src/implementations/http/hono/handlers/http.ts` — `installHttpRoute`
|
|
35
|
+
- `src/implementations/http/hono/handlers/http.test.ts`
|
|
36
|
+
- `src/implementations/http/hono/handlers/stream.ts` — `installRpcStreamRoute`
|
|
37
|
+
- `src/implementations/http/hono/handlers/stream.test.ts`
|
|
38
|
+
- `src/implementations/http/hono/handlers/http-stream.ts` — `installHttpStreamRoute`
|
|
39
|
+
- `src/implementations/http/hono/handlers/http-stream.test.ts`
|
|
40
|
+
- `src/implementations/http/hono/docs/rpc-doc.ts`
|
|
41
|
+
- `src/implementations/http/hono/docs/http-doc.ts`
|
|
42
|
+
- `src/implementations/http/hono/docs/stream-doc.ts`
|
|
43
|
+
- `src/implementations/http/hono/docs/http-stream-doc.ts`
|
|
44
|
+
|
|
45
|
+
**Deleted entirely (after the new code passes its tests):**
|
|
46
|
+
- `src/implementations/http/hono-rpc/` (4 files — index.ts, types.ts, index.test.ts, error-taxonomy.test.ts)
|
|
47
|
+
- `src/implementations/http/hono-api/` (4 files)
|
|
48
|
+
- `src/implementations/http/hono-stream/` (4 files)
|
|
49
|
+
|
|
50
|
+
**Modified:**
|
|
51
|
+
- `package.json` — remove `./hono-rpc`, `./hono-api`, `./hono-stream` exports; add `./hono` export
|
|
52
|
+
- `src/implementations/http/doc-registry.test.ts` — update imports + assertions to point at `HonoAppBuilder`
|
|
53
|
+
- `src/implementations/http/on-request-error.test.ts` — collapse three describe blocks into one, retarget at `HonoAppBuilder`
|
|
54
|
+
- `src/implementations/http/route-errors.test.ts` — retarget imports
|
|
55
|
+
- `src/implementations/http/error-taxonomy.test.ts` — retarget imports (top-level file, not the per-builder ones)
|
|
56
|
+
- `src/implementations/http/README.md` — rewrite around single builder
|
|
57
|
+
- `docs/http-integrations.md` — rewrite around single builder
|
|
58
|
+
- `CLAUDE.md` — update references to old builder names
|
|
59
|
+
- `README.md` — update v8 changelog/migration entry
|
|
60
|
+
- `agent_config/claude-code/skills/ts-procedures/SKILL.md`, `patterns.md`, `anti-patterns.md`, `api-reference.md` — update examples
|
|
61
|
+
- `agent_config/claude-code/skills/ts-procedures-scaffold/templates/` — delete `hono-rpc.ts`, `hono-stream.ts`; rename/rewrite `hono-api.ts` → `hono.ts`
|
|
62
|
+
- `agent_config/copilot/copilot-instructions.md`, `agent_config/cursor/cursorrules` — same content updates
|
|
63
|
+
- `src/types.ts` — update `ProcedureKind` JSDoc references from old builder names to `HonoAppBuilder`
|
|
64
|
+
- `src/create-stream.ts` — update comments referring to `HonoStreamAppBuilder`
|
|
65
|
+
- `src/implementations/http/doc-registry.ts` — update comment referring to `HonoRPCAppBuilder`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Phase 1 — Foundation (independent, no impact on existing code)
|
|
70
|
+
|
|
71
|
+
### Task 1: Create `path.ts` — `makeRoutePath` + `resolveFullPath`
|
|
72
|
+
|
|
73
|
+
**Files:**
|
|
74
|
+
- Create: `src/implementations/http/hono/path.ts`
|
|
75
|
+
- Test: `src/implementations/http/hono/path.test.ts`
|
|
76
|
+
|
|
77
|
+
The `makeRoutePath` function dispatches by kind: rpc/rpc-stream get scope+name+version generation, http/http-stream prepend the prefix to a developer-supplied path. `resolveFullPath` is the helper for the http case (also used directly by the http handlers).
|
|
78
|
+
|
|
79
|
+
- [ ] **Step 1: Write the failing test**
|
|
80
|
+
|
|
81
|
+
Create `src/implementations/http/hono/path.test.ts`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { describe, expect, test } from 'vitest'
|
|
85
|
+
import { makeRoutePath, resolveFullPath } from './path.js'
|
|
86
|
+
import type {
|
|
87
|
+
TProcedureRegistration,
|
|
88
|
+
TStreamProcedureRegistration,
|
|
89
|
+
THttpProcedureRegistration,
|
|
90
|
+
THttpStreamProcedureRegistration,
|
|
91
|
+
} from '../../../types.js'
|
|
92
|
+
|
|
93
|
+
describe('makeRoutePath', () => {
|
|
94
|
+
test('rpc: scope/name/version with kebab-case', () => {
|
|
95
|
+
const proc = {
|
|
96
|
+
name: 'GetUser',
|
|
97
|
+
kind: 'rpc',
|
|
98
|
+
config: { scope: 'users', version: 1 },
|
|
99
|
+
handler: async () => undefined,
|
|
100
|
+
} as unknown as TProcedureRegistration
|
|
101
|
+
expect(makeRoutePath({ procedure: proc })).toBe('/users/get-user/1')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('rpc: array scope joined with /', () => {
|
|
105
|
+
const proc = {
|
|
106
|
+
name: 'List',
|
|
107
|
+
kind: 'rpc',
|
|
108
|
+
config: { scope: ['UserModule', 'admin'], version: 2 },
|
|
109
|
+
handler: async () => undefined,
|
|
110
|
+
} as unknown as TProcedureRegistration
|
|
111
|
+
expect(makeRoutePath({ procedure: proc })).toBe('/user-module/admin/list/2')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('rpc: pathPrefix is normalized (leading slash inserted)', () => {
|
|
115
|
+
const proc = {
|
|
116
|
+
name: 'Echo',
|
|
117
|
+
kind: 'rpc',
|
|
118
|
+
config: { scope: 'echo', version: 1 },
|
|
119
|
+
handler: async () => undefined,
|
|
120
|
+
} as unknown as TProcedureRegistration
|
|
121
|
+
expect(makeRoutePath({ procedure: proc, prefix: 'api/v1' })).toBe('/api/v1/echo/echo/1')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('rpc-stream: same shape as rpc', () => {
|
|
125
|
+
const proc = {
|
|
126
|
+
name: 'Tail',
|
|
127
|
+
kind: 'rpc-stream',
|
|
128
|
+
isStream: true,
|
|
129
|
+
config: { scope: 'logs', version: 1 },
|
|
130
|
+
handler: async function* () {},
|
|
131
|
+
} as unknown as TStreamProcedureRegistration
|
|
132
|
+
expect(makeRoutePath({ procedure: proc, prefix: '/api' })).toBe('/api/logs/tail/1')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('http: prepends prefix to config.path', () => {
|
|
136
|
+
const proc = {
|
|
137
|
+
name: 'GetUser',
|
|
138
|
+
kind: 'http',
|
|
139
|
+
config: { path: '/users/:id', method: 'get' },
|
|
140
|
+
handler: async () => undefined,
|
|
141
|
+
} as unknown as THttpProcedureRegistration
|
|
142
|
+
expect(makeRoutePath({ procedure: proc, prefix: '/api/v1' })).toBe('/api/v1/users/:id')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('http: leading slash on path is preserved (not duplicated)', () => {
|
|
146
|
+
const proc = {
|
|
147
|
+
name: 'Get',
|
|
148
|
+
kind: 'http',
|
|
149
|
+
config: { path: 'users', method: 'get' },
|
|
150
|
+
handler: async () => undefined,
|
|
151
|
+
} as unknown as THttpProcedureRegistration
|
|
152
|
+
expect(makeRoutePath({ procedure: proc, prefix: '/api' })).toBe('/api/users')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('http-stream: same shape as http', () => {
|
|
156
|
+
const proc = {
|
|
157
|
+
name: 'TailLogs',
|
|
158
|
+
kind: 'http-stream',
|
|
159
|
+
config: { path: '/logs/tail', method: 'get' },
|
|
160
|
+
handler: async () => ({ stream: (async function* () {})(), initialHeaders: undefined }),
|
|
161
|
+
} as unknown as THttpStreamProcedureRegistration
|
|
162
|
+
expect(makeRoutePath({ procedure: proc })).toBe('/logs/tail')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('resolveFullPath', () => {
|
|
167
|
+
test('combines prefix and path', () => {
|
|
168
|
+
expect(resolveFullPath('/users', '/api/v1')).toBe('/api/v1/users')
|
|
169
|
+
})
|
|
170
|
+
test('normalizes prefix without leading slash', () => {
|
|
171
|
+
expect(resolveFullPath('/users', 'api')).toBe('/api/users')
|
|
172
|
+
})
|
|
173
|
+
test('normalizes path without leading slash', () => {
|
|
174
|
+
expect(resolveFullPath('users', '/api')).toBe('/api/users')
|
|
175
|
+
})
|
|
176
|
+
test('no prefix', () => {
|
|
177
|
+
expect(resolveFullPath('/users')).toBe('/users')
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
183
|
+
|
|
184
|
+
Run: `npx vitest run src/implementations/http/hono/path.test.ts`
|
|
185
|
+
Expected: FAIL — module `./path.js` does not exist.
|
|
186
|
+
|
|
187
|
+
- [ ] **Step 3: Implement `path.ts`**
|
|
188
|
+
|
|
189
|
+
Create `src/implementations/http/hono/path.ts`:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { kebabCase } from 'es-toolkit/string'
|
|
193
|
+
import { castArray } from 'es-toolkit/compat'
|
|
194
|
+
import type {
|
|
195
|
+
TProcedureRegistration,
|
|
196
|
+
TStreamProcedureRegistration,
|
|
197
|
+
THttpProcedureRegistration,
|
|
198
|
+
THttpStreamProcedureRegistration,
|
|
199
|
+
} from '../../../types.js'
|
|
200
|
+
|
|
201
|
+
export type AnyProcedureRegistration =
|
|
202
|
+
| TProcedureRegistration
|
|
203
|
+
| TStreamProcedureRegistration
|
|
204
|
+
| THttpProcedureRegistration<any>
|
|
205
|
+
| THttpStreamProcedureRegistration<any>
|
|
206
|
+
|
|
207
|
+
function normalizePrefix(prefix?: string): string {
|
|
208
|
+
if (!prefix) return ''
|
|
209
|
+
return prefix.startsWith('/') ? prefix : `/${prefix}`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolves the full route path for any procedure kind.
|
|
214
|
+
*
|
|
215
|
+
* - `rpc` / `rpc-stream`: `{prefix}/{kebab(scope)}/{kebab(name)}/{version}`
|
|
216
|
+
* - `http` / `http-stream`: `{prefix}{config.path}` (path-as-given, prefix normalized)
|
|
217
|
+
*/
|
|
218
|
+
export function makeRoutePath({
|
|
219
|
+
procedure,
|
|
220
|
+
prefix,
|
|
221
|
+
}: {
|
|
222
|
+
procedure: AnyProcedureRegistration
|
|
223
|
+
prefix?: string
|
|
224
|
+
}): string {
|
|
225
|
+
const normalizedPrefix = normalizePrefix(prefix)
|
|
226
|
+
|
|
227
|
+
switch (procedure.kind) {
|
|
228
|
+
case 'rpc':
|
|
229
|
+
case 'rpc-stream': {
|
|
230
|
+
const cfg = procedure.config as { scope: string | string[]; version: number }
|
|
231
|
+
const scopeSegments = castArray(cfg.scope).map(kebabCase).join('/')
|
|
232
|
+
return `${normalizedPrefix}/${scopeSegments}/${kebabCase(procedure.name)}/${String(cfg.version).trim()}`
|
|
233
|
+
}
|
|
234
|
+
case 'http':
|
|
235
|
+
case 'http-stream': {
|
|
236
|
+
const cfg = procedure.config as { path: string }
|
|
237
|
+
return resolveFullPath(cfg.path, prefix)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Combines a developer-supplied path with an optional prefix. Both are
|
|
244
|
+
* normalized to begin with a single `/`.
|
|
245
|
+
*/
|
|
246
|
+
export function resolveFullPath(procedurePath: string, prefix?: string): string {
|
|
247
|
+
const normalizedPrefix = normalizePrefix(prefix)
|
|
248
|
+
const normalizedPath = procedurePath.startsWith('/') ? procedurePath : `/${procedurePath}`
|
|
249
|
+
return `${normalizedPrefix}${normalizedPath}`
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
254
|
+
|
|
255
|
+
Run: `npx vitest run src/implementations/http/hono/path.test.ts`
|
|
256
|
+
Expected: PASS — all 11 tests green.
|
|
257
|
+
|
|
258
|
+
- [ ] **Step 5: Commit**
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
git add src/implementations/http/hono/path.ts src/implementations/http/hono/path.test.ts
|
|
262
|
+
git commit -m "$(cat <<'EOF'
|
|
263
|
+
feat(hono): add unified path resolver for all four procedure kinds
|
|
264
|
+
|
|
265
|
+
makeRoutePath dispatches by procedure.kind: rpc/rpc-stream use
|
|
266
|
+
scope+name+version generation; http/http-stream prepend the prefix to
|
|
267
|
+
config.path. Foundation for HonoAppBuilder convergence.
|
|
268
|
+
|
|
269
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
270
|
+
EOF
|
|
271
|
+
)"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### Task 2: `error-dispatch.ts` — `dispatchPreStreamError`
|
|
277
|
+
|
|
278
|
+
**Files:**
|
|
279
|
+
- Create: `src/implementations/http/error-dispatch.ts`
|
|
280
|
+
- Test: `src/implementations/http/error-dispatch.test.ts`
|
|
281
|
+
|
|
282
|
+
Pulls the inline three-step dispatch (observer → taxonomy → onError → hard default) out of the existing builders into one shared module. The pre-stream form returns a `Response`; mid-stream form (added in Task 3) returns body data.
|
|
283
|
+
|
|
284
|
+
- [ ] **Step 1: Write the failing test**
|
|
285
|
+
|
|
286
|
+
Create `src/implementations/http/error-dispatch.test.ts`:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
290
|
+
import { Hono } from 'hono'
|
|
291
|
+
import { dispatchPreStreamError } from './error-dispatch.js'
|
|
292
|
+
import { defineErrorTaxonomy } from './error-taxonomy.js'
|
|
293
|
+
import { ProcedureValidationError } from '../../errors.js'
|
|
294
|
+
|
|
295
|
+
function makeContext() {
|
|
296
|
+
// Hono's Context isn't directly constructible, so spin a request through
|
|
297
|
+
// a Hono app and capture the context inside a route handler.
|
|
298
|
+
return new Promise<import('hono').Context>((resolve) => {
|
|
299
|
+
const app = new Hono()
|
|
300
|
+
app.get('/', (c) => {
|
|
301
|
+
resolve(c)
|
|
302
|
+
return c.text('ok')
|
|
303
|
+
})
|
|
304
|
+
app.request('/')
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const dummyProcedure = {
|
|
309
|
+
name: 'Test',
|
|
310
|
+
kind: 'rpc',
|
|
311
|
+
config: { scope: 'test', version: 1 },
|
|
312
|
+
handler: async () => undefined,
|
|
313
|
+
} as any
|
|
314
|
+
|
|
315
|
+
describe('dispatchPreStreamError', () => {
|
|
316
|
+
test('default taxonomy: ProcedureValidationError → 400', async () => {
|
|
317
|
+
const c = await makeContext()
|
|
318
|
+
const err = new ProcedureValidationError('Test', 'Validation error for Test', [
|
|
319
|
+
{ instancePath: '/x', message: 'expected number' } as any,
|
|
320
|
+
])
|
|
321
|
+
|
|
322
|
+
const res = await dispatchPreStreamError({
|
|
323
|
+
err,
|
|
324
|
+
procedure: dummyProcedure,
|
|
325
|
+
raw: c,
|
|
326
|
+
cfg: {},
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
expect(res.status).toBe(400)
|
|
330
|
+
const body = await res.json()
|
|
331
|
+
expect(body.name).toBe('ProcedureValidationError')
|
|
332
|
+
expect(body.procedureName).toBe('Test')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('user taxonomy entry takes precedence', async () => {
|
|
336
|
+
const c = await makeContext()
|
|
337
|
+
class MyError extends Error {
|
|
338
|
+
constructor(public reason: string) { super(reason) }
|
|
339
|
+
}
|
|
340
|
+
const errors = defineErrorTaxonomy({
|
|
341
|
+
MyError: { class: MyError, statusCode: 422, toResponse: (e) => ({ name: 'MyError', reason: e.reason }) },
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
const res = await dispatchPreStreamError({
|
|
345
|
+
err: new MyError('boom'),
|
|
346
|
+
procedure: dummyProcedure,
|
|
347
|
+
raw: c,
|
|
348
|
+
cfg: { errors },
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
expect(res.status).toBe(422)
|
|
352
|
+
expect(await res.json()).toEqual({ name: 'MyError', reason: 'boom' })
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('falls back to onError imperative callback', async () => {
|
|
356
|
+
const c = await makeContext()
|
|
357
|
+
const onError = vi.fn().mockResolvedValue(c.json({ ok: false }, 503))
|
|
358
|
+
|
|
359
|
+
const res = await dispatchPreStreamError({
|
|
360
|
+
err: new Error('unmatched'),
|
|
361
|
+
procedure: dummyProcedure,
|
|
362
|
+
raw: c,
|
|
363
|
+
cfg: { onError: (proc, raw, e) => onError(proc, raw, e) },
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
expect(onError).toHaveBeenCalled()
|
|
367
|
+
expect(res.status).toBe(503)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('hard default 500 when nothing matches', async () => {
|
|
371
|
+
const c = await makeContext()
|
|
372
|
+
const res = await dispatchPreStreamError({
|
|
373
|
+
err: new Error('boom'),
|
|
374
|
+
procedure: dummyProcedure,
|
|
375
|
+
raw: c,
|
|
376
|
+
cfg: {},
|
|
377
|
+
})
|
|
378
|
+
expect(res.status).toBe(500)
|
|
379
|
+
expect(await res.json()).toEqual({ error: 'boom' })
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test('onRequestError observer fires before dispatch and is awaited', async () => {
|
|
383
|
+
const c = await makeContext()
|
|
384
|
+
const calls: string[] = []
|
|
385
|
+
const onRequestError = vi.fn(async () => {
|
|
386
|
+
calls.push('observer')
|
|
387
|
+
})
|
|
388
|
+
const errors = defineErrorTaxonomy({
|
|
389
|
+
AnyError: {
|
|
390
|
+
match: (e): e is Error => e instanceof Error,
|
|
391
|
+
statusCode: 500,
|
|
392
|
+
toResponse: () => {
|
|
393
|
+
calls.push('toResponse')
|
|
394
|
+
return { name: 'AnyError' }
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
await dispatchPreStreamError({
|
|
400
|
+
err: new Error('x'),
|
|
401
|
+
procedure: dummyProcedure,
|
|
402
|
+
raw: c,
|
|
403
|
+
cfg: { errors, onRequestError },
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
expect(calls).toEqual(['observer', 'toResponse'])
|
|
407
|
+
expect(onRequestError).toHaveBeenCalledWith({ err: expect.any(Error), procedure: dummyProcedure, raw: c })
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
test('observer throw is swallowed and logged', async () => {
|
|
411
|
+
const c = await makeContext()
|
|
412
|
+
const consoleErr = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
413
|
+
const onRequestError = vi.fn(() => { throw new Error('observer broke') })
|
|
414
|
+
|
|
415
|
+
const res = await dispatchPreStreamError({
|
|
416
|
+
err: new Error('x'),
|
|
417
|
+
procedure: dummyProcedure,
|
|
418
|
+
raw: c,
|
|
419
|
+
cfg: { onRequestError },
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
expect(res.status).toBe(500)
|
|
423
|
+
expect(consoleErr).toHaveBeenCalled()
|
|
424
|
+
consoleErr.mockRestore()
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
430
|
+
|
|
431
|
+
Run: `npx vitest run src/implementations/http/error-dispatch.test.ts`
|
|
432
|
+
Expected: FAIL — module does not exist.
|
|
433
|
+
|
|
434
|
+
- [ ] **Step 3: Implement `error-dispatch.ts` (pre-stream half)**
|
|
435
|
+
|
|
436
|
+
Create `src/implementations/http/error-dispatch.ts`:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import type { Context } from 'hono'
|
|
440
|
+
import {
|
|
441
|
+
resolveErrorResponse,
|
|
442
|
+
type ErrorTaxonomy,
|
|
443
|
+
type UnknownErrorConfig,
|
|
444
|
+
} from './error-taxonomy.js'
|
|
445
|
+
import type {
|
|
446
|
+
TProcedureRegistration,
|
|
447
|
+
TStreamProcedureRegistration,
|
|
448
|
+
THttpProcedureRegistration,
|
|
449
|
+
THttpStreamProcedureRegistration,
|
|
450
|
+
} from '../../types.js'
|
|
451
|
+
|
|
452
|
+
export type AnyProcedureRegistration =
|
|
453
|
+
| TProcedureRegistration
|
|
454
|
+
| TStreamProcedureRegistration
|
|
455
|
+
| THttpProcedureRegistration<any>
|
|
456
|
+
| THttpStreamProcedureRegistration<any>
|
|
457
|
+
|
|
458
|
+
export type OnRequestErrorContext = {
|
|
459
|
+
err: unknown
|
|
460
|
+
procedure: AnyProcedureRegistration
|
|
461
|
+
raw: Context
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export type OnRequestErrorObserver = (
|
|
465
|
+
ctx: OnRequestErrorContext,
|
|
466
|
+
) => void | Promise<void>
|
|
467
|
+
|
|
468
|
+
export type PreStreamOnError = (
|
|
469
|
+
procedure: AnyProcedureRegistration,
|
|
470
|
+
raw: Context,
|
|
471
|
+
err: Error,
|
|
472
|
+
) => Response | Promise<Response>
|
|
473
|
+
|
|
474
|
+
export type PreStreamErrorConfig = {
|
|
475
|
+
errors?: ErrorTaxonomy
|
|
476
|
+
unknownError?: UnknownErrorConfig
|
|
477
|
+
onError?: PreStreamOnError
|
|
478
|
+
onRequestError?: OnRequestErrorObserver
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Used by rpc, http, and the pre-stream guard in stream / http-stream
|
|
483
|
+
* handlers. Order: onRequestError observer (awaited, throws swallowed) →
|
|
484
|
+
* taxonomy resolver → imperative onError → hard default 500.
|
|
485
|
+
*/
|
|
486
|
+
export async function dispatchPreStreamError(params: {
|
|
487
|
+
err: unknown
|
|
488
|
+
procedure: AnyProcedureRegistration
|
|
489
|
+
raw: Context
|
|
490
|
+
cfg: PreStreamErrorConfig
|
|
491
|
+
}): Promise<Response> {
|
|
492
|
+
const { err, procedure, raw, cfg } = params
|
|
493
|
+
|
|
494
|
+
if (cfg.onRequestError) {
|
|
495
|
+
try {
|
|
496
|
+
await cfg.onRequestError({ err, procedure, raw })
|
|
497
|
+
} catch (observerErr) {
|
|
498
|
+
console.error(
|
|
499
|
+
'[ts-procedures hono] onRequestError threw — swallowed:',
|
|
500
|
+
observerErr,
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (cfg.errors || cfg.unknownError) {
|
|
506
|
+
const resolved = resolveErrorResponse({
|
|
507
|
+
err,
|
|
508
|
+
userTaxonomy: cfg.errors,
|
|
509
|
+
unknownError: cfg.unknownError,
|
|
510
|
+
procedure,
|
|
511
|
+
raw,
|
|
512
|
+
})
|
|
513
|
+
if (resolved) {
|
|
514
|
+
await resolved.runOnCatch()
|
|
515
|
+
return raw.json(resolved.body, resolved.statusCode as never)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (cfg.onError) {
|
|
520
|
+
return cfg.onError(procedure, raw, err as Error)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return raw.json({ error: (err as Error).message }, 500)
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
528
|
+
|
|
529
|
+
Run: `npx vitest run src/implementations/http/error-dispatch.test.ts`
|
|
530
|
+
Expected: PASS — 6 tests green.
|
|
531
|
+
|
|
532
|
+
- [ ] **Step 5: Commit**
|
|
533
|
+
|
|
534
|
+
```bash
|
|
535
|
+
git add src/implementations/http/error-dispatch.ts src/implementations/http/error-dispatch.test.ts
|
|
536
|
+
git commit -m "$(cat <<'EOF'
|
|
537
|
+
feat(http): extract dispatchPreStreamError shared helper
|
|
538
|
+
|
|
539
|
+
Consolidates the observer→taxonomy→onError→default chain duplicated
|
|
540
|
+
across hono-rpc, hono-api, and hono-stream into one helper. Lives one
|
|
541
|
+
level up from hono/ so future framework adapters can reuse it.
|
|
542
|
+
|
|
543
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
544
|
+
EOF
|
|
545
|
+
)"
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
### Task 3: `error-dispatch.ts` — `dispatchMidStreamError`
|
|
551
|
+
|
|
552
|
+
**Files:**
|
|
553
|
+
- Modify: `src/implementations/http/error-dispatch.ts`
|
|
554
|
+
- Modify: `src/implementations/http/error-dispatch.test.ts`
|
|
555
|
+
|
|
556
|
+
Mid-stream dispatcher returns body bytes (not a `Response`) because the HTTP status is already committed when streaming starts. The result includes optional SSE event/id/retry overrides and a deferred `runOnCatch`.
|
|
557
|
+
|
|
558
|
+
- [ ] **Step 1: Add failing tests for the mid-stream dispatcher**
|
|
559
|
+
|
|
560
|
+
Append to `src/implementations/http/error-dispatch.test.ts`:
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
import { dispatchMidStreamError } from './error-dispatch.js'
|
|
564
|
+
// `sse` currently lives in hono-stream/index.ts. Task 11 moves it into
|
|
565
|
+
// hono/handlers/stream.ts and rewrites this import to './hono/handlers/stream.js'.
|
|
566
|
+
import { sse } from './hono-stream/index.js'
|
|
567
|
+
|
|
568
|
+
const streamProcedure = {
|
|
569
|
+
name: 'Tail',
|
|
570
|
+
kind: 'rpc-stream',
|
|
571
|
+
isStream: true,
|
|
572
|
+
config: { scope: 'logs', version: 1 },
|
|
573
|
+
handler: async function* () {},
|
|
574
|
+
} as any
|
|
575
|
+
|
|
576
|
+
describe('dispatchMidStreamError', () => {
|
|
577
|
+
test('uses taxonomy body when configured', async () => {
|
|
578
|
+
const c = await makeContext()
|
|
579
|
+
class MidErr extends Error {}
|
|
580
|
+
const errors = defineErrorTaxonomy({
|
|
581
|
+
MidErr: { class: MidErr, statusCode: 500, toResponse: () => ({ name: 'MidErr', detail: 'x' }) },
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
const result = await dispatchMidStreamError({
|
|
585
|
+
err: new MidErr('mid'),
|
|
586
|
+
procedure: streamProcedure,
|
|
587
|
+
raw: c,
|
|
588
|
+
cfg: { errors },
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
expect(result.data).toEqual({ name: 'MidErr', detail: 'x' })
|
|
592
|
+
expect(result.sseEvent).toBe('error')
|
|
593
|
+
expect(result.runOnCatch).toBeTypeOf('function')
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
test('falls back to onMidStreamError', async () => {
|
|
597
|
+
const c = await makeContext()
|
|
598
|
+
const result = await dispatchMidStreamError({
|
|
599
|
+
err: new Error('boom'),
|
|
600
|
+
procedure: streamProcedure,
|
|
601
|
+
raw: c,
|
|
602
|
+
cfg: {
|
|
603
|
+
onMidStreamError: () => ({ data: { kind: 'fail', msg: 'boom' } }),
|
|
604
|
+
},
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
expect(result.data).toEqual({ kind: 'fail', msg: 'boom' })
|
|
608
|
+
expect(result.sseEvent).toBe('Tail') // procedure name override
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('hard default { error: msg } when nothing matches', async () => {
|
|
612
|
+
const c = await makeContext()
|
|
613
|
+
const result = await dispatchMidStreamError({
|
|
614
|
+
err: new Error('plain'),
|
|
615
|
+
procedure: streamProcedure,
|
|
616
|
+
raw: c,
|
|
617
|
+
cfg: {},
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
expect(result.data).toEqual({ error: 'plain' })
|
|
621
|
+
expect(result.sseEvent).toBe('error')
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
test('onRequestError observer fires before dispatch (mid-stream form)', async () => {
|
|
625
|
+
const c = await makeContext()
|
|
626
|
+
const onRequestError = vi.fn(async () => {})
|
|
627
|
+
|
|
628
|
+
await dispatchMidStreamError({
|
|
629
|
+
err: new Error('x'),
|
|
630
|
+
procedure: streamProcedure,
|
|
631
|
+
raw: c,
|
|
632
|
+
cfg: { onRequestError },
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
expect(onRequestError).toHaveBeenCalled()
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
641
|
+
|
|
642
|
+
Run: `npx vitest run src/implementations/http/error-dispatch.test.ts`
|
|
643
|
+
Expected: FAIL — `dispatchMidStreamError` not exported.
|
|
644
|
+
|
|
645
|
+
- [ ] **Step 3: Add `dispatchMidStreamError` to `error-dispatch.ts`**
|
|
646
|
+
|
|
647
|
+
Append to `src/implementations/http/error-dispatch.ts`:
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
export type MidStreamErrorResult<TErrorData = unknown> = {
|
|
651
|
+
data: TErrorData
|
|
652
|
+
closeStream?: boolean
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export type OnMidStreamError<TErrorData = unknown> = (
|
|
656
|
+
procedure: TStreamProcedureRegistration | THttpStreamProcedureRegistration<any>,
|
|
657
|
+
raw: Context,
|
|
658
|
+
err: Error,
|
|
659
|
+
) => MidStreamErrorResult<TErrorData> | undefined
|
|
660
|
+
|
|
661
|
+
export type MidStreamErrorConfig<TErrorData = unknown> = {
|
|
662
|
+
errors?: ErrorTaxonomy
|
|
663
|
+
unknownError?: UnknownErrorConfig
|
|
664
|
+
onMidStreamError?: OnMidStreamError<TErrorData>
|
|
665
|
+
onRequestError?: OnRequestErrorObserver
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export type DispatchedMidStreamError = {
|
|
669
|
+
data: unknown
|
|
670
|
+
sseEvent?: string
|
|
671
|
+
sseId?: string
|
|
672
|
+
sseRetry?: number
|
|
673
|
+
runOnCatch?: () => Promise<void>
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Used inside SSE / text-stream generator catch blocks. Returns the bytes
|
|
678
|
+
* to write to the open stream — the HTTP status is already committed, so we
|
|
679
|
+
* can't return a Response. Order matches dispatchPreStreamError: observer
|
|
680
|
+
* (awaited, throws swallowed) → taxonomy → onMidStreamError → hard default.
|
|
681
|
+
*/
|
|
682
|
+
export async function dispatchMidStreamError<TErrorData = unknown>(params: {
|
|
683
|
+
err: unknown
|
|
684
|
+
procedure: TStreamProcedureRegistration | THttpStreamProcedureRegistration<any>
|
|
685
|
+
raw: Context
|
|
686
|
+
cfg: MidStreamErrorConfig<TErrorData>
|
|
687
|
+
}): Promise<DispatchedMidStreamError> {
|
|
688
|
+
const { err, procedure, raw, cfg } = params
|
|
689
|
+
|
|
690
|
+
if (cfg.onRequestError) {
|
|
691
|
+
try {
|
|
692
|
+
await cfg.onRequestError({ err, procedure, raw })
|
|
693
|
+
} catch (observerErr) {
|
|
694
|
+
console.error(
|
|
695
|
+
'[ts-procedures hono] onRequestError (mid-stream) threw — swallowed:',
|
|
696
|
+
observerErr,
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (cfg.errors || cfg.unknownError) {
|
|
702
|
+
const resolved = resolveErrorResponse({
|
|
703
|
+
err,
|
|
704
|
+
userTaxonomy: cfg.errors,
|
|
705
|
+
unknownError: cfg.unknownError,
|
|
706
|
+
procedure,
|
|
707
|
+
raw,
|
|
708
|
+
})
|
|
709
|
+
if (resolved) {
|
|
710
|
+
return {
|
|
711
|
+
data: resolved.body,
|
|
712
|
+
sseEvent: 'error',
|
|
713
|
+
runOnCatch: resolved.runOnCatch,
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (cfg.onMidStreamError) {
|
|
719
|
+
const result = cfg.onMidStreamError(procedure, raw, err as Error)
|
|
720
|
+
if (result?.data !== undefined) {
|
|
721
|
+
return {
|
|
722
|
+
data: result.data,
|
|
723
|
+
sseEvent: procedure.name,
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
data: { error: (err as Error).message },
|
|
730
|
+
sseEvent: 'error',
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
You also need to add the import at the top of `error-dispatch.ts`:
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
import type { TStreamProcedureRegistration, THttpStreamProcedureRegistration } from '../../types.js'
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
For the test imports of `sse`, use the existing path until Task 9 moves it: `import { sse } from './hono-stream/index.js'`.
|
|
742
|
+
|
|
743
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
744
|
+
|
|
745
|
+
Run: `npx vitest run src/implementations/http/error-dispatch.test.ts`
|
|
746
|
+
Expected: PASS — all 10 tests green.
|
|
747
|
+
|
|
748
|
+
- [ ] **Step 5: Commit**
|
|
749
|
+
|
|
750
|
+
```bash
|
|
751
|
+
git add src/implementations/http/error-dispatch.ts src/implementations/http/error-dispatch.test.ts
|
|
752
|
+
git commit -m "$(cat <<'EOF'
|
|
753
|
+
feat(http): add dispatchMidStreamError for stream catch blocks
|
|
754
|
+
|
|
755
|
+
Returns body bytes plus optional SSE event/id/retry overrides, since
|
|
756
|
+
HTTP status is already committed once streaming starts. Same order as
|
|
757
|
+
the pre-stream form: observer → taxonomy → onMidStreamError → default.
|
|
758
|
+
|
|
759
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
760
|
+
EOF
|
|
761
|
+
)"
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
## Phase 2 — Doc Builders (one per kind, pure functions)
|
|
767
|
+
|
|
768
|
+
### Task 4: `docs/rpc-doc.ts` — `buildRpcRouteDoc`
|
|
769
|
+
|
|
770
|
+
**Files:**
|
|
771
|
+
- Create: `src/implementations/http/hono/docs/rpc-doc.ts`
|
|
772
|
+
|
|
773
|
+
Pure function — extracts an `RPCHttpRouteDoc` from a `TProcedureRegistration<any, RPCConfig>`. No tests for the pure builders in isolation; they're covered by the handler integration tests in Phase 3 and by the `index.test.ts` envelope tests in Phase 4.
|
|
774
|
+
|
|
775
|
+
- [ ] **Step 1: Implement `rpc-doc.ts`**
|
|
776
|
+
|
|
777
|
+
Create `src/implementations/http/hono/docs/rpc-doc.ts`:
|
|
778
|
+
|
|
779
|
+
```ts
|
|
780
|
+
import type { TProcedureRegistration } from '../../../../types.js'
|
|
781
|
+
import type { RPCConfig, RPCHttpRouteDoc } from '../../../types.js'
|
|
782
|
+
import { makeRoutePath } from '../path.js'
|
|
783
|
+
|
|
784
|
+
export function buildRpcRouteDoc(
|
|
785
|
+
procedure: TProcedureRegistration<any, RPCConfig>,
|
|
786
|
+
prefix: string | undefined,
|
|
787
|
+
extend?: (params: { base: RPCHttpRouteDoc; procedure: TProcedureRegistration<any, RPCConfig> }) => Record<string, unknown>,
|
|
788
|
+
): RPCHttpRouteDoc {
|
|
789
|
+
const config = procedure.config as RPCConfig & {
|
|
790
|
+
schema?: { params?: Record<string, unknown>; returnType?: Record<string, unknown> }
|
|
791
|
+
errors?: string[]
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const jsonSchema: { body?: Record<string, unknown>; response?: Record<string, unknown> } = {}
|
|
795
|
+
if (config.schema?.params) jsonSchema.body = config.schema.params
|
|
796
|
+
if (config.schema?.returnType) jsonSchema.response = config.schema.returnType
|
|
797
|
+
|
|
798
|
+
const base: RPCHttpRouteDoc = {
|
|
799
|
+
kind: 'rpc',
|
|
800
|
+
name: procedure.name,
|
|
801
|
+
version: config.version,
|
|
802
|
+
scope: config.scope,
|
|
803
|
+
path: makeRoutePath({ procedure, prefix }),
|
|
804
|
+
method: 'post',
|
|
805
|
+
jsonSchema,
|
|
806
|
+
}
|
|
807
|
+
if (config.errors && config.errors.length > 0) {
|
|
808
|
+
base.errors = [...config.errors]
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const extended = extend ? extend({ base, procedure }) : {}
|
|
812
|
+
return { ...extended, ...base }
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
- [ ] **Step 2: Verify it builds**
|
|
817
|
+
|
|
818
|
+
Run: `npx tsc --noEmit`
|
|
819
|
+
Expected: PASS — no type errors. (No test file yet; integration tests come in Phase 3.)
|
|
820
|
+
|
|
821
|
+
- [ ] **Step 3: Commit**
|
|
822
|
+
|
|
823
|
+
```bash
|
|
824
|
+
git add src/implementations/http/hono/docs/rpc-doc.ts
|
|
825
|
+
git commit -m "feat(hono): add rpc-doc builder
|
|
826
|
+
|
|
827
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
### Task 5: `docs/stream-doc.ts` — `buildStreamRouteDoc`
|
|
833
|
+
|
|
834
|
+
**Files:**
|
|
835
|
+
- Create: `src/implementations/http/hono/docs/stream-doc.ts`
|
|
836
|
+
|
|
837
|
+
- [ ] **Step 1: Implement `stream-doc.ts`**
|
|
838
|
+
|
|
839
|
+
Create `src/implementations/http/hono/docs/stream-doc.ts`:
|
|
840
|
+
|
|
841
|
+
```ts
|
|
842
|
+
import type { TStreamProcedureRegistration } from '../../../../types.js'
|
|
843
|
+
import type { RPCConfig, StreamHttpRouteDoc, StreamMode } from '../../../types.js'
|
|
844
|
+
import { makeRoutePath } from '../path.js'
|
|
845
|
+
|
|
846
|
+
export function buildStreamRouteDoc(
|
|
847
|
+
procedure: TStreamProcedureRegistration<any, RPCConfig>,
|
|
848
|
+
streamMode: StreamMode,
|
|
849
|
+
prefix: string | undefined,
|
|
850
|
+
extend?: (params: { base: StreamHttpRouteDoc; procedure: TStreamProcedureRegistration<any, RPCConfig> }) => Record<string, unknown>,
|
|
851
|
+
): StreamHttpRouteDoc {
|
|
852
|
+
const config = procedure.config as RPCConfig & {
|
|
853
|
+
schema?: { params?: Record<string, unknown>; yieldType?: Record<string, unknown>; returnType?: Record<string, unknown> }
|
|
854
|
+
errors?: string[]
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const jsonSchema: StreamHttpRouteDoc['jsonSchema'] = {}
|
|
858
|
+
if (config.schema?.params) jsonSchema.params = config.schema.params
|
|
859
|
+
|
|
860
|
+
if (streamMode === 'sse') {
|
|
861
|
+
jsonSchema.yieldType = {
|
|
862
|
+
type: 'object',
|
|
863
|
+
description: 'SSE message envelope. The data field contains the procedure yield value.',
|
|
864
|
+
required: ['data', 'event', 'id'],
|
|
865
|
+
properties: {
|
|
866
|
+
data: config.schema?.yieldType ?? {},
|
|
867
|
+
event: { type: 'string' },
|
|
868
|
+
id: { type: 'string' },
|
|
869
|
+
retry: { type: 'number' },
|
|
870
|
+
},
|
|
871
|
+
}
|
|
872
|
+
} else if (config.schema?.yieldType) {
|
|
873
|
+
jsonSchema.yieldType = config.schema.yieldType
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (config.schema?.returnType) jsonSchema.returnType = config.schema.returnType
|
|
877
|
+
|
|
878
|
+
// POST first so codegen (which reads `methods[0]`) defaults to POST. POST is
|
|
879
|
+
// the canonical method for streams because it can carry a body for params.
|
|
880
|
+
const base: StreamHttpRouteDoc = {
|
|
881
|
+
kind: 'stream',
|
|
882
|
+
name: procedure.name,
|
|
883
|
+
version: config.version,
|
|
884
|
+
scope: config.scope,
|
|
885
|
+
path: makeRoutePath({ procedure, prefix }),
|
|
886
|
+
methods: ['post', 'get'],
|
|
887
|
+
streamMode,
|
|
888
|
+
jsonSchema,
|
|
889
|
+
}
|
|
890
|
+
if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
|
|
891
|
+
|
|
892
|
+
const extended = extend ? extend({ base, procedure }) : {}
|
|
893
|
+
return { ...extended, ...base }
|
|
894
|
+
}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
- [ ] **Step 2: Verify build**
|
|
898
|
+
|
|
899
|
+
Run: `npx tsc --noEmit`
|
|
900
|
+
Expected: PASS.
|
|
901
|
+
|
|
902
|
+
- [ ] **Step 3: Commit**
|
|
903
|
+
|
|
904
|
+
```bash
|
|
905
|
+
git add src/implementations/http/hono/docs/stream-doc.ts
|
|
906
|
+
git commit -m "feat(hono): add stream-doc builder
|
|
907
|
+
|
|
908
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
### Task 6: `docs/http-doc.ts` — `buildHttpRouteDoc`
|
|
914
|
+
|
|
915
|
+
**Files:**
|
|
916
|
+
- Create: `src/implementations/http/hono/docs/http-doc.ts`
|
|
917
|
+
|
|
918
|
+
- [ ] **Step 1: Implement `http-doc.ts`**
|
|
919
|
+
|
|
920
|
+
Create `src/implementations/http/hono/docs/http-doc.ts`:
|
|
921
|
+
|
|
922
|
+
```ts
|
|
923
|
+
import type { THttpProcedureRegistration } from '../../../../types.js'
|
|
924
|
+
import type { APIHttpRouteDoc } from '../../../types.js'
|
|
925
|
+
import { resolveFullPath } from '../path.js'
|
|
926
|
+
|
|
927
|
+
export function buildHttpRouteDoc(
|
|
928
|
+
procedure: THttpProcedureRegistration<any>,
|
|
929
|
+
prefix: string | undefined,
|
|
930
|
+
extend?: (params: { base: APIHttpRouteDoc; procedure: THttpProcedureRegistration<any> }) => Record<string, unknown>,
|
|
931
|
+
): APIHttpRouteDoc {
|
|
932
|
+
const config = procedure.config
|
|
933
|
+
const fullPath = resolveFullPath(config.path, prefix)
|
|
934
|
+
const reqSchema = config.schema?.req
|
|
935
|
+
const resSchema = config.schema?.res
|
|
936
|
+
const jsonSchema: APIHttpRouteDoc['jsonSchema'] = {}
|
|
937
|
+
|
|
938
|
+
if (reqSchema && (reqSchema.pathParams || reqSchema.query || reqSchema.body || reqSchema.headers)) {
|
|
939
|
+
jsonSchema.req = {}
|
|
940
|
+
if (reqSchema.pathParams) jsonSchema.req.pathParams = reqSchema.pathParams as any
|
|
941
|
+
if (reqSchema.query) jsonSchema.req.query = reqSchema.query as any
|
|
942
|
+
if (reqSchema.body) jsonSchema.req.body = reqSchema.body as any
|
|
943
|
+
if (reqSchema.headers) jsonSchema.req.headers = reqSchema.headers as any
|
|
944
|
+
}
|
|
945
|
+
if (resSchema && (resSchema.body || resSchema.headers)) {
|
|
946
|
+
jsonSchema.res = {}
|
|
947
|
+
if (resSchema.body) jsonSchema.res.body = resSchema.body as any
|
|
948
|
+
if (resSchema.headers) jsonSchema.res.headers = resSchema.headers as any
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const base: APIHttpRouteDoc = {
|
|
952
|
+
kind: 'api',
|
|
953
|
+
name: procedure.name,
|
|
954
|
+
scope: config.scope,
|
|
955
|
+
path: config.path,
|
|
956
|
+
method: config.method,
|
|
957
|
+
fullPath,
|
|
958
|
+
jsonSchema,
|
|
959
|
+
}
|
|
960
|
+
if (config.successStatus) base.successStatus = config.successStatus
|
|
961
|
+
if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
|
|
962
|
+
|
|
963
|
+
const extended = extend ? extend({ base, procedure }) : {}
|
|
964
|
+
return { ...extended, ...base }
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
- [ ] **Step 2: Verify build**
|
|
969
|
+
|
|
970
|
+
Run: `npx tsc --noEmit`
|
|
971
|
+
Expected: PASS.
|
|
972
|
+
|
|
973
|
+
- [ ] **Step 3: Commit**
|
|
974
|
+
|
|
975
|
+
```bash
|
|
976
|
+
git add src/implementations/http/hono/docs/http-doc.ts
|
|
977
|
+
git commit -m "feat(hono): add http-doc builder
|
|
978
|
+
|
|
979
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
---
|
|
983
|
+
|
|
984
|
+
### Task 7: `docs/http-stream-doc.ts` — `buildHttpStreamRouteDoc`
|
|
985
|
+
|
|
986
|
+
**Files:**
|
|
987
|
+
- Create: `src/implementations/http/hono/docs/http-stream-doc.ts`
|
|
988
|
+
|
|
989
|
+
- [ ] **Step 1: Implement `http-stream-doc.ts`**
|
|
990
|
+
|
|
991
|
+
Create `src/implementations/http/hono/docs/http-stream-doc.ts`:
|
|
992
|
+
|
|
993
|
+
```ts
|
|
994
|
+
import type { THttpStreamProcedureRegistration } from '../../../../types.js'
|
|
995
|
+
import type { HttpStreamRouteDoc } from '../../../types.js'
|
|
996
|
+
import { resolveFullPath } from '../path.js'
|
|
997
|
+
|
|
998
|
+
export function buildHttpStreamRouteDoc(
|
|
999
|
+
procedure: THttpStreamProcedureRegistration<any>,
|
|
1000
|
+
prefix: string | undefined,
|
|
1001
|
+
extend?: (params: {
|
|
1002
|
+
base: HttpStreamRouteDoc
|
|
1003
|
+
procedure: THttpStreamProcedureRegistration<any>
|
|
1004
|
+
}) => Record<string, unknown>,
|
|
1005
|
+
): HttpStreamRouteDoc {
|
|
1006
|
+
const config = procedure.config
|
|
1007
|
+
const fullPath = resolveFullPath(config.path, prefix)
|
|
1008
|
+
const reqSchema = config.schema?.req
|
|
1009
|
+
const resSchema = config.schema?.res
|
|
1010
|
+
const jsonSchema: HttpStreamRouteDoc['jsonSchema'] = {}
|
|
1011
|
+
|
|
1012
|
+
if (reqSchema && (reqSchema.pathParams || reqSchema.query || reqSchema.body || reqSchema.headers)) {
|
|
1013
|
+
jsonSchema.req = {}
|
|
1014
|
+
if (reqSchema.pathParams) jsonSchema.req.pathParams = reqSchema.pathParams as any
|
|
1015
|
+
if (reqSchema.query) jsonSchema.req.query = reqSchema.query as any
|
|
1016
|
+
if (reqSchema.body) jsonSchema.req.body = reqSchema.body as any
|
|
1017
|
+
if (reqSchema.headers) jsonSchema.req.headers = reqSchema.headers as any
|
|
1018
|
+
}
|
|
1019
|
+
if (resSchema?.headers) jsonSchema.res = { headers: resSchema.headers as any }
|
|
1020
|
+
if (config.schema?.yield) jsonSchema.yield = config.schema.yield as any
|
|
1021
|
+
if (config.schema?.returnType) jsonSchema.returnType = config.schema.returnType as any
|
|
1022
|
+
|
|
1023
|
+
const base: HttpStreamRouteDoc = {
|
|
1024
|
+
kind: 'http-stream',
|
|
1025
|
+
name: procedure.name,
|
|
1026
|
+
scope: config.scope,
|
|
1027
|
+
path: config.path,
|
|
1028
|
+
method: config.method,
|
|
1029
|
+
fullPath,
|
|
1030
|
+
streamMode: 'sse',
|
|
1031
|
+
jsonSchema,
|
|
1032
|
+
}
|
|
1033
|
+
if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
|
|
1034
|
+
|
|
1035
|
+
const extended = extend ? extend({ base, procedure }) : {}
|
|
1036
|
+
return { ...extended, ...base }
|
|
1037
|
+
}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
- [ ] **Step 2: Verify build**
|
|
1041
|
+
|
|
1042
|
+
Run: `npx tsc --noEmit`
|
|
1043
|
+
Expected: PASS.
|
|
1044
|
+
|
|
1045
|
+
- [ ] **Step 3: Commit**
|
|
1046
|
+
|
|
1047
|
+
```bash
|
|
1048
|
+
git add src/implementations/http/hono/docs/http-stream-doc.ts
|
|
1049
|
+
git commit -m "feat(hono): add http-stream-doc builder
|
|
1050
|
+
|
|
1051
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
## Phase 3 — Per-Kind Handler Modules
|
|
1057
|
+
|
|
1058
|
+
Each handler installer takes `(app, procedure, factoryItem, config, docs)` and:
|
|
1059
|
+
1. Builds the route doc, pushes to `docs`.
|
|
1060
|
+
2. Registers a Hono handler that resolves context, extracts params, invokes the procedure, applies result, calls `dispatchPreStreamError` on throw.
|
|
1061
|
+
|
|
1062
|
+
### Task 8: Define shared `types.ts` for the hono module
|
|
1063
|
+
|
|
1064
|
+
**Files:**
|
|
1065
|
+
- Create: `src/implementations/http/hono/types.ts`
|
|
1066
|
+
|
|
1067
|
+
This file is referenced by all handler modules and the public `index.ts`. No tests — pure type declarations.
|
|
1068
|
+
|
|
1069
|
+
- [ ] **Step 1: Implement `types.ts`**
|
|
1070
|
+
|
|
1071
|
+
Create `src/implementations/http/hono/types.ts`:
|
|
1072
|
+
|
|
1073
|
+
```ts
|
|
1074
|
+
import type { Context, Hono } from 'hono'
|
|
1075
|
+
import type { ErrorTaxonomy, UnknownErrorConfig } from '../error-taxonomy.js'
|
|
1076
|
+
import type {
|
|
1077
|
+
AnyProcedureRegistration,
|
|
1078
|
+
OnRequestErrorObserver,
|
|
1079
|
+
OnMidStreamError,
|
|
1080
|
+
PreStreamOnError,
|
|
1081
|
+
} from '../error-dispatch.js'
|
|
1082
|
+
import type {
|
|
1083
|
+
ProceduresFactory,
|
|
1084
|
+
ExtractContext,
|
|
1085
|
+
StreamMode,
|
|
1086
|
+
AnyHttpRouteDoc,
|
|
1087
|
+
RPCHttpRouteDoc,
|
|
1088
|
+
APIHttpRouteDoc,
|
|
1089
|
+
StreamHttpRouteDoc,
|
|
1090
|
+
HttpStreamRouteDoc,
|
|
1091
|
+
} from '../../types.js'
|
|
1092
|
+
import type {
|
|
1093
|
+
TProcedureRegistration,
|
|
1094
|
+
TStreamProcedureRegistration,
|
|
1095
|
+
THttpProcedureRegistration,
|
|
1096
|
+
} from '../../../types.js'
|
|
1097
|
+
|
|
1098
|
+
export type { AnyProcedureRegistration } from '../error-dispatch.js'
|
|
1099
|
+
|
|
1100
|
+
/** Default query parser using URLSearchParams. */
|
|
1101
|
+
export type QueryParser = (queryString: string) => Record<string, unknown>
|
|
1102
|
+
|
|
1103
|
+
export type OnRequestErrorContext = {
|
|
1104
|
+
err: unknown
|
|
1105
|
+
procedure: AnyProcedureRegistration
|
|
1106
|
+
raw: Context
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
export type ExtendProcedureDoc = (params: {
|
|
1110
|
+
base: RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc | HttpStreamRouteDoc
|
|
1111
|
+
procedure: AnyProcedureRegistration
|
|
1112
|
+
}) => Record<string, unknown>
|
|
1113
|
+
|
|
1114
|
+
export type HonoFactoryItem<TFactory extends ProceduresFactory = ProceduresFactory> = {
|
|
1115
|
+
factory: TFactory
|
|
1116
|
+
factoryContext:
|
|
1117
|
+
| ExtractContext<TFactory>
|
|
1118
|
+
| ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
1119
|
+
streamMode?: StreamMode
|
|
1120
|
+
extendProcedureDoc?: ExtendProcedureDoc
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
export type HonoAppBuilderConfig<TStreamErrorData = unknown> = {
|
|
1124
|
+
/** Existing Hono app. If omitted, a new instance is created. */
|
|
1125
|
+
app?: Hono
|
|
1126
|
+
/** Path prefix applied to every route across all kinds. */
|
|
1127
|
+
pathPrefix?: string
|
|
1128
|
+
|
|
1129
|
+
// Error handling — peers
|
|
1130
|
+
errors?: ErrorTaxonomy
|
|
1131
|
+
unknownError?: UnknownErrorConfig
|
|
1132
|
+
onError?: PreStreamOnError
|
|
1133
|
+
/** Cross-cutting observer — fires for every caught error before dispatch. */
|
|
1134
|
+
onRequestError?: OnRequestErrorObserver
|
|
1135
|
+
|
|
1136
|
+
// Lifecycle (every kind)
|
|
1137
|
+
onRequestStart?: (c: Context) => void
|
|
1138
|
+
onRequestEnd?: (c: Context) => void
|
|
1139
|
+
|
|
1140
|
+
// Kind-specific blocks
|
|
1141
|
+
rpc?: {
|
|
1142
|
+
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
1143
|
+
}
|
|
1144
|
+
api?: {
|
|
1145
|
+
queryParser?: QueryParser
|
|
1146
|
+
onSuccess?: (procedure: THttpProcedureRegistration<any>, c: Context) => void
|
|
1147
|
+
}
|
|
1148
|
+
stream?: {
|
|
1149
|
+
/** Default for both rpc-stream and http-stream. */
|
|
1150
|
+
defaultStreamMode?: StreamMode
|
|
1151
|
+
onStreamStart?: (
|
|
1152
|
+
procedure: TStreamProcedureRegistration | THttpProcedureRegistration<any>,
|
|
1153
|
+
c: Context,
|
|
1154
|
+
streamMode: StreamMode,
|
|
1155
|
+
) => void
|
|
1156
|
+
onStreamEnd?: (
|
|
1157
|
+
procedure: TStreamProcedureRegistration | THttpProcedureRegistration<any>,
|
|
1158
|
+
c: Context,
|
|
1159
|
+
streamMode: StreamMode,
|
|
1160
|
+
) => void
|
|
1161
|
+
onMidStreamError?: OnMidStreamError<TStreamErrorData>
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
export type DocAccumulator = AnyHttpRouteDoc[]
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
- [ ] **Step 2: Verify build**
|
|
1169
|
+
|
|
1170
|
+
Run: `npx tsc --noEmit`
|
|
1171
|
+
Expected: PASS.
|
|
1172
|
+
|
|
1173
|
+
- [ ] **Step 3: Commit**
|
|
1174
|
+
|
|
1175
|
+
```bash
|
|
1176
|
+
git add src/implementations/http/hono/types.ts
|
|
1177
|
+
git commit -m "feat(hono): add HonoAppBuilderConfig + factory item types
|
|
1178
|
+
|
|
1179
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
---
|
|
1183
|
+
|
|
1184
|
+
### Task 9: `handlers/rpc.ts` — `installRpcRoute`
|
|
1185
|
+
|
|
1186
|
+
**Files:**
|
|
1187
|
+
- Create: `src/implementations/http/hono/handlers/rpc.ts`
|
|
1188
|
+
- Test: `src/implementations/http/hono/handlers/rpc.test.ts`
|
|
1189
|
+
|
|
1190
|
+
- [ ] **Step 1: Write the failing test**
|
|
1191
|
+
|
|
1192
|
+
Create `src/implementations/http/hono/handlers/rpc.test.ts`:
|
|
1193
|
+
|
|
1194
|
+
```ts
|
|
1195
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
1196
|
+
import { Hono } from 'hono'
|
|
1197
|
+
import { Type } from 'typebox'
|
|
1198
|
+
import { Procedures } from '../../../../index.js'
|
|
1199
|
+
import type { RPCConfig } from '../../../types.js'
|
|
1200
|
+
import { installRpcRoute } from './rpc.js'
|
|
1201
|
+
|
|
1202
|
+
function buildApp(register: (factory: ReturnType<typeof Procedures<{}, RPCConfig>>) => void, cfg: any = {}) {
|
|
1203
|
+
const RPC = Procedures<{ userId: string }, RPCConfig>()
|
|
1204
|
+
register(RPC)
|
|
1205
|
+
const app = new Hono()
|
|
1206
|
+
const docs: any[] = []
|
|
1207
|
+
for (const proc of RPC.getProcedures().values()) {
|
|
1208
|
+
if (proc.kind !== 'rpc') continue
|
|
1209
|
+
installRpcRoute({
|
|
1210
|
+
app,
|
|
1211
|
+
procedure: proc as any,
|
|
1212
|
+
factoryItem: { factory: RPC, factoryContext: () => ({ userId: '123' }) },
|
|
1213
|
+
cfg,
|
|
1214
|
+
docs,
|
|
1215
|
+
})
|
|
1216
|
+
}
|
|
1217
|
+
return { app, docs }
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
describe('installRpcRoute', () => {
|
|
1221
|
+
test('mounts POST route at scope/name/version path', async () => {
|
|
1222
|
+
const { app, docs } = buildApp((RPC) => {
|
|
1223
|
+
RPC.Create('Echo', {
|
|
1224
|
+
scope: 'echo',
|
|
1225
|
+
version: 1,
|
|
1226
|
+
schema: { params: Type.Object({ msg: Type.String() }) },
|
|
1227
|
+
}, async (_ctx, params) => params)
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
const res = await app.request('/echo/echo/1', {
|
|
1231
|
+
method: 'POST',
|
|
1232
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1233
|
+
body: JSON.stringify({ msg: 'hi' }),
|
|
1234
|
+
})
|
|
1235
|
+
expect(res.status).toBe(200)
|
|
1236
|
+
expect(await res.json()).toEqual({ msg: 'hi' })
|
|
1237
|
+
expect(docs).toHaveLength(1)
|
|
1238
|
+
expect(docs[0].kind).toBe('rpc')
|
|
1239
|
+
expect(docs[0].path).toBe('/echo/echo/1')
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1242
|
+
test('rpc.onSuccess fires after handler', async () => {
|
|
1243
|
+
const onSuccess = vi.fn()
|
|
1244
|
+
const { app } = buildApp((RPC) => {
|
|
1245
|
+
RPC.Create('Ping', { scope: 'p', version: 1 }, async () => ({ ok: true }))
|
|
1246
|
+
}, { rpc: { onSuccess } })
|
|
1247
|
+
|
|
1248
|
+
await app.request('/p/ping/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
1249
|
+
expect(onSuccess).toHaveBeenCalledOnce()
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
test('thrown error → dispatchPreStreamError default 500', async () => {
|
|
1253
|
+
const { app } = buildApp((RPC) => {
|
|
1254
|
+
RPC.Create('Boom', { scope: 'b', version: 1 }, async () => { throw new Error('boom') })
|
|
1255
|
+
})
|
|
1256
|
+
|
|
1257
|
+
const res = await app.request('/b/boom/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
1258
|
+
expect(res.status).toBe(500)
|
|
1259
|
+
expect(await res.json()).toEqual({ error: 'boom' })
|
|
1260
|
+
})
|
|
1261
|
+
|
|
1262
|
+
test('AbortSignal is injected into ctx', async () => {
|
|
1263
|
+
let receivedSignal: AbortSignal | undefined
|
|
1264
|
+
const { app } = buildApp((RPC) => {
|
|
1265
|
+
RPC.Create('Sig', { scope: 's', version: 1 }, async (ctx) => {
|
|
1266
|
+
receivedSignal = ctx.signal
|
|
1267
|
+
return {}
|
|
1268
|
+
})
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
await app.request('/s/sig/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
1272
|
+
expect(receivedSignal).toBeInstanceOf(AbortSignal)
|
|
1273
|
+
})
|
|
1274
|
+
})
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1278
|
+
|
|
1279
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/rpc.test.ts`
|
|
1280
|
+
Expected: FAIL — `./rpc.js` not found.
|
|
1281
|
+
|
|
1282
|
+
- [ ] **Step 3: Implement `handlers/rpc.ts`**
|
|
1283
|
+
|
|
1284
|
+
Create `src/implementations/http/hono/handlers/rpc.ts`:
|
|
1285
|
+
|
|
1286
|
+
```ts
|
|
1287
|
+
import type { Context, Hono } from 'hono'
|
|
1288
|
+
import type { TProcedureRegistration } from '../../../../types.js'
|
|
1289
|
+
import type { RPCConfig } from '../../../types.js'
|
|
1290
|
+
import { dispatchPreStreamError } from '../../error-dispatch.js'
|
|
1291
|
+
import { buildRpcRouteDoc } from '../docs/rpc-doc.js'
|
|
1292
|
+
import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem } from '../types.js'
|
|
1293
|
+
|
|
1294
|
+
export function installRpcRoute(params: {
|
|
1295
|
+
app: Hono
|
|
1296
|
+
procedure: TProcedureRegistration<any, RPCConfig>
|
|
1297
|
+
factoryItem: HonoFactoryItem
|
|
1298
|
+
cfg: HonoAppBuilderConfig
|
|
1299
|
+
docs: DocAccumulator
|
|
1300
|
+
}): void {
|
|
1301
|
+
const { app, procedure, factoryItem, cfg, docs } = params
|
|
1302
|
+
|
|
1303
|
+
const route = buildRpcRouteDoc(
|
|
1304
|
+
procedure,
|
|
1305
|
+
cfg.pathPrefix,
|
|
1306
|
+
factoryItem.extendProcedureDoc as any,
|
|
1307
|
+
)
|
|
1308
|
+
docs.push(route)
|
|
1309
|
+
|
|
1310
|
+
app.post(route.path, async (c: Context) => {
|
|
1311
|
+
try {
|
|
1312
|
+
const context =
|
|
1313
|
+
typeof factoryItem.factoryContext === 'function'
|
|
1314
|
+
? await (factoryItem.factoryContext as (c: Context) => any)(c)
|
|
1315
|
+
: factoryItem.factoryContext
|
|
1316
|
+
|
|
1317
|
+
const body = await c.req.json().catch(() => ({}))
|
|
1318
|
+
const result = await procedure.handler(
|
|
1319
|
+
{ ...context, signal: c.req.raw.signal },
|
|
1320
|
+
body,
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
cfg.rpc?.onSuccess?.(procedure, c)
|
|
1324
|
+
|
|
1325
|
+
return c.json(result)
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
return dispatchPreStreamError({
|
|
1328
|
+
err: error,
|
|
1329
|
+
procedure,
|
|
1330
|
+
raw: c,
|
|
1331
|
+
cfg: {
|
|
1332
|
+
errors: cfg.errors,
|
|
1333
|
+
unknownError: cfg.unknownError,
|
|
1334
|
+
onError: cfg.onError,
|
|
1335
|
+
onRequestError: cfg.onRequestError,
|
|
1336
|
+
},
|
|
1337
|
+
})
|
|
1338
|
+
}
|
|
1339
|
+
})
|
|
1340
|
+
}
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
1344
|
+
|
|
1345
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/rpc.test.ts`
|
|
1346
|
+
Expected: PASS — 4 tests green.
|
|
1347
|
+
|
|
1348
|
+
- [ ] **Step 5: Commit**
|
|
1349
|
+
|
|
1350
|
+
```bash
|
|
1351
|
+
git add src/implementations/http/hono/handlers/rpc.ts src/implementations/http/hono/handlers/rpc.test.ts
|
|
1352
|
+
git commit -m "feat(hono): add installRpcRoute handler
|
|
1353
|
+
|
|
1354
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
---
|
|
1358
|
+
|
|
1359
|
+
### Task 10: `handlers/http.ts` — `installHttpRoute`
|
|
1360
|
+
|
|
1361
|
+
**Files:**
|
|
1362
|
+
- Create: `src/implementations/http/hono/handlers/http.ts`
|
|
1363
|
+
- Test: `src/implementations/http/hono/handlers/http.test.ts`
|
|
1364
|
+
|
|
1365
|
+
Implements the API/REST-style handler. Extracts structured params per channel (`pathParams`, `query`, `body`, `headers`), invokes the procedure, supports `{ body, headers }` and bare-result return shapes, and applies `successStatus` defaults.
|
|
1366
|
+
|
|
1367
|
+
- [ ] **Step 1: Write the failing test**
|
|
1368
|
+
|
|
1369
|
+
Create `src/implementations/http/hono/handlers/http.test.ts`:
|
|
1370
|
+
|
|
1371
|
+
```ts
|
|
1372
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
1373
|
+
import { Hono } from 'hono'
|
|
1374
|
+
import { Type } from 'typebox'
|
|
1375
|
+
import { Procedures } from '../../../../index.js'
|
|
1376
|
+
import { installHttpRoute } from './http.js'
|
|
1377
|
+
|
|
1378
|
+
function buildApp(setup: (P: ReturnType<typeof Procedures<any>>) => void, cfg: any = {}) {
|
|
1379
|
+
const P = Procedures<{ uid: string }>()
|
|
1380
|
+
setup(P)
|
|
1381
|
+
const app = new Hono()
|
|
1382
|
+
const docs: any[] = []
|
|
1383
|
+
for (const proc of P.getProcedures().values()) {
|
|
1384
|
+
if (proc.kind !== 'http') continue
|
|
1385
|
+
installHttpRoute({
|
|
1386
|
+
app,
|
|
1387
|
+
procedure: proc as any,
|
|
1388
|
+
factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
|
|
1389
|
+
cfg,
|
|
1390
|
+
docs,
|
|
1391
|
+
})
|
|
1392
|
+
}
|
|
1393
|
+
return { app, docs }
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
describe('installHttpRoute', () => {
|
|
1397
|
+
test('GET with pathParams and query', async () => {
|
|
1398
|
+
const { app, docs } = buildApp((P) => {
|
|
1399
|
+
P.CreateHttp('GetUser', {
|
|
1400
|
+
path: '/users/:id',
|
|
1401
|
+
method: 'get',
|
|
1402
|
+
scope: 'users',
|
|
1403
|
+
schema: {
|
|
1404
|
+
req: {
|
|
1405
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
1406
|
+
query: Type.Object({ include: Type.Optional(Type.String()) }),
|
|
1407
|
+
},
|
|
1408
|
+
res: { body: Type.Object({ id: Type.String(), include: Type.Optional(Type.String()) }) },
|
|
1409
|
+
},
|
|
1410
|
+
}, async (_ctx, { pathParams, query }) => ({ body: { id: pathParams.id, include: query.include } }))
|
|
1411
|
+
})
|
|
1412
|
+
|
|
1413
|
+
const res = await app.request('/users/abc?include=foo')
|
|
1414
|
+
expect(res.status).toBe(200)
|
|
1415
|
+
expect(await res.json()).toEqual({ id: 'abc', include: 'foo' })
|
|
1416
|
+
expect(docs[0].kind).toBe('api')
|
|
1417
|
+
expect(docs[0].fullPath).toBe('/users/:id')
|
|
1418
|
+
})
|
|
1419
|
+
|
|
1420
|
+
test('POST with body, default 201', async () => {
|
|
1421
|
+
const { app } = buildApp((P) => {
|
|
1422
|
+
P.CreateHttp('Create', {
|
|
1423
|
+
path: '/items',
|
|
1424
|
+
method: 'post',
|
|
1425
|
+
schema: {
|
|
1426
|
+
req: { body: Type.Object({ name: Type.String() }) },
|
|
1427
|
+
res: { body: Type.Object({ name: Type.String() }) },
|
|
1428
|
+
},
|
|
1429
|
+
}, async (_ctx, { body }) => ({ body }))
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
const res = await app.request('/items', {
|
|
1433
|
+
method: 'POST',
|
|
1434
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1435
|
+
body: JSON.stringify({ name: 'gizmo' }),
|
|
1436
|
+
})
|
|
1437
|
+
expect(res.status).toBe(201)
|
|
1438
|
+
expect(await res.json()).toEqual({ name: 'gizmo' })
|
|
1439
|
+
})
|
|
1440
|
+
|
|
1441
|
+
test('DELETE returns 204 with no body', async () => {
|
|
1442
|
+
const { app } = buildApp((P) => {
|
|
1443
|
+
P.CreateHttp('Delete', {
|
|
1444
|
+
path: '/items/:id',
|
|
1445
|
+
method: 'delete',
|
|
1446
|
+
schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
|
|
1447
|
+
}, async () => ({}))
|
|
1448
|
+
})
|
|
1449
|
+
|
|
1450
|
+
const res = await app.request('/items/x', { method: 'DELETE' })
|
|
1451
|
+
expect(res.status).toBe(204)
|
|
1452
|
+
expect(await res.text()).toBe('')
|
|
1453
|
+
})
|
|
1454
|
+
|
|
1455
|
+
test('headers in result are forwarded', async () => {
|
|
1456
|
+
const { app } = buildApp((P) => {
|
|
1457
|
+
P.CreateHttp('WithHeaders', {
|
|
1458
|
+
path: '/h', method: 'get',
|
|
1459
|
+
schema: { res: { body: Type.Object({ ok: Type.Boolean() }), headers: Type.Object({}) } },
|
|
1460
|
+
}, async () => ({ body: { ok: true }, headers: { 'x-trace': 'abc' } }))
|
|
1461
|
+
})
|
|
1462
|
+
const res = await app.request('/h')
|
|
1463
|
+
expect(res.headers.get('x-trace')).toBe('abc')
|
|
1464
|
+
expect(await res.json()).toEqual({ ok: true })
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
test('api.onSuccess fires after handler', async () => {
|
|
1468
|
+
const onSuccess = vi.fn()
|
|
1469
|
+
const { app } = buildApp((P) => {
|
|
1470
|
+
P.CreateHttp('Ping', { path: '/ping', method: 'get' }, async () => ({}))
|
|
1471
|
+
}, { api: { onSuccess } })
|
|
1472
|
+
|
|
1473
|
+
await app.request('/ping')
|
|
1474
|
+
expect(onSuccess).toHaveBeenCalledOnce()
|
|
1475
|
+
})
|
|
1476
|
+
|
|
1477
|
+
test('thrown error → dispatchPreStreamError default 500', async () => {
|
|
1478
|
+
const { app } = buildApp((P) => {
|
|
1479
|
+
P.CreateHttp('Boom', { path: '/boom', method: 'get' }, async () => { throw new Error('x') })
|
|
1480
|
+
})
|
|
1481
|
+
const res = await app.request('/boom')
|
|
1482
|
+
expect(res.status).toBe(500)
|
|
1483
|
+
expect(await res.json()).toEqual({ error: 'x' })
|
|
1484
|
+
})
|
|
1485
|
+
|
|
1486
|
+
test('custom queryParser is used', async () => {
|
|
1487
|
+
const queryParser = vi.fn((q: string) => ({ parsed: true, raw: q }))
|
|
1488
|
+
const { app } = buildApp((P) => {
|
|
1489
|
+
P.CreateHttp('Q', { path: '/q', method: 'get', schema: { req: { query: Type.Any() } } },
|
|
1490
|
+
async (_ctx, { query }) => ({ body: query }))
|
|
1491
|
+
}, { api: { queryParser } })
|
|
1492
|
+
|
|
1493
|
+
const res = await app.request('/q?a=1&b=2')
|
|
1494
|
+
expect(queryParser).toHaveBeenCalledWith('a=1&b=2')
|
|
1495
|
+
expect(await res.json()).toEqual({ parsed: true, raw: 'a=1&b=2' })
|
|
1496
|
+
})
|
|
1497
|
+
})
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1501
|
+
|
|
1502
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/http.test.ts`
|
|
1503
|
+
Expected: FAIL — `./http.js` not found.
|
|
1504
|
+
|
|
1505
|
+
- [ ] **Step 3: Implement `handlers/http.ts`**
|
|
1506
|
+
|
|
1507
|
+
Create `src/implementations/http/hono/handlers/http.ts`:
|
|
1508
|
+
|
|
1509
|
+
```ts
|
|
1510
|
+
import type { Context, Hono } from 'hono'
|
|
1511
|
+
import type { THttpProcedureRegistration } from '../../../../types.js'
|
|
1512
|
+
import type { HttpMethod } from '../../../types.js'
|
|
1513
|
+
import { dispatchPreStreamError } from '../../error-dispatch.js'
|
|
1514
|
+
import { buildHttpRouteDoc } from '../docs/http-doc.js'
|
|
1515
|
+
import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem, QueryParser } from '../types.js'
|
|
1516
|
+
|
|
1517
|
+
const BODY_METHODS: HttpMethod[] = ['post', 'put', 'patch']
|
|
1518
|
+
|
|
1519
|
+
function defaultSuccessStatus(method: HttpMethod): number {
|
|
1520
|
+
switch (method) {
|
|
1521
|
+
case 'post': return 201
|
|
1522
|
+
case 'delete': return 204
|
|
1523
|
+
default: return 200
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function parseQueryNative(queryString: string): Record<string, unknown> {
|
|
1528
|
+
const sp = new URLSearchParams(queryString)
|
|
1529
|
+
const result: Record<string, unknown> = {}
|
|
1530
|
+
for (const key of new Set(sp.keys())) {
|
|
1531
|
+
const values = sp.getAll(key)
|
|
1532
|
+
result[key] = values.length > 1 ? values : values[0]
|
|
1533
|
+
}
|
|
1534
|
+
return result
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function extractQuery(url: string, parser: QueryParser): Record<string, unknown> {
|
|
1538
|
+
const q = url.indexOf('?')
|
|
1539
|
+
if (q === -1) return {}
|
|
1540
|
+
const raw = url.slice(q + 1)
|
|
1541
|
+
return raw ? parser(raw) : {}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
async function extractParams(
|
|
1545
|
+
c: Context,
|
|
1546
|
+
method: HttpMethod,
|
|
1547
|
+
reqSchema: Record<string, unknown>,
|
|
1548
|
+
parser: QueryParser,
|
|
1549
|
+
): Promise<Record<string, unknown>> {
|
|
1550
|
+
const params: Record<string, unknown> = {}
|
|
1551
|
+
for (const channel of Object.keys(reqSchema)) {
|
|
1552
|
+
switch (channel) {
|
|
1553
|
+
case 'pathParams':
|
|
1554
|
+
params.pathParams = c.req.param()
|
|
1555
|
+
break
|
|
1556
|
+
case 'query':
|
|
1557
|
+
params.query = extractQuery(c.req.url, parser)
|
|
1558
|
+
break
|
|
1559
|
+
case 'body':
|
|
1560
|
+
if (BODY_METHODS.includes(method)) {
|
|
1561
|
+
params.body = await c.req.json().catch(() => ({}))
|
|
1562
|
+
}
|
|
1563
|
+
break
|
|
1564
|
+
case 'headers': {
|
|
1565
|
+
const obj: Record<string, string> = {}
|
|
1566
|
+
c.req.raw.headers.forEach((v, k) => { obj[k] = v })
|
|
1567
|
+
params.headers = obj
|
|
1568
|
+
break
|
|
1569
|
+
}
|
|
1570
|
+
default:
|
|
1571
|
+
params[channel] = undefined
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return params
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
export function installHttpRoute(params: {
|
|
1578
|
+
app: Hono
|
|
1579
|
+
procedure: THttpProcedureRegistration<any>
|
|
1580
|
+
factoryItem: HonoFactoryItem
|
|
1581
|
+
cfg: HonoAppBuilderConfig
|
|
1582
|
+
docs: DocAccumulator
|
|
1583
|
+
}): void {
|
|
1584
|
+
const { app, procedure, factoryItem, cfg, docs } = params
|
|
1585
|
+
const queryParser = cfg.api?.queryParser ?? parseQueryNative
|
|
1586
|
+
|
|
1587
|
+
const route = buildHttpRouteDoc(
|
|
1588
|
+
procedure,
|
|
1589
|
+
cfg.pathPrefix,
|
|
1590
|
+
factoryItem.extendProcedureDoc as any,
|
|
1591
|
+
)
|
|
1592
|
+
docs.push(route)
|
|
1593
|
+
|
|
1594
|
+
const successStatus = procedure.config.successStatus ?? defaultSuccessStatus(procedure.config.method)
|
|
1595
|
+
const reqSchema = procedure.config.schema?.req
|
|
1596
|
+
|
|
1597
|
+
app.on(procedure.config.method.toUpperCase(), route.fullPath, async (c: Context) => {
|
|
1598
|
+
try {
|
|
1599
|
+
const context =
|
|
1600
|
+
typeof factoryItem.factoryContext === 'function'
|
|
1601
|
+
? await (factoryItem.factoryContext as (c: Context) => any)(c)
|
|
1602
|
+
: factoryItem.factoryContext
|
|
1603
|
+
|
|
1604
|
+
const reqParams = reqSchema
|
|
1605
|
+
? await extractParams(c, procedure.config.method, reqSchema, queryParser)
|
|
1606
|
+
: undefined
|
|
1607
|
+
|
|
1608
|
+
const result = await procedure.handler(
|
|
1609
|
+
{ ...context, signal: c.req.raw.signal },
|
|
1610
|
+
reqParams,
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
cfg.api?.onSuccess?.(procedure, c)
|
|
1614
|
+
|
|
1615
|
+
// 204 No Content — no body, just optional headers
|
|
1616
|
+
if (successStatus === 204) {
|
|
1617
|
+
if (result && typeof result === 'object' && 'headers' in result && (result as any).headers) {
|
|
1618
|
+
for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
|
|
1619
|
+
c.header(k, v)
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return c.body(null, 204)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
let body: unknown = result
|
|
1626
|
+
let headers: Record<string, string> | undefined
|
|
1627
|
+
|
|
1628
|
+
if (result && typeof result === 'object' && 'body' in result && 'headers' in result) {
|
|
1629
|
+
body = (result as any).body
|
|
1630
|
+
headers = (result as any).headers as Record<string, string>
|
|
1631
|
+
} else if (result && typeof result === 'object' && 'headers' in result && !('body' in result)) {
|
|
1632
|
+
for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
|
|
1633
|
+
c.header(k, v)
|
|
1634
|
+
}
|
|
1635
|
+
return c.body(null, successStatus as any)
|
|
1636
|
+
} else if (result && typeof result === 'object' && 'body' in result && !('headers' in result)) {
|
|
1637
|
+
body = (result as any).body
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
if (headers) for (const [k, v] of Object.entries(headers)) c.header(k, v)
|
|
1641
|
+
return c.json(body, successStatus as any)
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
return dispatchPreStreamError({
|
|
1644
|
+
err: error,
|
|
1645
|
+
procedure,
|
|
1646
|
+
raw: c,
|
|
1647
|
+
cfg: {
|
|
1648
|
+
errors: cfg.errors,
|
|
1649
|
+
unknownError: cfg.unknownError,
|
|
1650
|
+
onError: cfg.onError,
|
|
1651
|
+
onRequestError: cfg.onRequestError,
|
|
1652
|
+
},
|
|
1653
|
+
})
|
|
1654
|
+
}
|
|
1655
|
+
})
|
|
1656
|
+
}
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
1660
|
+
|
|
1661
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/http.test.ts`
|
|
1662
|
+
Expected: PASS — 7 tests green.
|
|
1663
|
+
|
|
1664
|
+
- [ ] **Step 5: Commit**
|
|
1665
|
+
|
|
1666
|
+
```bash
|
|
1667
|
+
git add src/implementations/http/hono/handlers/http.ts src/implementations/http/hono/handlers/http.test.ts
|
|
1668
|
+
git commit -m "feat(hono): add installHttpRoute handler
|
|
1669
|
+
|
|
1670
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
---
|
|
1674
|
+
|
|
1675
|
+
### Task 11: `handlers/stream.ts` — `installRpcStreamRoute` + `sse` helper
|
|
1676
|
+
|
|
1677
|
+
**Files:**
|
|
1678
|
+
- Create: `src/implementations/http/hono/handlers/stream.ts`
|
|
1679
|
+
- Test: `src/implementations/http/hono/handlers/stream.test.ts`
|
|
1680
|
+
|
|
1681
|
+
Moves `sse(data, options)` from `hono-stream/index.ts` into this module. Pre-stream validates params, throws `ProcedureValidationError` (caught by `dispatchPreStreamError`). Mid-stream uses `dispatchMidStreamError`. Pumps SSE or text mode.
|
|
1682
|
+
|
|
1683
|
+
- [ ] **Step 1: Write the failing test**
|
|
1684
|
+
|
|
1685
|
+
Create `src/implementations/http/hono/handlers/stream.test.ts`:
|
|
1686
|
+
|
|
1687
|
+
```ts
|
|
1688
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
1689
|
+
import { Hono } from 'hono'
|
|
1690
|
+
import { Type } from 'typebox'
|
|
1691
|
+
import { Procedures } from '../../../../index.js'
|
|
1692
|
+
import type { RPCConfig } from '../../../types.js'
|
|
1693
|
+
import { installRpcStreamRoute } from './stream.js'
|
|
1694
|
+
|
|
1695
|
+
function buildApp(setup: (P: ReturnType<typeof Procedures<any, RPCConfig>>) => void, cfg: any = {}) {
|
|
1696
|
+
const P = Procedures<{ uid: string }, RPCConfig>()
|
|
1697
|
+
setup(P)
|
|
1698
|
+
const app = new Hono()
|
|
1699
|
+
const docs: any[] = []
|
|
1700
|
+
for (const proc of P.getProcedures().values()) {
|
|
1701
|
+
if (proc.kind !== 'rpc-stream') continue
|
|
1702
|
+
installRpcStreamRoute({
|
|
1703
|
+
app,
|
|
1704
|
+
procedure: proc as any,
|
|
1705
|
+
factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
|
|
1706
|
+
cfg,
|
|
1707
|
+
docs,
|
|
1708
|
+
streamMode: cfg.stream?.defaultStreamMode ?? 'sse',
|
|
1709
|
+
})
|
|
1710
|
+
}
|
|
1711
|
+
return { app, docs }
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
describe('installRpcStreamRoute', () => {
|
|
1715
|
+
test('SSE: yields are written as data events; final return as event:return', async () => {
|
|
1716
|
+
const { app, docs } = buildApp((P) => {
|
|
1717
|
+
P.CreateStream('Counts', { scope: 'c', version: 1 }, async function* () {
|
|
1718
|
+
yield 1
|
|
1719
|
+
yield 2
|
|
1720
|
+
return 'done'
|
|
1721
|
+
})
|
|
1722
|
+
})
|
|
1723
|
+
expect(docs[0].kind).toBe('stream')
|
|
1724
|
+
expect(docs[0].path).toBe('/c/counts/1')
|
|
1725
|
+
|
|
1726
|
+
const res = await app.request('/c/counts/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
1727
|
+
expect(res.status).toBe(200)
|
|
1728
|
+
const text = await res.text()
|
|
1729
|
+
expect(text).toContain('data: 1')
|
|
1730
|
+
expect(text).toContain('data: 2')
|
|
1731
|
+
expect(text).toContain('event: return')
|
|
1732
|
+
expect(text).toContain('data: "done"')
|
|
1733
|
+
})
|
|
1734
|
+
|
|
1735
|
+
test('mid-stream throw → dispatchMidStreamError default { error }', async () => {
|
|
1736
|
+
const { app } = buildApp((P) => {
|
|
1737
|
+
P.CreateStream('Boom', { scope: 'b', version: 1 }, async function* () {
|
|
1738
|
+
yield 'first'
|
|
1739
|
+
throw new Error('mid-boom')
|
|
1740
|
+
})
|
|
1741
|
+
})
|
|
1742
|
+
const res = await app.request('/b/boom/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
1743
|
+
const text = await res.text()
|
|
1744
|
+
expect(text).toContain('event: error')
|
|
1745
|
+
expect(text).toContain('"error":"mid-boom"')
|
|
1746
|
+
})
|
|
1747
|
+
|
|
1748
|
+
test('pre-stream validation error → 400 via dispatchPreStreamError', async () => {
|
|
1749
|
+
const { app } = buildApp((P) => {
|
|
1750
|
+
P.CreateStream('NeedsParams', {
|
|
1751
|
+
scope: 'n', version: 1,
|
|
1752
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
1753
|
+
}, async function* () { yield 'ok' })
|
|
1754
|
+
})
|
|
1755
|
+
const res = await app.request('/n/needs-params/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
1756
|
+
expect(res.status).toBe(400)
|
|
1757
|
+
const body = await res.json()
|
|
1758
|
+
expect(body.name).toBe('ProcedureValidationError')
|
|
1759
|
+
})
|
|
1760
|
+
|
|
1761
|
+
test('onStreamStart and onStreamEnd fire', async () => {
|
|
1762
|
+
const onStreamStart = vi.fn()
|
|
1763
|
+
const onStreamEnd = vi.fn()
|
|
1764
|
+
const { app } = buildApp((P) => {
|
|
1765
|
+
P.CreateStream('Ev', { scope: 'e', version: 1 }, async function* () { yield 'x' })
|
|
1766
|
+
}, { stream: { onStreamStart, onStreamEnd } })
|
|
1767
|
+
|
|
1768
|
+
await app.request('/e/ev/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
1769
|
+
expect(onStreamStart).toHaveBeenCalled()
|
|
1770
|
+
expect(onStreamEnd).toHaveBeenCalled()
|
|
1771
|
+
})
|
|
1772
|
+
})
|
|
1773
|
+
```
|
|
1774
|
+
|
|
1775
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1776
|
+
|
|
1777
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/stream.test.ts`
|
|
1778
|
+
Expected: FAIL — `./stream.js` not found.
|
|
1779
|
+
|
|
1780
|
+
- [ ] **Step 3: Implement `handlers/stream.ts`**
|
|
1781
|
+
|
|
1782
|
+
Create `src/implementations/http/hono/handlers/stream.ts`:
|
|
1783
|
+
|
|
1784
|
+
```ts
|
|
1785
|
+
import type { Context, Hono } from 'hono'
|
|
1786
|
+
import { streamSSE, streamText } from 'hono/streaming'
|
|
1787
|
+
import type { TStreamProcedureRegistration } from '../../../../types.js'
|
|
1788
|
+
import type { RPCConfig, StreamMode } from '../../../types.js'
|
|
1789
|
+
import { ProcedureValidationError } from '../../../../errors.js'
|
|
1790
|
+
import { dispatchMidStreamError, dispatchPreStreamError } from '../../error-dispatch.js'
|
|
1791
|
+
import { buildStreamRouteDoc } from '../docs/stream-doc.js'
|
|
1792
|
+
import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem } from '../types.js'
|
|
1793
|
+
|
|
1794
|
+
export type SSEOptions = {
|
|
1795
|
+
event?: string
|
|
1796
|
+
id?: string
|
|
1797
|
+
retry?: number
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const sseMetadata = new WeakMap<object, SSEOptions>()
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* Marks an object yield as an SSE event with custom metadata. Stream handlers
|
|
1804
|
+
* read this metadata to set the SSE `event:`, `id:`, and `retry:` fields.
|
|
1805
|
+
*/
|
|
1806
|
+
export function sse<T extends object>(data: T, options?: SSEOptions): T {
|
|
1807
|
+
sseMetadata.set(data, options ?? {})
|
|
1808
|
+
return data
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function getSSEMeta(value: unknown): SSEOptions | undefined {
|
|
1812
|
+
if (typeof value === 'object' && value !== null) {
|
|
1813
|
+
return sseMetadata.get(value as object)
|
|
1814
|
+
}
|
|
1815
|
+
return undefined
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
export function installRpcStreamRoute(params: {
|
|
1819
|
+
app: Hono
|
|
1820
|
+
procedure: TStreamProcedureRegistration<any, RPCConfig>
|
|
1821
|
+
factoryItem: HonoFactoryItem
|
|
1822
|
+
cfg: HonoAppBuilderConfig
|
|
1823
|
+
docs: DocAccumulator
|
|
1824
|
+
streamMode: StreamMode
|
|
1825
|
+
}): void {
|
|
1826
|
+
const { app, procedure, factoryItem, cfg, docs, streamMode } = params
|
|
1827
|
+
|
|
1828
|
+
const route = buildStreamRouteDoc(
|
|
1829
|
+
procedure,
|
|
1830
|
+
streamMode,
|
|
1831
|
+
cfg.pathPrefix,
|
|
1832
|
+
factoryItem.extendProcedureDoc as any,
|
|
1833
|
+
)
|
|
1834
|
+
docs.push(route)
|
|
1835
|
+
|
|
1836
|
+
const handler = async (c: Context) => {
|
|
1837
|
+
try {
|
|
1838
|
+
const context =
|
|
1839
|
+
typeof factoryItem.factoryContext === 'function'
|
|
1840
|
+
? await (factoryItem.factoryContext as (c: Context) => any)(c)
|
|
1841
|
+
: factoryItem.factoryContext
|
|
1842
|
+
|
|
1843
|
+
const reqParams =
|
|
1844
|
+
c.req.method === 'GET'
|
|
1845
|
+
? Object.fromEntries(new URL(c.req.url).searchParams)
|
|
1846
|
+
: await c.req.json().catch(() => ({}))
|
|
1847
|
+
|
|
1848
|
+
// Pre-stream validation — throw so the catch routes through dispatchPreStreamError.
|
|
1849
|
+
if ((procedure.config as any).validation?.params) {
|
|
1850
|
+
const { errors } = (procedure.config as any).validation.params(reqParams)
|
|
1851
|
+
if (errors) {
|
|
1852
|
+
throw new ProcedureValidationError(
|
|
1853
|
+
procedure.name,
|
|
1854
|
+
`Validation error for ${procedure.name}`,
|
|
1855
|
+
errors,
|
|
1856
|
+
)
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
cfg.stream?.onStreamStart?.(procedure, c, streamMode)
|
|
1861
|
+
|
|
1862
|
+
return streamMode === 'sse'
|
|
1863
|
+
? handleSSE(procedure, context, reqParams, c, cfg)
|
|
1864
|
+
: handleText(procedure, context, reqParams, c, cfg)
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
return dispatchPreStreamError({
|
|
1867
|
+
err: error,
|
|
1868
|
+
procedure,
|
|
1869
|
+
raw: c,
|
|
1870
|
+
cfg: {
|
|
1871
|
+
errors: cfg.errors,
|
|
1872
|
+
unknownError: cfg.unknownError,
|
|
1873
|
+
onError: cfg.onError,
|
|
1874
|
+
onRequestError: cfg.onRequestError,
|
|
1875
|
+
},
|
|
1876
|
+
})
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
app.get(route.path, handler)
|
|
1881
|
+
app.post(route.path, handler)
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function handleSSE(
|
|
1885
|
+
procedure: TStreamProcedureRegistration<any, RPCConfig>,
|
|
1886
|
+
context: any,
|
|
1887
|
+
reqParams: any,
|
|
1888
|
+
c: Context,
|
|
1889
|
+
cfg: HonoAppBuilderConfig,
|
|
1890
|
+
) {
|
|
1891
|
+
return streamSSE(c, async (stream) => {
|
|
1892
|
+
const generator = procedure.handler(
|
|
1893
|
+
{ ...context, signal: c.req.raw.signal, isPrevalidated: true } as any,
|
|
1894
|
+
reqParams,
|
|
1895
|
+
)
|
|
1896
|
+
stream.onAbort(async () => { await generator.return(undefined) })
|
|
1897
|
+
|
|
1898
|
+
let eventId = 0
|
|
1899
|
+
try {
|
|
1900
|
+
const iterator = generator[Symbol.asyncIterator]()
|
|
1901
|
+
let it = await iterator.next()
|
|
1902
|
+
|
|
1903
|
+
while (!it.done) {
|
|
1904
|
+
const value = it.value
|
|
1905
|
+
const meta = getSSEMeta(value)
|
|
1906
|
+
const data =
|
|
1907
|
+
typeof value === 'string' ? value
|
|
1908
|
+
: value != null ? JSON.stringify(value)
|
|
1909
|
+
: ''
|
|
1910
|
+
|
|
1911
|
+
await stream.writeSSE({
|
|
1912
|
+
data,
|
|
1913
|
+
event: meta?.event ?? procedure.name,
|
|
1914
|
+
id: meta?.id ?? String(eventId++),
|
|
1915
|
+
...(meta?.retry !== undefined && { retry: meta.retry }),
|
|
1916
|
+
})
|
|
1917
|
+
it = await iterator.next()
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (it.value !== undefined) {
|
|
1921
|
+
const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
|
|
1922
|
+
await stream.writeSSE({ data, event: 'return', id: String(eventId++) })
|
|
1923
|
+
}
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
const dispatched = await dispatchMidStreamError({
|
|
1926
|
+
err: error,
|
|
1927
|
+
procedure,
|
|
1928
|
+
raw: c,
|
|
1929
|
+
cfg: {
|
|
1930
|
+
errors: cfg.errors,
|
|
1931
|
+
unknownError: cfg.unknownError,
|
|
1932
|
+
onMidStreamError: cfg.stream?.onMidStreamError,
|
|
1933
|
+
onRequestError: cfg.onRequestError,
|
|
1934
|
+
},
|
|
1935
|
+
})
|
|
1936
|
+
|
|
1937
|
+
const meta = getSSEMeta(dispatched.data)
|
|
1938
|
+
await stream.writeSSE({
|
|
1939
|
+
data: typeof dispatched.data === 'string' ? dispatched.data : JSON.stringify(dispatched.data),
|
|
1940
|
+
event: meta?.event ?? dispatched.sseEvent ?? 'error',
|
|
1941
|
+
id: meta?.id ?? dispatched.sseId ?? String(eventId++),
|
|
1942
|
+
...((meta?.retry ?? dispatched.sseRetry) !== undefined && {
|
|
1943
|
+
retry: (meta?.retry ?? dispatched.sseRetry) as number,
|
|
1944
|
+
}),
|
|
1945
|
+
})
|
|
1946
|
+
|
|
1947
|
+
if (dispatched.runOnCatch) await dispatched.runOnCatch()
|
|
1948
|
+
} finally {
|
|
1949
|
+
cfg.stream?.onStreamEnd?.(procedure, c, 'sse')
|
|
1950
|
+
cfg.onRequestEnd?.(c)
|
|
1951
|
+
}
|
|
1952
|
+
})
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function handleText(
|
|
1956
|
+
procedure: TStreamProcedureRegistration<any, RPCConfig>,
|
|
1957
|
+
context: any,
|
|
1958
|
+
reqParams: any,
|
|
1959
|
+
c: Context,
|
|
1960
|
+
cfg: HonoAppBuilderConfig,
|
|
1961
|
+
) {
|
|
1962
|
+
return streamText(c, async (stream) => {
|
|
1963
|
+
const generator = procedure.handler(
|
|
1964
|
+
{ ...context, signal: c.req.raw.signal, isPrevalidated: true } as any,
|
|
1965
|
+
reqParams,
|
|
1966
|
+
)
|
|
1967
|
+
stream.onAbort(async () => { await generator.return(undefined) })
|
|
1968
|
+
|
|
1969
|
+
try {
|
|
1970
|
+
for await (const value of generator) {
|
|
1971
|
+
await stream.writeln(JSON.stringify(value))
|
|
1972
|
+
}
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
const dispatched = await dispatchMidStreamError({
|
|
1975
|
+
err: error,
|
|
1976
|
+
procedure,
|
|
1977
|
+
raw: c,
|
|
1978
|
+
cfg: {
|
|
1979
|
+
errors: cfg.errors,
|
|
1980
|
+
unknownError: cfg.unknownError,
|
|
1981
|
+
onMidStreamError: cfg.stream?.onMidStreamError,
|
|
1982
|
+
onRequestError: cfg.onRequestError,
|
|
1983
|
+
},
|
|
1984
|
+
})
|
|
1985
|
+
await stream.writeln(JSON.stringify(dispatched.data))
|
|
1986
|
+
if (dispatched.runOnCatch) await dispatched.runOnCatch()
|
|
1987
|
+
} finally {
|
|
1988
|
+
cfg.stream?.onStreamEnd?.(procedure, c, 'text')
|
|
1989
|
+
cfg.onRequestEnd?.(c)
|
|
1990
|
+
}
|
|
1991
|
+
})
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
export type { MidStreamErrorResult } from '../../error-dispatch.js'
|
|
1995
|
+
```
|
|
1996
|
+
|
|
1997
|
+
Now update the test file in `error-dispatch.test.ts` (Task 3) — change the placeholder import comment to actually import `sse` from this new module. Edit the file: replace the placeholder import line with `import { sse } from './hono/handlers/stream.js'`.
|
|
1998
|
+
|
|
1999
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
2000
|
+
|
|
2001
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/stream.test.ts src/implementations/http/error-dispatch.test.ts`
|
|
2002
|
+
Expected: PASS — 4 + 10 = 14 tests green.
|
|
2003
|
+
|
|
2004
|
+
- [ ] **Step 5: Commit**
|
|
2005
|
+
|
|
2006
|
+
```bash
|
|
2007
|
+
git add src/implementations/http/hono/handlers/stream.ts src/implementations/http/hono/handlers/stream.test.ts src/implementations/http/error-dispatch.test.ts
|
|
2008
|
+
git commit -m "feat(hono): add installRpcStreamRoute handler + sse helper
|
|
2009
|
+
|
|
2010
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2011
|
+
```
|
|
2012
|
+
|
|
2013
|
+
---
|
|
2014
|
+
|
|
2015
|
+
### Task 12: `handlers/http-stream.ts` — `installHttpStreamRoute`
|
|
2016
|
+
|
|
2017
|
+
**Files:**
|
|
2018
|
+
- Create: `src/implementations/http/hono/handlers/http-stream.ts`
|
|
2019
|
+
- Test: `src/implementations/http/hono/handlers/http-stream.test.ts`
|
|
2020
|
+
|
|
2021
|
+
Mounts http-stream procedures (developer-supplied path, `{ stream, initialHeaders }` return shape, SSE only). Adds mid-stream error handling that the existing `hono-api.ts` lacks — calls `dispatchMidStreamError` inside the SSE pump catch.
|
|
2022
|
+
|
|
2023
|
+
- [ ] **Step 1: Write the failing test**
|
|
2024
|
+
|
|
2025
|
+
Create `src/implementations/http/hono/handlers/http-stream.test.ts`:
|
|
2026
|
+
|
|
2027
|
+
```ts
|
|
2028
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2029
|
+
import { Hono } from 'hono'
|
|
2030
|
+
import { Type } from 'typebox'
|
|
2031
|
+
import { Procedures } from '../../../../index.js'
|
|
2032
|
+
import { installHttpStreamRoute } from './http-stream.js'
|
|
2033
|
+
|
|
2034
|
+
function buildApp(setup: (P: ReturnType<typeof Procedures<any>>) => void, cfg: any = {}) {
|
|
2035
|
+
const P = Procedures<{ uid: string }>()
|
|
2036
|
+
setup(P)
|
|
2037
|
+
const app = new Hono()
|
|
2038
|
+
const docs: any[] = []
|
|
2039
|
+
for (const proc of P.getProcedures().values()) {
|
|
2040
|
+
if (proc.kind !== 'http-stream') continue
|
|
2041
|
+
installHttpStreamRoute({
|
|
2042
|
+
app,
|
|
2043
|
+
procedure: proc as any,
|
|
2044
|
+
factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
|
|
2045
|
+
cfg,
|
|
2046
|
+
docs,
|
|
2047
|
+
})
|
|
2048
|
+
}
|
|
2049
|
+
return { app, docs }
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
describe('installHttpStreamRoute', () => {
|
|
2053
|
+
test('SSE pumping with initialHeaders and event:return', async () => {
|
|
2054
|
+
const { app, docs } = buildApp((P) => {
|
|
2055
|
+
P.CreateHttpStream('Tail', {
|
|
2056
|
+
path: '/logs/tail', method: 'get',
|
|
2057
|
+
schema: { yield: Type.Object({ line: Type.String() }) },
|
|
2058
|
+
}, async () => ({
|
|
2059
|
+
stream: (async function* () {
|
|
2060
|
+
yield { line: 'a' }
|
|
2061
|
+
yield { line: 'b' }
|
|
2062
|
+
return 'fin'
|
|
2063
|
+
})(),
|
|
2064
|
+
initialHeaders: { 'x-trace': 'abc' },
|
|
2065
|
+
}))
|
|
2066
|
+
})
|
|
2067
|
+
expect(docs[0].kind).toBe('http-stream')
|
|
2068
|
+
|
|
2069
|
+
const res = await app.request('/logs/tail')
|
|
2070
|
+
expect(res.headers.get('x-trace')).toBe('abc')
|
|
2071
|
+
const text = await res.text()
|
|
2072
|
+
expect(text).toContain('"line":"a"')
|
|
2073
|
+
expect(text).toContain('"line":"b"')
|
|
2074
|
+
expect(text).toContain('event: return')
|
|
2075
|
+
expect(text).toContain('"fin"')
|
|
2076
|
+
})
|
|
2077
|
+
|
|
2078
|
+
test('mid-stream throw → dispatchMidStreamError default { error }', async () => {
|
|
2079
|
+
const { app } = buildApp((P) => {
|
|
2080
|
+
P.CreateHttpStream('Boom', { path: '/boom', method: 'get' }, async () => ({
|
|
2081
|
+
stream: (async function* () {
|
|
2082
|
+
yield { ok: 1 }
|
|
2083
|
+
throw new Error('mid')
|
|
2084
|
+
})(),
|
|
2085
|
+
}))
|
|
2086
|
+
})
|
|
2087
|
+
const res = await app.request('/boom')
|
|
2088
|
+
const text = await res.text()
|
|
2089
|
+
expect(text).toContain('event: error')
|
|
2090
|
+
expect(text).toContain('"error":"mid"')
|
|
2091
|
+
})
|
|
2092
|
+
|
|
2093
|
+
test('pre-stream throw → dispatchPreStreamError 500', async () => {
|
|
2094
|
+
const { app } = buildApp((P) => {
|
|
2095
|
+
P.CreateHttpStream('Pre', { path: '/pre', method: 'get' }, async () => {
|
|
2096
|
+
throw new Error('pre')
|
|
2097
|
+
})
|
|
2098
|
+
})
|
|
2099
|
+
const res = await app.request('/pre')
|
|
2100
|
+
expect(res.status).toBe(500)
|
|
2101
|
+
expect(await res.json()).toEqual({ error: 'pre' })
|
|
2102
|
+
})
|
|
2103
|
+
})
|
|
2104
|
+
```
|
|
2105
|
+
|
|
2106
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
2107
|
+
|
|
2108
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/http-stream.test.ts`
|
|
2109
|
+
Expected: FAIL — `./http-stream.js` not found.
|
|
2110
|
+
|
|
2111
|
+
- [ ] **Step 3: Implement `handlers/http-stream.ts`**
|
|
2112
|
+
|
|
2113
|
+
Create `src/implementations/http/hono/handlers/http-stream.ts`:
|
|
2114
|
+
|
|
2115
|
+
```ts
|
|
2116
|
+
import type { Context, Hono } from 'hono'
|
|
2117
|
+
import { streamSSE } from 'hono/streaming'
|
|
2118
|
+
import type { THttpStreamProcedureRegistration } from '../../../../types.js'
|
|
2119
|
+
import type { HttpMethod } from '../../../types.js'
|
|
2120
|
+
import { dispatchMidStreamError, dispatchPreStreamError } from '../../error-dispatch.js'
|
|
2121
|
+
import { buildHttpStreamRouteDoc } from '../docs/http-stream-doc.js'
|
|
2122
|
+
import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem, QueryParser } from '../types.js'
|
|
2123
|
+
|
|
2124
|
+
const BODY_METHODS: HttpMethod[] = ['post', 'put', 'patch']
|
|
2125
|
+
|
|
2126
|
+
function parseQueryNative(queryString: string): Record<string, unknown> {
|
|
2127
|
+
const sp = new URLSearchParams(queryString)
|
|
2128
|
+
const result: Record<string, unknown> = {}
|
|
2129
|
+
for (const key of new Set(sp.keys())) {
|
|
2130
|
+
const values = sp.getAll(key)
|
|
2131
|
+
result[key] = values.length > 1 ? values : values[0]
|
|
2132
|
+
}
|
|
2133
|
+
return result
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
function extractQuery(url: string, parser: QueryParser): Record<string, unknown> {
|
|
2137
|
+
const q = url.indexOf('?')
|
|
2138
|
+
if (q === -1) return {}
|
|
2139
|
+
const raw = url.slice(q + 1)
|
|
2140
|
+
return raw ? parser(raw) : {}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
async function extractParams(
|
|
2144
|
+
c: Context,
|
|
2145
|
+
method: HttpMethod,
|
|
2146
|
+
reqSchema: Record<string, unknown>,
|
|
2147
|
+
parser: QueryParser,
|
|
2148
|
+
): Promise<Record<string, unknown>> {
|
|
2149
|
+
const params: Record<string, unknown> = {}
|
|
2150
|
+
for (const channel of Object.keys(reqSchema)) {
|
|
2151
|
+
switch (channel) {
|
|
2152
|
+
case 'pathParams':
|
|
2153
|
+
params.pathParams = c.req.param()
|
|
2154
|
+
break
|
|
2155
|
+
case 'query':
|
|
2156
|
+
params.query = extractQuery(c.req.url, parser)
|
|
2157
|
+
break
|
|
2158
|
+
case 'body':
|
|
2159
|
+
if (BODY_METHODS.includes(method)) {
|
|
2160
|
+
params.body = await c.req.json().catch(() => ({}))
|
|
2161
|
+
}
|
|
2162
|
+
break
|
|
2163
|
+
case 'headers': {
|
|
2164
|
+
const obj: Record<string, string> = {}
|
|
2165
|
+
c.req.raw.headers.forEach((v, k) => { obj[k] = v })
|
|
2166
|
+
params.headers = obj
|
|
2167
|
+
break
|
|
2168
|
+
}
|
|
2169
|
+
default:
|
|
2170
|
+
params[channel] = undefined
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
return params
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
export function installHttpStreamRoute(params: {
|
|
2177
|
+
app: Hono
|
|
2178
|
+
procedure: THttpStreamProcedureRegistration<any>
|
|
2179
|
+
factoryItem: HonoFactoryItem
|
|
2180
|
+
cfg: HonoAppBuilderConfig
|
|
2181
|
+
docs: DocAccumulator
|
|
2182
|
+
}): void {
|
|
2183
|
+
const { app, procedure, factoryItem, cfg, docs } = params
|
|
2184
|
+
const queryParser = cfg.api?.queryParser ?? parseQueryNative
|
|
2185
|
+
|
|
2186
|
+
const route = buildHttpStreamRouteDoc(
|
|
2187
|
+
procedure,
|
|
2188
|
+
cfg.pathPrefix,
|
|
2189
|
+
factoryItem.extendProcedureDoc as any,
|
|
2190
|
+
)
|
|
2191
|
+
docs.push(route)
|
|
2192
|
+
|
|
2193
|
+
const reqSchema = procedure.config.schema?.req
|
|
2194
|
+
|
|
2195
|
+
app.on(procedure.config.method.toUpperCase(), route.fullPath, async (c: Context) => {
|
|
2196
|
+
try {
|
|
2197
|
+
const context =
|
|
2198
|
+
typeof factoryItem.factoryContext === 'function'
|
|
2199
|
+
? await (factoryItem.factoryContext as (c: Context) => any)(c)
|
|
2200
|
+
: factoryItem.factoryContext
|
|
2201
|
+
|
|
2202
|
+
const reqParams = reqSchema
|
|
2203
|
+
? await extractParams(c, procedure.config.method, reqSchema as Record<string, unknown>, queryParser)
|
|
2204
|
+
: undefined
|
|
2205
|
+
|
|
2206
|
+
const { stream: gen, initialHeaders } = await procedure.handler(
|
|
2207
|
+
{ ...context, signal: c.req.raw.signal },
|
|
2208
|
+
reqParams,
|
|
2209
|
+
)
|
|
2210
|
+
|
|
2211
|
+
if (initialHeaders) {
|
|
2212
|
+
for (const [k, v] of Object.entries(initialHeaders)) c.header(k, v)
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
cfg.stream?.onStreamStart?.(procedure, c, 'sse')
|
|
2216
|
+
|
|
2217
|
+
return streamSSE(c, async (stream) => {
|
|
2218
|
+
let eventId = 0
|
|
2219
|
+
try {
|
|
2220
|
+
let it = await gen.next()
|
|
2221
|
+
while (!it.done) {
|
|
2222
|
+
const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
|
|
2223
|
+
await stream.writeSSE({ data, event: procedure.name, id: String(eventId++) })
|
|
2224
|
+
it = await gen.next()
|
|
2225
|
+
}
|
|
2226
|
+
if (it.value !== undefined) {
|
|
2227
|
+
const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
|
|
2228
|
+
await stream.writeSSE({ data, event: 'return', id: String(eventId++) })
|
|
2229
|
+
}
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
const dispatched = await dispatchMidStreamError({
|
|
2232
|
+
err: error,
|
|
2233
|
+
procedure,
|
|
2234
|
+
raw: c,
|
|
2235
|
+
cfg: {
|
|
2236
|
+
errors: cfg.errors,
|
|
2237
|
+
unknownError: cfg.unknownError,
|
|
2238
|
+
onMidStreamError: cfg.stream?.onMidStreamError,
|
|
2239
|
+
onRequestError: cfg.onRequestError,
|
|
2240
|
+
},
|
|
2241
|
+
})
|
|
2242
|
+
await stream.writeSSE({
|
|
2243
|
+
data: typeof dispatched.data === 'string' ? dispatched.data : JSON.stringify(dispatched.data),
|
|
2244
|
+
event: dispatched.sseEvent ?? 'error',
|
|
2245
|
+
id: String(eventId++),
|
|
2246
|
+
...(dispatched.sseRetry !== undefined && { retry: dispatched.sseRetry }),
|
|
2247
|
+
})
|
|
2248
|
+
if (dispatched.runOnCatch) await dispatched.runOnCatch()
|
|
2249
|
+
} finally {
|
|
2250
|
+
cfg.stream?.onStreamEnd?.(procedure, c, 'sse')
|
|
2251
|
+
cfg.onRequestEnd?.(c)
|
|
2252
|
+
}
|
|
2253
|
+
})
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
return dispatchPreStreamError({
|
|
2256
|
+
err: error,
|
|
2257
|
+
procedure,
|
|
2258
|
+
raw: c,
|
|
2259
|
+
cfg: {
|
|
2260
|
+
errors: cfg.errors,
|
|
2261
|
+
unknownError: cfg.unknownError,
|
|
2262
|
+
onError: cfg.onError,
|
|
2263
|
+
onRequestError: cfg.onRequestError,
|
|
2264
|
+
},
|
|
2265
|
+
})
|
|
2266
|
+
}
|
|
2267
|
+
})
|
|
2268
|
+
}
|
|
2269
|
+
```
|
|
2270
|
+
|
|
2271
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
2272
|
+
|
|
2273
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/http-stream.test.ts`
|
|
2274
|
+
Expected: PASS — 3 tests green.
|
|
2275
|
+
|
|
2276
|
+
- [ ] **Step 5: Commit**
|
|
2277
|
+
|
|
2278
|
+
```bash
|
|
2279
|
+
git add src/implementations/http/hono/handlers/http-stream.ts src/implementations/http/hono/handlers/http-stream.test.ts
|
|
2280
|
+
git commit -m "feat(hono): add installHttpStreamRoute handler
|
|
2281
|
+
|
|
2282
|
+
Adds mid-stream error handling that the existing hono-api http-stream
|
|
2283
|
+
branch was missing — generator throws now dispatch through
|
|
2284
|
+
dispatchMidStreamError instead of propagating into an unhandled SSE
|
|
2285
|
+
abort.
|
|
2286
|
+
|
|
2287
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2288
|
+
```
|
|
2289
|
+
|
|
2290
|
+
---
|
|
2291
|
+
|
|
2292
|
+
## Phase 4 — HonoAppBuilder Public Class
|
|
2293
|
+
|
|
2294
|
+
### Task 13: `index.ts` — class skeleton + `register` + lazy `docs`
|
|
2295
|
+
|
|
2296
|
+
**Files:**
|
|
2297
|
+
- Create: `src/implementations/http/hono/index.ts`
|
|
2298
|
+
- Test: `src/implementations/http/hono/index.test.ts`
|
|
2299
|
+
|
|
2300
|
+
- [ ] **Step 1: Write the failing test**
|
|
2301
|
+
|
|
2302
|
+
Create `src/implementations/http/hono/index.test.ts`:
|
|
2303
|
+
|
|
2304
|
+
```ts
|
|
2305
|
+
import { describe, expect, test } from 'vitest'
|
|
2306
|
+
import { Hono } from 'hono'
|
|
2307
|
+
import { Type } from 'typebox'
|
|
2308
|
+
import { Procedures } from '../../../index.js'
|
|
2309
|
+
import type { RPCConfig } from '../../types.js'
|
|
2310
|
+
import { HonoAppBuilder } from './index.js'
|
|
2311
|
+
|
|
2312
|
+
describe('HonoAppBuilder — public surface', () => {
|
|
2313
|
+
test('constructor with no args', () => {
|
|
2314
|
+
const b = new HonoAppBuilder()
|
|
2315
|
+
expect(b.app).toBeInstanceOf(Hono)
|
|
2316
|
+
expect(b.docs).toEqual([])
|
|
2317
|
+
expect(b.skippedProcedures).toEqual([])
|
|
2318
|
+
})
|
|
2319
|
+
|
|
2320
|
+
test('constructor accepts existing Hono app', () => {
|
|
2321
|
+
const custom = new Hono()
|
|
2322
|
+
const b = new HonoAppBuilder({ app: custom })
|
|
2323
|
+
expect(b.app).toBe(custom)
|
|
2324
|
+
})
|
|
2325
|
+
|
|
2326
|
+
test('register is chainable', () => {
|
|
2327
|
+
const b = new HonoAppBuilder()
|
|
2328
|
+
const P = Procedures<{}, RPCConfig>()
|
|
2329
|
+
const result = b.register(P, () => ({}))
|
|
2330
|
+
expect(result).toBe(b)
|
|
2331
|
+
})
|
|
2332
|
+
|
|
2333
|
+
test('docs is lazily computed before build()', () => {
|
|
2334
|
+
const b = new HonoAppBuilder()
|
|
2335
|
+
const P = Procedures<{}, RPCConfig>()
|
|
2336
|
+
P.Create('A', { scope: 's', version: 1 }, async () => ({}))
|
|
2337
|
+
b.register(P, () => ({}))
|
|
2338
|
+
|
|
2339
|
+
const docs1 = b.docs
|
|
2340
|
+
expect(docs1).toHaveLength(1)
|
|
2341
|
+
expect(docs1[0].kind).toBe('rpc')
|
|
2342
|
+
// Calling docs again returns the same cached array reference
|
|
2343
|
+
expect(b.docs).toBe(docs1)
|
|
2344
|
+
})
|
|
2345
|
+
|
|
2346
|
+
test('docs reads after build() return the same content', () => {
|
|
2347
|
+
const b = new HonoAppBuilder()
|
|
2348
|
+
const P = Procedures<{}, RPCConfig>()
|
|
2349
|
+
P.Create('A', { scope: 's', version: 1 }, async () => ({}))
|
|
2350
|
+
b.register(P, () => ({}))
|
|
2351
|
+
b.build()
|
|
2352
|
+
expect(b.docs).toHaveLength(1)
|
|
2353
|
+
})
|
|
2354
|
+
})
|
|
2355
|
+
```
|
|
2356
|
+
|
|
2357
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
2358
|
+
|
|
2359
|
+
Run: `npx vitest run src/implementations/http/hono/index.test.ts`
|
|
2360
|
+
Expected: FAIL — `./index.js` not found.
|
|
2361
|
+
|
|
2362
|
+
- [ ] **Step 3: Implement `index.ts` skeleton**
|
|
2363
|
+
|
|
2364
|
+
Create `src/implementations/http/hono/index.ts`:
|
|
2365
|
+
|
|
2366
|
+
```ts
|
|
2367
|
+
import { Context, Hono } from 'hono'
|
|
2368
|
+
import type {
|
|
2369
|
+
AnyHttpRouteDoc,
|
|
2370
|
+
ProceduresFactory,
|
|
2371
|
+
ExtractContext,
|
|
2372
|
+
StreamMode,
|
|
2373
|
+
} from '../../types.js'
|
|
2374
|
+
import { buildRpcRouteDoc } from './docs/rpc-doc.js'
|
|
2375
|
+
import { buildStreamRouteDoc } from './docs/stream-doc.js'
|
|
2376
|
+
import { buildHttpRouteDoc } from './docs/http-doc.js'
|
|
2377
|
+
import { buildHttpStreamRouteDoc } from './docs/http-stream-doc.js'
|
|
2378
|
+
import { installRpcRoute } from './handlers/rpc.js'
|
|
2379
|
+
import { installRpcStreamRoute } from './handlers/stream.js'
|
|
2380
|
+
import { installHttpRoute } from './handlers/http.js'
|
|
2381
|
+
import { installHttpStreamRoute } from './handlers/http-stream.js'
|
|
2382
|
+
import { makeRoutePath as _makeRoutePath } from './path.js'
|
|
2383
|
+
import type {
|
|
2384
|
+
HonoAppBuilderConfig,
|
|
2385
|
+
HonoFactoryItem,
|
|
2386
|
+
ExtendProcedureDoc,
|
|
2387
|
+
AnyProcedureRegistration,
|
|
2388
|
+
OnRequestErrorContext,
|
|
2389
|
+
} from './types.js'
|
|
2390
|
+
|
|
2391
|
+
export type { HonoAppBuilderConfig, OnRequestErrorContext, ExtendProcedureDoc } from './types.js'
|
|
2392
|
+
export type { AnyProcedureRegistration } from './types.js'
|
|
2393
|
+
export { sse } from './handlers/stream.js'
|
|
2394
|
+
export type { SSEOptions, MidStreamErrorResult } from './handlers/stream.js'
|
|
2395
|
+
export { defineErrorTaxonomy } from '../error-taxonomy.js'
|
|
2396
|
+
export type { ErrorTaxonomy, ErrorTaxonomyEntry, UnknownErrorConfig } from '../error-taxonomy.js'
|
|
2397
|
+
export type { QueryParser } from './types.js'
|
|
2398
|
+
|
|
2399
|
+
export class HonoAppBuilder<TStreamErrorData = unknown> {
|
|
2400
|
+
private readonly _app: Hono
|
|
2401
|
+
private readonly factories: HonoFactoryItem<any>[] = []
|
|
2402
|
+
private _docs: AnyHttpRouteDoc[] | null = null
|
|
2403
|
+
private _skipped: { name: string; reason: string }[] = []
|
|
2404
|
+
private _built = false
|
|
2405
|
+
|
|
2406
|
+
constructor(readonly config?: HonoAppBuilderConfig<TStreamErrorData>) {
|
|
2407
|
+
this._app = config?.app ?? new Hono()
|
|
2408
|
+
|
|
2409
|
+
if (config?.onRequestStart) {
|
|
2410
|
+
this._app.use('*', async (c, next) => {
|
|
2411
|
+
config.onRequestStart!(c)
|
|
2412
|
+
await next()
|
|
2413
|
+
})
|
|
2414
|
+
}
|
|
2415
|
+
if (config?.onRequestEnd) {
|
|
2416
|
+
this._app.use('*', async (c, next) => {
|
|
2417
|
+
await next()
|
|
2418
|
+
config.onRequestEnd!(c)
|
|
2419
|
+
})
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
static makeRoutePath = _makeRoutePath
|
|
2424
|
+
|
|
2425
|
+
get app(): Hono {
|
|
2426
|
+
return this._app
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
/**
|
|
2430
|
+
* Lazily computed on first read or `build()`. Computing without `build()`
|
|
2431
|
+
* lets tests and tooling introspect routes without spinning up handlers.
|
|
2432
|
+
*/
|
|
2433
|
+
get docs(): AnyHttpRouteDoc[] {
|
|
2434
|
+
if (this._docs === null) this.computeDocs()
|
|
2435
|
+
return this._docs!
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
get skippedProcedures(): { name: string; reason: string }[] {
|
|
2439
|
+
return this._skipped
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
register<TFactory extends ProceduresFactory>(
|
|
2443
|
+
factory: TFactory,
|
|
2444
|
+
factoryContext:
|
|
2445
|
+
| ExtractContext<TFactory>
|
|
2446
|
+
| ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
|
|
2447
|
+
options?: {
|
|
2448
|
+
streamMode?: StreamMode
|
|
2449
|
+
extendProcedureDoc?: ExtendProcedureDoc
|
|
2450
|
+
},
|
|
2451
|
+
): this {
|
|
2452
|
+
this.factories.push({
|
|
2453
|
+
factory,
|
|
2454
|
+
factoryContext,
|
|
2455
|
+
streamMode: options?.streamMode,
|
|
2456
|
+
extendProcedureDoc: options?.extendProcedureDoc,
|
|
2457
|
+
} as HonoFactoryItem<any>)
|
|
2458
|
+
// Invalidate the docs cache — next read recomputes.
|
|
2459
|
+
this._docs = null
|
|
2460
|
+
return this
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
private computeDocs(): void {
|
|
2464
|
+
const docs: AnyHttpRouteDoc[] = []
|
|
2465
|
+
const skipped: { name: string; reason: string }[] = []
|
|
2466
|
+
|
|
2467
|
+
for (const item of this.factories) {
|
|
2468
|
+
for (const procedure of item.factory.getProcedures().values() as Iterable<AnyProcedureRegistration>) {
|
|
2469
|
+
const prefix = this.config?.pathPrefix
|
|
2470
|
+
switch (procedure.kind) {
|
|
2471
|
+
case 'rpc':
|
|
2472
|
+
docs.push(buildRpcRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
|
|
2473
|
+
break
|
|
2474
|
+
case 'rpc-stream':
|
|
2475
|
+
docs.push(buildStreamRouteDoc(
|
|
2476
|
+
procedure as any,
|
|
2477
|
+
item.streamMode ?? this.config?.stream?.defaultStreamMode ?? 'sse',
|
|
2478
|
+
prefix,
|
|
2479
|
+
item.extendProcedureDoc as any,
|
|
2480
|
+
))
|
|
2481
|
+
break
|
|
2482
|
+
case 'http':
|
|
2483
|
+
docs.push(buildHttpRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
|
|
2484
|
+
break
|
|
2485
|
+
case 'http-stream':
|
|
2486
|
+
docs.push(buildHttpStreamRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
|
|
2487
|
+
break
|
|
2488
|
+
default: {
|
|
2489
|
+
const reason = `Unknown procedure kind "${(procedure as any).kind}"`
|
|
2490
|
+
skipped.push({ name: (procedure as any).name, reason })
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
this._docs = docs
|
|
2497
|
+
this._skipped = skipped
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
/**
|
|
2501
|
+
* Mounts every registered procedure on the Hono app and returns it.
|
|
2502
|
+
* Idempotent: calling twice is a no-op on the second call.
|
|
2503
|
+
*/
|
|
2504
|
+
build(): Hono {
|
|
2505
|
+
if (this._built) return this._app
|
|
2506
|
+
this._built = true
|
|
2507
|
+
|
|
2508
|
+
const docs: AnyHttpRouteDoc[] = []
|
|
2509
|
+
const skipped: { name: string; reason: string }[] = []
|
|
2510
|
+
const cfg = this.config ?? {}
|
|
2511
|
+
|
|
2512
|
+
for (const item of this.factories) {
|
|
2513
|
+
for (const procedure of item.factory.getProcedures().values() as Iterable<AnyProcedureRegistration>) {
|
|
2514
|
+
switch (procedure.kind) {
|
|
2515
|
+
case 'rpc':
|
|
2516
|
+
installRpcRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
|
|
2517
|
+
break
|
|
2518
|
+
case 'rpc-stream': {
|
|
2519
|
+
const streamMode = item.streamMode ?? cfg.stream?.defaultStreamMode ?? 'sse'
|
|
2520
|
+
installRpcStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs, streamMode })
|
|
2521
|
+
break
|
|
2522
|
+
}
|
|
2523
|
+
case 'http':
|
|
2524
|
+
installHttpRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
|
|
2525
|
+
break
|
|
2526
|
+
case 'http-stream':
|
|
2527
|
+
installHttpStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
|
|
2528
|
+
break
|
|
2529
|
+
default: {
|
|
2530
|
+
const reason = `Unknown procedure kind "${(procedure as any).kind}"`
|
|
2531
|
+
skipped.push({ name: (procedure as any).name, reason })
|
|
2532
|
+
console.warn(`[ts-procedures hono] Skipping procedure "${(procedure as any).name}": ${reason}`)
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
this._docs = docs
|
|
2539
|
+
this._skipped = skipped
|
|
2540
|
+
return this._app
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
```
|
|
2544
|
+
|
|
2545
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
2546
|
+
|
|
2547
|
+
Run: `npx vitest run src/implementations/http/hono/index.test.ts`
|
|
2548
|
+
Expected: PASS — 5 tests green.
|
|
2549
|
+
|
|
2550
|
+
- [ ] **Step 5: Commit**
|
|
2551
|
+
|
|
2552
|
+
```bash
|
|
2553
|
+
git add src/implementations/http/hono/index.ts src/implementations/http/hono/index.test.ts
|
|
2554
|
+
git commit -m "feat(hono): add HonoAppBuilder class with lazy docs + dispatch
|
|
2555
|
+
|
|
2556
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2557
|
+
```
|
|
2558
|
+
|
|
2559
|
+
---
|
|
2560
|
+
|
|
2561
|
+
### Task 14: Mixed-kind dispatch integration test + `toDocEnvelope`
|
|
2562
|
+
|
|
2563
|
+
**Files:**
|
|
2564
|
+
- Modify: `src/implementations/http/hono/index.ts` — add `toDocEnvelope`
|
|
2565
|
+
- Modify: `src/implementations/http/hono/index.test.ts`
|
|
2566
|
+
|
|
2567
|
+
Verifies that one factory containing all four procedure kinds dispatches correctly under one builder. Adds `toDocEnvelope()` mirroring `DocRegistry.toJSON()`.
|
|
2568
|
+
|
|
2569
|
+
- [ ] **Step 1: Append failing tests**
|
|
2570
|
+
|
|
2571
|
+
Append to `src/implementations/http/hono/index.test.ts`:
|
|
2572
|
+
|
|
2573
|
+
```ts
|
|
2574
|
+
import { Type } from 'typebox'
|
|
2575
|
+
import { Procedures } from '../../../index.js'
|
|
2576
|
+
import type { RPCConfig } from '../../types.js'
|
|
2577
|
+
import { defineErrorTaxonomy } from '../error-taxonomy.js'
|
|
2578
|
+
|
|
2579
|
+
describe('HonoAppBuilder — mixed-kind dispatch', () => {
|
|
2580
|
+
test('one factory with all four kinds mounts each at the correct path', async () => {
|
|
2581
|
+
const P = Procedures<{ uid: string }, RPCConfig>()
|
|
2582
|
+
P.Create('Echo', { scope: 'rpc', version: 1 }, async (_ctx, p) => p)
|
|
2583
|
+
P.CreateStream('Tail', { scope: 'rpc', version: 1 }, async function* () { yield 1 })
|
|
2584
|
+
P.CreateHttp('GetUser', { path: '/users/:id', method: 'get' }, async () => ({ body: { id: '1' } }))
|
|
2585
|
+
P.CreateHttpStream('Watch', { path: '/watch', method: 'get' }, async () => ({
|
|
2586
|
+
stream: (async function* () { yield 'tick' })(),
|
|
2587
|
+
}))
|
|
2588
|
+
|
|
2589
|
+
const app = new HonoAppBuilder()
|
|
2590
|
+
.register(P, () => ({ uid: 'u1' }))
|
|
2591
|
+
.build()
|
|
2592
|
+
|
|
2593
|
+
const rpc = await app.request('/rpc/echo/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
2594
|
+
expect(rpc.status).toBe(200)
|
|
2595
|
+
|
|
2596
|
+
const stream = await app.request('/rpc/tail/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
2597
|
+
expect(stream.status).toBe(200)
|
|
2598
|
+
|
|
2599
|
+
const http = await app.request('/users/1')
|
|
2600
|
+
expect(http.status).toBe(200)
|
|
2601
|
+
|
|
2602
|
+
const httpStream = await app.request('/watch')
|
|
2603
|
+
expect(httpStream.status).toBe(200)
|
|
2604
|
+
})
|
|
2605
|
+
|
|
2606
|
+
test('toDocEnvelope produces same shape as DocRegistry.toJSON', async () => {
|
|
2607
|
+
const { DocRegistry } = await import('../doc-registry.js')
|
|
2608
|
+
|
|
2609
|
+
const P = Procedures<{}, RPCConfig>()
|
|
2610
|
+
P.Create('Echo', { scope: 'e', version: 1 }, async () => ({}))
|
|
2611
|
+
|
|
2612
|
+
const errors = defineErrorTaxonomy({})
|
|
2613
|
+
const builder = new HonoAppBuilder({ pathPrefix: '/api', errors })
|
|
2614
|
+
builder.register(P, () => ({}))
|
|
2615
|
+
builder.build()
|
|
2616
|
+
|
|
2617
|
+
const builderEnvelope = builder.toDocEnvelope({ basePath: '/api', errors })
|
|
2618
|
+
const registryEnvelope = new DocRegistry({ basePath: '/api', errors }).from(builder).toJSON()
|
|
2619
|
+
|
|
2620
|
+
expect(builderEnvelope.basePath).toBe(registryEnvelope.basePath)
|
|
2621
|
+
expect(builderEnvelope.routes).toEqual(registryEnvelope.routes)
|
|
2622
|
+
expect(builderEnvelope.errors.map(e => e.name).sort()).toEqual(registryEnvelope.errors.map(e => e.name).sort())
|
|
2623
|
+
})
|
|
2624
|
+
|
|
2625
|
+
test('toDocEnvelope filter and transform options work', () => {
|
|
2626
|
+
const P = Procedures<{}, RPCConfig>()
|
|
2627
|
+
P.Create('A', { scope: 's', version: 1 }, async () => ({}))
|
|
2628
|
+
P.Create('B', { scope: 's', version: 1 }, async () => ({}))
|
|
2629
|
+
|
|
2630
|
+
const builder = new HonoAppBuilder().register(P, () => ({}))
|
|
2631
|
+
const onlyA = builder.toDocEnvelope({ filter: r => r.name === 'A' })
|
|
2632
|
+
expect(onlyA.routes).toHaveLength(1)
|
|
2633
|
+
expect(onlyA.routes[0].name).toBe('A')
|
|
2634
|
+
|
|
2635
|
+
const tagged = builder.toDocEnvelope({
|
|
2636
|
+
transform: env => ({ ...env, tagged: true }) as any,
|
|
2637
|
+
}) as any
|
|
2638
|
+
expect(tagged.tagged).toBe(true)
|
|
2639
|
+
})
|
|
2640
|
+
})
|
|
2641
|
+
```
|
|
2642
|
+
|
|
2643
|
+
- [ ] **Step 2: Add `toDocEnvelope` to `index.ts`**
|
|
2644
|
+
|
|
2645
|
+
First, add a static import to the top of `src/implementations/http/hono/index.ts` (alongside the other imports):
|
|
2646
|
+
|
|
2647
|
+
```ts
|
|
2648
|
+
import { DocRegistry } from '../doc-registry.js'
|
|
2649
|
+
import type { DocEnvelope, ErrorDoc, HeaderDoc } from '../../types.js'
|
|
2650
|
+
import type { ErrorTaxonomy } from '../error-taxonomy.js'
|
|
2651
|
+
```
|
|
2652
|
+
|
|
2653
|
+
Then append the method to the `HonoAppBuilder` class body:
|
|
2654
|
+
|
|
2655
|
+
```ts
|
|
2656
|
+
/**
|
|
2657
|
+
* Produces a {@link DocEnvelope} for single-app usage. For multi-app
|
|
2658
|
+
* aggregation, use {@link DocRegistry} and `from(builder)` instead.
|
|
2659
|
+
*
|
|
2660
|
+
* Mirrors `DocRegistry.toJSON()` options so the two paths produce
|
|
2661
|
+
* interchangeable envelopes for the codegen pipeline.
|
|
2662
|
+
*/
|
|
2663
|
+
toDocEnvelope<T = DocEnvelope>(options?: {
|
|
2664
|
+
basePath?: string
|
|
2665
|
+
errors?: ErrorTaxonomy | ErrorDoc[]
|
|
2666
|
+
includeDefaults?: boolean
|
|
2667
|
+
headers?: HeaderDoc[]
|
|
2668
|
+
filter?: (route: AnyHttpRouteDoc) => boolean
|
|
2669
|
+
transform?: (envelope: DocEnvelope) => T
|
|
2670
|
+
}): T {
|
|
2671
|
+
return new DocRegistry({
|
|
2672
|
+
basePath: options?.basePath ?? this.config?.pathPrefix,
|
|
2673
|
+
errors: options?.errors ?? this.config?.errors,
|
|
2674
|
+
includeDefaults: options?.includeDefaults,
|
|
2675
|
+
headers: options?.headers,
|
|
2676
|
+
}).from(this).toJSON<T>(options)
|
|
2677
|
+
}
|
|
2678
|
+
```
|
|
2679
|
+
|
|
2680
|
+
- [ ] **Step 3: Run tests to verify they pass**
|
|
2681
|
+
|
|
2682
|
+
Run: `npx vitest run src/implementations/http/hono/index.test.ts`
|
|
2683
|
+
Expected: PASS — 8 tests green (5 existing + 3 new).
|
|
2684
|
+
|
|
2685
|
+
- [ ] **Step 4: Commit**
|
|
2686
|
+
|
|
2687
|
+
```bash
|
|
2688
|
+
git add src/implementations/http/hono/index.ts src/implementations/http/hono/index.test.ts
|
|
2689
|
+
git commit -m "feat(hono): add toDocEnvelope + mixed-kind dispatch coverage
|
|
2690
|
+
|
|
2691
|
+
Delegates to DocRegistry internally so single-app and multi-app paths
|
|
2692
|
+
produce byte-identical envelopes for codegen.
|
|
2693
|
+
|
|
2694
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2695
|
+
```
|
|
2696
|
+
|
|
2697
|
+
---
|
|
2698
|
+
|
|
2699
|
+
## Phase 5 — Wire Up Exports
|
|
2700
|
+
|
|
2701
|
+
### Task 15: Add `./hono` to `package.json#exports`
|
|
2702
|
+
|
|
2703
|
+
**Files:**
|
|
2704
|
+
- Modify: `package.json`
|
|
2705
|
+
|
|
2706
|
+
- [ ] **Step 1: Edit `package.json`**
|
|
2707
|
+
|
|
2708
|
+
In `package.json`, add the new export after `./hono-stream` (which is still present — Phase 6 deletes it). Add this entry inside the `exports` object:
|
|
2709
|
+
|
|
2710
|
+
```json
|
|
2711
|
+
"./hono": {
|
|
2712
|
+
"types": "./build/implementations/http/hono/index.d.ts",
|
|
2713
|
+
"import": "./build/implementations/http/hono/index.js"
|
|
2714
|
+
},
|
|
2715
|
+
```
|
|
2716
|
+
|
|
2717
|
+
- [ ] **Step 2: Verify build**
|
|
2718
|
+
|
|
2719
|
+
Run: `npm run build`
|
|
2720
|
+
Expected: PASS — TypeScript compiles `src/implementations/http/hono/` into `build/implementations/http/hono/`.
|
|
2721
|
+
|
|
2722
|
+
- [ ] **Step 3: Commit**
|
|
2723
|
+
|
|
2724
|
+
```bash
|
|
2725
|
+
git add package.json
|
|
2726
|
+
git commit -m "chore(pkg): add ts-procedures/hono subpath export
|
|
2727
|
+
|
|
2728
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2729
|
+
```
|
|
2730
|
+
|
|
2731
|
+
---
|
|
2732
|
+
|
|
2733
|
+
## Phase 6 — Migrate Shared Tests
|
|
2734
|
+
|
|
2735
|
+
These four files at `src/implementations/http/` import from the soon-to-be-deleted directories. Retarget them at `HonoAppBuilder` BEFORE deleting the old directories so the test suite stays green throughout.
|
|
2736
|
+
|
|
2737
|
+
### Task 16: Retarget `on-request-error.test.ts`
|
|
2738
|
+
|
|
2739
|
+
**Files:**
|
|
2740
|
+
- Modify: `src/implementations/http/on-request-error.test.ts`
|
|
2741
|
+
|
|
2742
|
+
The current file has three `describe` blocks, one per old builder. Collapse to one `describe('onRequestError — HonoAppBuilder')` covering all kinds.
|
|
2743
|
+
|
|
2744
|
+
- [ ] **Step 1: Read the current file**
|
|
2745
|
+
|
|
2746
|
+
Run: `cat src/implementations/http/on-request-error.test.ts`
|
|
2747
|
+
Note the existing test cases — three describe blocks with similar shapes (rpc, api, stream).
|
|
2748
|
+
|
|
2749
|
+
- [ ] **Step 2: Rewrite imports and `boomApp` helpers**
|
|
2750
|
+
|
|
2751
|
+
Replace the imports section (currently importing from `./hono-rpc/index.js`, `./hono-api/index.js`, `./hono-stream/index.js`) with a single import:
|
|
2752
|
+
|
|
2753
|
+
```ts
|
|
2754
|
+
import { HonoAppBuilder } from './hono/index.js'
|
|
2755
|
+
```
|
|
2756
|
+
|
|
2757
|
+
Replace the three `boomApp` helpers with one parameterized helper that mounts a procedure of any kind:
|
|
2758
|
+
|
|
2759
|
+
```ts
|
|
2760
|
+
function boomApp(
|
|
2761
|
+
kind: 'rpc' | 'rpc-stream' | 'http' | 'http-stream',
|
|
2762
|
+
config: ConstructorParameters<typeof HonoAppBuilder>[0],
|
|
2763
|
+
) {
|
|
2764
|
+
const P = Procedures<{}, RPCConfig>()
|
|
2765
|
+
switch (kind) {
|
|
2766
|
+
case 'rpc':
|
|
2767
|
+
P.Create('Boom', { scope: 'b', version: 1 }, async () => { throw new Error('boom') })
|
|
2768
|
+
break
|
|
2769
|
+
case 'rpc-stream':
|
|
2770
|
+
P.CreateStream('Boom', { scope: 'b', version: 1 }, async function* () {
|
|
2771
|
+
throw new Error('boom')
|
|
2772
|
+
})
|
|
2773
|
+
break
|
|
2774
|
+
case 'http':
|
|
2775
|
+
P.CreateHttp('Boom', { path: '/boom', method: 'get' }, async () => { throw new Error('boom') })
|
|
2776
|
+
break
|
|
2777
|
+
case 'http-stream':
|
|
2778
|
+
P.CreateHttpStream('Boom', { path: '/boom', method: 'get' }, async () => { throw new Error('boom') })
|
|
2779
|
+
break
|
|
2780
|
+
}
|
|
2781
|
+
return new HonoAppBuilder(config).register(P, () => ({})).build()
|
|
2782
|
+
}
|
|
2783
|
+
```
|
|
2784
|
+
|
|
2785
|
+
Update each `describe` block to call `boomApp` with its kind parameter. The assertions stay the same — `onRequestError` should fire once per request, observer throws should be swallowed, etc.
|
|
2786
|
+
|
|
2787
|
+
- [ ] **Step 3: Run tests**
|
|
2788
|
+
|
|
2789
|
+
Run: `npx vitest run src/implementations/http/on-request-error.test.ts`
|
|
2790
|
+
Expected: PASS — all converted tests green.
|
|
2791
|
+
|
|
2792
|
+
- [ ] **Step 4: Commit**
|
|
2793
|
+
|
|
2794
|
+
```bash
|
|
2795
|
+
git add src/implementations/http/on-request-error.test.ts
|
|
2796
|
+
git commit -m "test(http): retarget on-request-error.test.ts at HonoAppBuilder
|
|
2797
|
+
|
|
2798
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2799
|
+
```
|
|
2800
|
+
|
|
2801
|
+
---
|
|
2802
|
+
|
|
2803
|
+
### Task 17: Retarget `doc-registry.test.ts`
|
|
2804
|
+
|
|
2805
|
+
**Files:**
|
|
2806
|
+
- Modify: `src/implementations/http/doc-registry.test.ts`
|
|
2807
|
+
|
|
2808
|
+
- [ ] **Step 1: Update imports and instantiations**
|
|
2809
|
+
|
|
2810
|
+
Replace `import { HonoRPCAppBuilder } from './hono-rpc/index.js'` and `import { HonoAPIAppBuilder } from './hono-api/index.js'` with:
|
|
2811
|
+
|
|
2812
|
+
```ts
|
|
2813
|
+
import { HonoAppBuilder } from './hono/index.js'
|
|
2814
|
+
```
|
|
2815
|
+
|
|
2816
|
+
Replace every `new HonoRPCAppBuilder()` and `new HonoAPIAppBuilder()` with `new HonoAppBuilder()`. The assertions don't need to change — both produce `DocSource`.
|
|
2817
|
+
|
|
2818
|
+
- [ ] **Step 2: Run tests**
|
|
2819
|
+
|
|
2820
|
+
Run: `npx vitest run src/implementations/http/doc-registry.test.ts`
|
|
2821
|
+
Expected: PASS — all tests still green.
|
|
2822
|
+
|
|
2823
|
+
- [ ] **Step 3: Commit**
|
|
2824
|
+
|
|
2825
|
+
```bash
|
|
2826
|
+
git add src/implementations/http/doc-registry.test.ts
|
|
2827
|
+
git commit -m "test(http): retarget doc-registry.test.ts at HonoAppBuilder
|
|
2828
|
+
|
|
2829
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2830
|
+
```
|
|
2831
|
+
|
|
2832
|
+
---
|
|
2833
|
+
|
|
2834
|
+
### Task 18: Retarget `route-errors.test.ts` and top-level `error-taxonomy.test.ts`
|
|
2835
|
+
|
|
2836
|
+
**Files:**
|
|
2837
|
+
- Modify: `src/implementations/http/route-errors.test.ts`
|
|
2838
|
+
- Modify: `src/implementations/http/error-taxonomy.test.ts`
|
|
2839
|
+
|
|
2840
|
+
- [ ] **Step 1: Inspect both files**
|
|
2841
|
+
|
|
2842
|
+
Run: `grep -nE "Hono(RPC|API|Stream)AppBuilder" src/implementations/http/route-errors.test.ts src/implementations/http/error-taxonomy.test.ts`
|
|
2843
|
+
Note every reference site.
|
|
2844
|
+
|
|
2845
|
+
- [ ] **Step 2: Retarget imports and instantiations**
|
|
2846
|
+
|
|
2847
|
+
In each file, replace old builder imports with `import { HonoAppBuilder } from './hono/index.js'` and update each `new HonoXAppBuilder()` to `new HonoAppBuilder()`. For tests using `onSuccess` flat at the constructor (rpc/api), wrap into `rpc: { onSuccess }` or `api: { onSuccess }` per kind. For tests using `onMidStreamError` flat at the constructor, wrap into `stream: { onMidStreamError }`.
|
|
2848
|
+
|
|
2849
|
+
- [ ] **Step 3: Run tests**
|
|
2850
|
+
|
|
2851
|
+
Run: `npx vitest run src/implementations/http/route-errors.test.ts src/implementations/http/error-taxonomy.test.ts`
|
|
2852
|
+
Expected: PASS.
|
|
2853
|
+
|
|
2854
|
+
- [ ] **Step 4: Commit**
|
|
2855
|
+
|
|
2856
|
+
```bash
|
|
2857
|
+
git add src/implementations/http/route-errors.test.ts src/implementations/http/error-taxonomy.test.ts
|
|
2858
|
+
git commit -m "test(http): retarget route-errors and error-taxonomy tests
|
|
2859
|
+
|
|
2860
|
+
Wrap flat builder hooks in stratified blocks (rpc.onSuccess,
|
|
2861
|
+
stream.onMidStreamError) to match the converged config shape.
|
|
2862
|
+
|
|
2863
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2864
|
+
```
|
|
2865
|
+
|
|
2866
|
+
---
|
|
2867
|
+
|
|
2868
|
+
## Phase 7 — Port Deep Test Suites
|
|
2869
|
+
|
|
2870
|
+
The three old test files (`hono-rpc/index.test.ts` 1129 lines, `hono-api/index.test.ts` 1070 lines, `hono-stream/index.test.ts` 1836 lines) cover hundreds of behaviors that the per-handler test files (Tasks 9–12) don't fully replicate. Port their distinctive cases into the per-handler test files before deletion so coverage is preserved.
|
|
2871
|
+
|
|
2872
|
+
### Task 19: Port `hono-rpc/index.test.ts` cases into `hono/handlers/rpc.test.ts`
|
|
2873
|
+
|
|
2874
|
+
**Files:**
|
|
2875
|
+
- Modify: `src/implementations/http/hono/handlers/rpc.test.ts`
|
|
2876
|
+
- Reference (read, do not modify): `src/implementations/http/hono-rpc/index.test.ts`
|
|
2877
|
+
- Reference: `src/implementations/http/hono-rpc/error-taxonomy.test.ts`
|
|
2878
|
+
|
|
2879
|
+
- [ ] **Step 1: Inventory cases worth porting**
|
|
2880
|
+
|
|
2881
|
+
Run: `grep -nE "test\(|it\(" src/implementations/http/hono-rpc/index.test.ts src/implementations/http/hono-rpc/error-taxonomy.test.ts | head -80`
|
|
2882
|
+
|
|
2883
|
+
Identify cases not already covered by the four base tests in `hono/handlers/rpc.test.ts`. Look for: pathPrefix variations, factoryContext variations (sync/async/static object), `extendProcedureDoc` callback, error taxonomy with user entries, `unknownError` fallback, observer-then-taxonomy ordering, validation errors, registration of multiple factories, and tests where `kind !== 'rpc'` procedures should be passed-through (no longer skipped under the unified builder).
|
|
2884
|
+
|
|
2885
|
+
- [ ] **Step 2: Port the missing cases**
|
|
2886
|
+
|
|
2887
|
+
For each case not yet covered, add a test in `hono/handlers/rpc.test.ts` using the existing `buildApp` helper. Wrap any constructor-flat hooks (`onSuccess`, `errors`, `onError`, `onRequestError`, `onRequestStart`, `onRequestEnd`) — `onSuccess` goes under `rpc: { onSuccess }`; the rest stay top-level.
|
|
2888
|
+
|
|
2889
|
+
For the "non-rpc procedures get skipped" tests (e.g., registering a stream procedure with the rpc builder), DELETE them — under the unified builder, mixed-kind factories work natively, so the skip behavior no longer applies. The `index.test.ts` mixed-kind dispatch test (Task 14) covers the new behavior.
|
|
2890
|
+
|
|
2891
|
+
For tests asserting on `builder.skippedProcedures`, retarget them to assert that the array is empty for known kinds — only "unknown future kind" entries should appear.
|
|
2892
|
+
|
|
2893
|
+
- [ ] **Step 3: Run the consolidated test file**
|
|
2894
|
+
|
|
2895
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/rpc.test.ts`
|
|
2896
|
+
Expected: PASS — all ported tests green.
|
|
2897
|
+
|
|
2898
|
+
- [ ] **Step 4: Commit**
|
|
2899
|
+
|
|
2900
|
+
```bash
|
|
2901
|
+
git add src/implementations/http/hono/handlers/rpc.test.ts
|
|
2902
|
+
git commit -m "test(hono): port hono-rpc test coverage into handlers/rpc.test.ts
|
|
2903
|
+
|
|
2904
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2905
|
+
```
|
|
2906
|
+
|
|
2907
|
+
---
|
|
2908
|
+
|
|
2909
|
+
### Task 20: Port `hono-api/index.test.ts` cases into `hono/handlers/http.test.ts`
|
|
2910
|
+
|
|
2911
|
+
**Files:**
|
|
2912
|
+
- Modify: `src/implementations/http/hono/handlers/http.test.ts`
|
|
2913
|
+
- Reference: `src/implementations/http/hono-api/index.test.ts`
|
|
2914
|
+
- Reference: `src/implementations/http/hono-api/error-taxonomy.test.ts`
|
|
2915
|
+
|
|
2916
|
+
Same approach as Task 19. The hono-api file also covers the http-stream branch (since hono-api today serves both http and http-stream procedures); split those cases between `handlers/http.test.ts` and `handlers/http-stream.test.ts` based on the procedure kind under test.
|
|
2917
|
+
|
|
2918
|
+
- [ ] **Step 1: Inventory and port to `handlers/http.test.ts`**
|
|
2919
|
+
|
|
2920
|
+
`grep -nE "test\(|it\(" src/implementations/http/hono-api/index.test.ts | head -60` — identify cases for `kind: 'http'` (developer paths, queryParser variants, all six HTTP methods, headers handling, `successStatus` overrides, response shape detection { body, headers } / bare body / headers-only, `extendProcedureDoc`, error taxonomy).
|
|
2921
|
+
|
|
2922
|
+
Wrap flat constructor knobs: `onSuccess` → `api.onSuccess`, `queryParser` → `api.queryParser`.
|
|
2923
|
+
|
|
2924
|
+
- [ ] **Step 2: Inventory and port to `handlers/http-stream.test.ts`**
|
|
2925
|
+
|
|
2926
|
+
In the same `hono-api` file, the http-stream cases are mixed into the same `describe('HonoAPIAppBuilder')`. Identify them by the procedure created via `CreateHttpStream` and port them into `handlers/http-stream.test.ts`.
|
|
2927
|
+
|
|
2928
|
+
- [ ] **Step 3: Run both files**
|
|
2929
|
+
|
|
2930
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/http.test.ts src/implementations/http/hono/handlers/http-stream.test.ts`
|
|
2931
|
+
Expected: PASS.
|
|
2932
|
+
|
|
2933
|
+
- [ ] **Step 4: Commit**
|
|
2934
|
+
|
|
2935
|
+
```bash
|
|
2936
|
+
git add src/implementations/http/hono/handlers/http.test.ts src/implementations/http/hono/handlers/http-stream.test.ts
|
|
2937
|
+
git commit -m "test(hono): port hono-api test coverage into handlers/http and handlers/http-stream
|
|
2938
|
+
|
|
2939
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2940
|
+
```
|
|
2941
|
+
|
|
2942
|
+
---
|
|
2943
|
+
|
|
2944
|
+
### Task 21: Port `hono-stream/index.test.ts` cases into `hono/handlers/stream.test.ts`
|
|
2945
|
+
|
|
2946
|
+
**Files:**
|
|
2947
|
+
- Modify: `src/implementations/http/hono/handlers/stream.test.ts`
|
|
2948
|
+
- Reference: `src/implementations/http/hono-stream/index.test.ts`
|
|
2949
|
+
- Reference: `src/implementations/http/hono-stream/error-taxonomy.test.ts`
|
|
2950
|
+
|
|
2951
|
+
Same approach. Cover: SSE vs text mode, `defaultStreamMode` global default vs per-register override (`register(F, ctx, { streamMode: 'text' })`), `sse(data, options)` helper for custom event/id/retry, mid-stream error variants, `onMidStreamError` returning custom data, abort signal propagation when client disconnects, `validateYields: true` behavior.
|
|
2952
|
+
|
|
2953
|
+
Wrap flat constructor knobs: `onMidStreamError` → `stream.onMidStreamError`, `defaultStreamMode` → `stream.defaultStreamMode`, `onStreamStart`/`onStreamEnd` → `stream.onStreamStart`/`stream.onStreamEnd`.
|
|
2954
|
+
|
|
2955
|
+
- [ ] **Step 1: Inventory and port**
|
|
2956
|
+
|
|
2957
|
+
Run the same inventory as Tasks 19–20.
|
|
2958
|
+
|
|
2959
|
+
- [ ] **Step 2: Add a test for the `register(F, ctx, { streamMode: 'text' })` per-factory override**
|
|
2960
|
+
|
|
2961
|
+
This is the converged shape's replacement for the old `register(F, ctx, { streamMode: 'text' })` on `HonoStreamAppBuilder`. Port the equivalent cases.
|
|
2962
|
+
|
|
2963
|
+
- [ ] **Step 3: Run**
|
|
2964
|
+
|
|
2965
|
+
Run: `npx vitest run src/implementations/http/hono/handlers/stream.test.ts`
|
|
2966
|
+
Expected: PASS.
|
|
2967
|
+
|
|
2968
|
+
- [ ] **Step 4: Commit**
|
|
2969
|
+
|
|
2970
|
+
```bash
|
|
2971
|
+
git add src/implementations/http/hono/handlers/stream.test.ts
|
|
2972
|
+
git commit -m "test(hono): port hono-stream test coverage into handlers/stream.test.ts
|
|
2973
|
+
|
|
2974
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
2975
|
+
```
|
|
2976
|
+
|
|
2977
|
+
---
|
|
2978
|
+
|
|
2979
|
+
## Phase 8 — Delete Old + Cleanup
|
|
2980
|
+
|
|
2981
|
+
### Task 22: Delete the three old builder directories + remove from `package.json#exports`
|
|
2982
|
+
|
|
2983
|
+
**Files:**
|
|
2984
|
+
- Delete: `src/implementations/http/hono-rpc/`
|
|
2985
|
+
- Delete: `src/implementations/http/hono-api/`
|
|
2986
|
+
- Delete: `src/implementations/http/hono-stream/`
|
|
2987
|
+
- Modify: `package.json`
|
|
2988
|
+
|
|
2989
|
+
- [ ] **Step 1: Confirm no remaining references**
|
|
2990
|
+
|
|
2991
|
+
Run: `grep -rEn "hono-rpc|hono-api|hono-stream" src/ docs/ agent_config/ CLAUDE.md README.md package.json 2>/dev/null | grep -v "build/" | grep -v "docs/superpowers/"`
|
|
2992
|
+
|
|
2993
|
+
Expected: only `package.json` (entries we're about to delete), comments in source files (Task 27 fixes those), and docs/agent_config files (Tasks 24–26 fix those). No imports.
|
|
2994
|
+
|
|
2995
|
+
If any imports remain, retarget them at `./hono/index.js` before proceeding.
|
|
2996
|
+
|
|
2997
|
+
- [ ] **Step 2: Delete the directories**
|
|
2998
|
+
|
|
2999
|
+
```bash
|
|
3000
|
+
rm -rf src/implementations/http/hono-rpc/
|
|
3001
|
+
rm -rf src/implementations/http/hono-api/
|
|
3002
|
+
rm -rf src/implementations/http/hono-stream/
|
|
3003
|
+
```
|
|
3004
|
+
|
|
3005
|
+
- [ ] **Step 3: Remove old subpath exports from `package.json`**
|
|
3006
|
+
|
|
3007
|
+
Open `package.json` and delete these three blocks from the `exports` object:
|
|
3008
|
+
|
|
3009
|
+
```json
|
|
3010
|
+
"./hono-rpc": {
|
|
3011
|
+
"types": "./build/implementations/http/hono-rpc/index.d.ts",
|
|
3012
|
+
"import": "./build/implementations/http/hono-rpc/index.js"
|
|
3013
|
+
},
|
|
3014
|
+
"./hono-stream": {
|
|
3015
|
+
"types": "./build/implementations/http/hono-stream/index.d.ts",
|
|
3016
|
+
"import": "./build/implementations/http/hono-stream/index.js"
|
|
3017
|
+
},
|
|
3018
|
+
"./hono-api": {
|
|
3019
|
+
"types": "./build/implementations/http/hono-api/index.d.ts",
|
|
3020
|
+
"import": "./build/implementations/http/hono-api/index.js"
|
|
3021
|
+
},
|
|
3022
|
+
```
|
|
3023
|
+
|
|
3024
|
+
- [ ] **Step 4: Run full suite + lint + build**
|
|
3025
|
+
|
|
3026
|
+
Run these in parallel:
|
|
3027
|
+
|
|
3028
|
+
```bash
|
|
3029
|
+
npm run test
|
|
3030
|
+
npm run lint
|
|
3031
|
+
npm run build
|
|
3032
|
+
```
|
|
3033
|
+
|
|
3034
|
+
Expected: all pass. If anything fails, the most likely cause is a forgotten import that wasn't caught in step 1 — fix it and re-run.
|
|
3035
|
+
|
|
3036
|
+
- [ ] **Step 5: Commit**
|
|
3037
|
+
|
|
3038
|
+
```bash
|
|
3039
|
+
git add -A
|
|
3040
|
+
git commit -m "$(cat <<'EOF'
|
|
3041
|
+
refactor(hono): remove old hono-rpc, hono-api, hono-stream builders
|
|
3042
|
+
|
|
3043
|
+
The three specialized builders are replaced by HonoAppBuilder which
|
|
3044
|
+
dispatches all four procedure kinds (rpc, rpc-stream, http,
|
|
3045
|
+
http-stream) by procedure.kind from a single register() call.
|
|
3046
|
+
|
|
3047
|
+
Removes ~1,580 lines of near-parallel dispatch/error/lifecycle code
|
|
3048
|
+
from src/implementations/http/.
|
|
3049
|
+
|
|
3050
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
3051
|
+
EOF
|
|
3052
|
+
)"
|
|
3053
|
+
```
|
|
3054
|
+
|
|
3055
|
+
---
|
|
3056
|
+
|
|
3057
|
+
### Task 23: Update source-comment references to old builder names
|
|
3058
|
+
|
|
3059
|
+
**Files:**
|
|
3060
|
+
- Modify: `src/types.ts` (lines around the `ProcedureKind` JSDoc)
|
|
3061
|
+
- Modify: `src/create-stream.ts` (comments referring to `HonoStreamAppBuilder`)
|
|
3062
|
+
- Modify: `src/implementations/http/doc-registry.ts` (comment about `HonoRPCAppBuilder`)
|
|
3063
|
+
|
|
3064
|
+
- [ ] **Step 1: Update `src/types.ts`**
|
|
3065
|
+
|
|
3066
|
+
In `src/types.ts`, find the `ProcedureKind` JSDoc block (around line 17–28) and replace the four "served by Hono*AppBuilder" lines with a single line:
|
|
3067
|
+
|
|
3068
|
+
```ts
|
|
3069
|
+
/**
|
|
3070
|
+
* Discriminant on every procedure registration that drives builder routing.
|
|
3071
|
+
*
|
|
3072
|
+
* - `'rpc'` — Create() registration
|
|
3073
|
+
* - `'rpc-stream'` — CreateStream() registration
|
|
3074
|
+
* - `'http'` — CreateHttp() registration
|
|
3075
|
+
* - `'http-stream'` — CreateHttpStream() registration
|
|
3076
|
+
*
|
|
3077
|
+
* `HonoAppBuilder` dispatches all four kinds from a single registration.
|
|
3078
|
+
* Use `kind` for any logic that needs to distinguish procedure shape; the
|
|
3079
|
+
* legacy `isStream` field on TStreamProcedureRegistration is kept for
|
|
3080
|
+
* back-compat but new code should branch on `kind`.
|
|
3081
|
+
*/
|
|
3082
|
+
```
|
|
3083
|
+
|
|
3084
|
+
- [ ] **Step 2: Update `src/create-stream.ts`**
|
|
3085
|
+
|
|
3086
|
+
In `src/create-stream.ts`, find the two comments referencing `HonoStreamAppBuilder` (around lines 97 and 160) and replace `HonoStreamAppBuilder` with `HonoAppBuilder`.
|
|
3087
|
+
|
|
3088
|
+
- [ ] **Step 3: Update `src/implementations/http/doc-registry.ts`**
|
|
3089
|
+
|
|
3090
|
+
In `src/implementations/http/doc-registry.ts`, find the comment around line 92 (`// streaming procedure registered with HonoRPCAppBuilder`) and replace `HonoRPCAppBuilder` with `HonoAppBuilder`. Update the surrounding sentence to reflect that under the unified builder, mismatches are now "unknown procedure kind" rather than "registered with the wrong builder".
|
|
3091
|
+
|
|
3092
|
+
- [ ] **Step 4: Verify build + lint**
|
|
3093
|
+
|
|
3094
|
+
Run: `npm run lint && npm run build`
|
|
3095
|
+
Expected: PASS.
|
|
3096
|
+
|
|
3097
|
+
- [ ] **Step 5: Commit**
|
|
3098
|
+
|
|
3099
|
+
```bash
|
|
3100
|
+
git add src/types.ts src/create-stream.ts src/implementations/http/doc-registry.ts
|
|
3101
|
+
git commit -m "docs(types): update JSDoc references from old builder names
|
|
3102
|
+
|
|
3103
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
3104
|
+
```
|
|
3105
|
+
|
|
3106
|
+
---
|
|
3107
|
+
|
|
3108
|
+
### Task 24: Update `src/implementations/http/README.md`
|
|
3109
|
+
|
|
3110
|
+
**Files:**
|
|
3111
|
+
- Modify: `src/implementations/http/README.md`
|
|
3112
|
+
|
|
3113
|
+
This README currently documents three separate builders. Rewrite around the single `HonoAppBuilder`.
|
|
3114
|
+
|
|
3115
|
+
- [ ] **Step 1: Read the current README**
|
|
3116
|
+
|
|
3117
|
+
Run: `cat src/implementations/http/README.md`
|
|
3118
|
+
Reference structure (sections to keep, sections to drop, sections to rewrite).
|
|
3119
|
+
|
|
3120
|
+
- [ ] **Step 2: Rewrite the README**
|
|
3121
|
+
|
|
3122
|
+
Replace the file's content with the structure below. The sections "Procedure Types," "Path Generation," "Context Resolution," "Abort Signal," "Error Handling," "Lifecycle Hooks," "Route Documentation," "DocRegistry," and "Client Code Generation" stay (with internal references updated). The sections "Available Implementations" and "Builder Pattern" (which currently enumerate three builders) collapse into one "HonoAppBuilder" section. The "One Hono Server, Multiple Builders" reference at the bottom is removed.
|
|
3123
|
+
|
|
3124
|
+
The Hono section's example should mirror the spec's Public API code block.
|
|
3125
|
+
|
|
3126
|
+
Update the table at the bottom mapping kind → params source → generated callable so it lists all four kinds (rpc, rpc-stream, http, http-stream) under HonoAppBuilder.
|
|
3127
|
+
|
|
3128
|
+
- [ ] **Step 3: Run docs-consistency check (if it exists)**
|
|
3129
|
+
|
|
3130
|
+
Run: `npm run check-docs`
|
|
3131
|
+
Expected: PASS — or note any issues for follow-up.
|
|
3132
|
+
|
|
3133
|
+
- [ ] **Step 4: Commit**
|
|
3134
|
+
|
|
3135
|
+
```bash
|
|
3136
|
+
git add src/implementations/http/README.md
|
|
3137
|
+
git commit -m "docs(http): rewrite README around HonoAppBuilder
|
|
3138
|
+
|
|
3139
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
3140
|
+
```
|
|
3141
|
+
|
|
3142
|
+
---
|
|
3143
|
+
|
|
3144
|
+
### Task 25: Update `docs/http-integrations.md`
|
|
3145
|
+
|
|
3146
|
+
**Files:**
|
|
3147
|
+
- Modify: `docs/http-integrations.md`
|
|
3148
|
+
|
|
3149
|
+
- [ ] **Step 1: Inspect the current doc**
|
|
3150
|
+
|
|
3151
|
+
Run: `head -200 docs/http-integrations.md` and `grep -nE "Hono(RPC|API|Stream)AppBuilder|One Hono Server" docs/http-integrations.md`
|
|
3152
|
+
|
|
3153
|
+
- [ ] **Step 2: Rewrite the doc**
|
|
3154
|
+
|
|
3155
|
+
- Replace every `HonoRPCAppBuilder`, `HonoAPIAppBuilder`, `HonoStreamAppBuilder` reference with `HonoAppBuilder`.
|
|
3156
|
+
- Rewrite all example code to use the unified builder + stratified config.
|
|
3157
|
+
- Delete the section `## One Hono Server, Multiple Builders` entirely (it's no longer relevant).
|
|
3158
|
+
- Update the error-handling section: keep the spec narrative (declarative + imperative peers, observer); update the "Hono-stream coverage: pre-stream path only" note to reflect that mid-stream errors now also dispatch through the taxonomy in http-stream (not just rpc-stream).
|
|
3159
|
+
|
|
3160
|
+
- [ ] **Step 3: Run docs-consistency check**
|
|
3161
|
+
|
|
3162
|
+
Run: `npm run check-docs`
|
|
3163
|
+
Expected: PASS.
|
|
3164
|
+
|
|
3165
|
+
- [ ] **Step 4: Commit**
|
|
3166
|
+
|
|
3167
|
+
```bash
|
|
3168
|
+
git add docs/http-integrations.md
|
|
3169
|
+
git commit -m "docs: rewrite http-integrations around HonoAppBuilder
|
|
3170
|
+
|
|
3171
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
3172
|
+
```
|
|
3173
|
+
|
|
3174
|
+
---
|
|
3175
|
+
|
|
3176
|
+
### Task 26: Update agent_config templates and skills
|
|
3177
|
+
|
|
3178
|
+
**Files:**
|
|
3179
|
+
- Delete: `agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.ts`
|
|
3180
|
+
- Delete: `agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.ts`
|
|
3181
|
+
- Modify (rename): `agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.ts` → `hono.ts`
|
|
3182
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md`
|
|
3183
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures/SKILL.md`
|
|
3184
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures/patterns.md`
|
|
3185
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures/anti-patterns.md`
|
|
3186
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures/api-reference.md`
|
|
3187
|
+
- Modify: `agent_config/copilot/copilot-instructions.md`
|
|
3188
|
+
- Modify: `agent_config/cursor/cursorrules`
|
|
3189
|
+
|
|
3190
|
+
- [ ] **Step 1: Inspect existing templates and skills**
|
|
3191
|
+
|
|
3192
|
+
```bash
|
|
3193
|
+
ls agent_config/claude-code/skills/ts-procedures-scaffold/templates/
|
|
3194
|
+
grep -lE "Hono(RPC|API|Stream)AppBuilder" agent_config/
|
|
3195
|
+
```
|
|
3196
|
+
|
|
3197
|
+
- [ ] **Step 2: Replace the templates**
|
|
3198
|
+
|
|
3199
|
+
Delete `hono-rpc.ts` and `hono-stream.ts`. Rename `hono-api.ts` to `hono.ts` and rewrite it to demonstrate one builder serving all four kinds — pattern from the spec's Public API block. Keep the `Procedures<Ctx>()` factory + `Create`/`CreateStream`/`CreateHttp`/`CreateHttpStream` examples + a single `new HonoAppBuilder(...)` registration.
|
|
3200
|
+
|
|
3201
|
+
- [ ] **Step 3: Update skill docs**
|
|
3202
|
+
|
|
3203
|
+
In each of the listed skill / instruction files, replace every reference to `HonoRPCAppBuilder` / `HonoAPIAppBuilder` / `HonoStreamAppBuilder` with `HonoAppBuilder`, and rewrite any code examples to use the stratified config shape. Remove any "use this builder for this kind" stratification — replace with a "which procedure kind to use" decision tree.
|
|
3204
|
+
|
|
3205
|
+
- [ ] **Step 4: Verify scaffolding still works**
|
|
3206
|
+
|
|
3207
|
+
Run: `node ./agent_config/bin/setup.mjs --dry-run`
|
|
3208
|
+
Expected: PASS — dry run reports the new file structure with no missing template references.
|
|
3209
|
+
|
|
3210
|
+
- [ ] **Step 5: Commit**
|
|
3211
|
+
|
|
3212
|
+
```bash
|
|
3213
|
+
git add agent_config/
|
|
3214
|
+
git commit -m "$(cat <<'EOF'
|
|
3215
|
+
docs(agent_config): consolidate hono templates around HonoAppBuilder
|
|
3216
|
+
|
|
3217
|
+
Delete hono-rpc.ts and hono-stream.ts templates; rename hono-api.ts to
|
|
3218
|
+
hono.ts and rewrite around one builder serving all four kinds. Update
|
|
3219
|
+
skills, patterns, copilot instructions, and cursor rules to match.
|
|
3220
|
+
|
|
3221
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
3222
|
+
EOF
|
|
3223
|
+
)"
|
|
3224
|
+
```
|
|
3225
|
+
|
|
3226
|
+
---
|
|
3227
|
+
|
|
3228
|
+
### Task 27: Update `CLAUDE.md` and `README.md`
|
|
3229
|
+
|
|
3230
|
+
**Files:**
|
|
3231
|
+
- Modify: `CLAUDE.md`
|
|
3232
|
+
- Modify: `README.md`
|
|
3233
|
+
|
|
3234
|
+
- [ ] **Step 1: Update `CLAUDE.md`**
|
|
3235
|
+
|
|
3236
|
+
In the "Key Files" section (around the `src/implementations/http/` entries), replace the three separate hono builder lines with a single line:
|
|
3237
|
+
|
|
3238
|
+
```
|
|
3239
|
+
- `src/implementations/http/hono/` - Unified Hono builder (`HonoAppBuilder`) — dispatches all four procedure kinds from one register() call. Subpath: `ts-procedures/hono`.
|
|
3240
|
+
- `src/implementations/http/error-dispatch.ts` - Shared dispatchPreStreamError + dispatchMidStreamError helpers used by HonoAppBuilder.
|
|
3241
|
+
```
|
|
3242
|
+
|
|
3243
|
+
In the "Builder Pattern" section, update text to describe one builder; remove the "Single Hono server across builders" callout (now built-in).
|
|
3244
|
+
|
|
3245
|
+
In the "Generated client code" sections, update any references to old builder names.
|
|
3246
|
+
|
|
3247
|
+
- [ ] **Step 2: Update `README.md`**
|
|
3248
|
+
|
|
3249
|
+
Find the v8 changelog/migration section and add an entry summarizing the convergence:
|
|
3250
|
+
|
|
3251
|
+
```
|
|
3252
|
+
### Hono builders converged into HonoAppBuilder (v8)
|
|
3253
|
+
|
|
3254
|
+
The three Hono builders — `HonoRPCAppBuilder`, `HonoAPIAppBuilder`,
|
|
3255
|
+
`HonoStreamAppBuilder` — are replaced by a single `HonoAppBuilder` that
|
|
3256
|
+
accepts any `Procedures<>()` factory and dispatches all four procedure
|
|
3257
|
+
kinds (`rpc`, `rpc-stream`, `http`, `http-stream`) from one
|
|
3258
|
+
registration call.
|
|
3259
|
+
|
|
3260
|
+
Migration:
|
|
3261
|
+
- `import { HonoAppBuilder } from 'ts-procedures/hono'`
|
|
3262
|
+
- Wrap kind-specific knobs in stratified blocks: `rpc.onSuccess`,
|
|
3263
|
+
`api.queryParser`, `api.onSuccess`, `stream.defaultStreamMode`,
|
|
3264
|
+
`stream.onMidStreamError`, `stream.onStreamStart`, `stream.onStreamEnd`.
|
|
3265
|
+
- Single-app users can call `builder.toDocEnvelope()` instead of going
|
|
3266
|
+
through `DocRegistry`. Multi-app users keep using `DocRegistry`.
|
|
3267
|
+
```
|
|
3268
|
+
|
|
3269
|
+
- [ ] **Step 3: Verify check-docs**
|
|
3270
|
+
|
|
3271
|
+
Run: `npm run check-docs`
|
|
3272
|
+
Expected: PASS.
|
|
3273
|
+
|
|
3274
|
+
- [ ] **Step 4: Commit**
|
|
3275
|
+
|
|
3276
|
+
```bash
|
|
3277
|
+
git add CLAUDE.md README.md
|
|
3278
|
+
git commit -m "docs: update CLAUDE.md and README.md for HonoAppBuilder convergence
|
|
3279
|
+
|
|
3280
|
+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
|
3281
|
+
```
|
|
3282
|
+
|
|
3283
|
+
---
|
|
3284
|
+
|
|
3285
|
+
## Phase 9 — Final Verification
|
|
3286
|
+
|
|
3287
|
+
### Task 28: Codegen end-to-end + full suite
|
|
3288
|
+
|
|
3289
|
+
**Files:** none — verification only
|
|
3290
|
+
|
|
3291
|
+
- [ ] **Step 1: Run full test suite, lint, build**
|
|
3292
|
+
|
|
3293
|
+
```bash
|
|
3294
|
+
npm run test
|
|
3295
|
+
npm run lint
|
|
3296
|
+
npm run build
|
|
3297
|
+
```
|
|
3298
|
+
|
|
3299
|
+
Expected: all PASS.
|
|
3300
|
+
|
|
3301
|
+
- [ ] **Step 2: Verify codegen against the new builder**
|
|
3302
|
+
|
|
3303
|
+
The codegen e2e fixtures consume `DocEnvelope` JSON. Build a small inline harness that:
|
|
3304
|
+
1. Sets up a `HonoAppBuilder` with one factory containing all four kinds.
|
|
3305
|
+
2. Calls `builder.toDocEnvelope({ basePath: '/api' })`.
|
|
3306
|
+
3. Passes the envelope to `generateClient({ envelope, outDir, dryRun: true, target: 'ts' })`.
|
|
3307
|
+
4. Asserts the dry-run output includes scope files for each kind.
|
|
3308
|
+
|
|
3309
|
+
If a similar test already exists in `src/codegen/`, run it: `npx vitest run src/codegen/` and confirm all green.
|
|
3310
|
+
|
|
3311
|
+
- [ ] **Step 3: Build typecheck of generated output (optional sanity check)**
|
|
3312
|
+
|
|
3313
|
+
If the existing codegen e2e test (e.g., `src/codegen/e2e-compile.test.ts`) compiles generated TypeScript via `tsc`, run it and confirm green. This validates that the new builder's `toDocEnvelope()` produces a codegen-compatible envelope.
|
|
3314
|
+
|
|
3315
|
+
```bash
|
|
3316
|
+
grep -lE "e2e|compile" src/codegen/*.test.ts
|
|
3317
|
+
```
|
|
3318
|
+
|
|
3319
|
+
If found: `npx vitest run <path>` and confirm pass.
|
|
3320
|
+
|
|
3321
|
+
- [ ] **Step 4: Final smoke summary**
|
|
3322
|
+
|
|
3323
|
+
Verify the work is complete by checking:
|
|
3324
|
+
- `src/implementations/http/hono-rpc/`, `hono-api/`, `hono-stream/` no longer exist
|
|
3325
|
+
- `src/implementations/http/hono/` exists with the layout from the File Structure section
|
|
3326
|
+
- `src/implementations/http/error-dispatch.ts` exists with both dispatchers
|
|
3327
|
+
- `package.json#exports` has `./hono` and lacks `./hono-rpc`, `./hono-api`, `./hono-stream`
|
|
3328
|
+
- `npm run test` passes
|
|
3329
|
+
- `npm run lint` passes
|
|
3330
|
+
- `npm run build` passes
|
|
3331
|
+
- `npm run check-docs` passes (if available)
|
|
3332
|
+
- `grep -rEn "Hono(RPC|API|Stream)AppBuilder" src/ docs/ agent_config/ CLAUDE.md README.md` returns zero hits in non-build files
|
|
3333
|
+
|
|
3334
|
+
If all eight checks pass, the convergence is complete.
|
|
3335
|
+
|
|
3336
|
+
- [ ] **Step 5: Optional — final summary commit (only if there are stray artifacts)**
|
|
3337
|
+
|
|
3338
|
+
If the verification surfaced any small fixes (typo, leftover reference), commit them in a single `chore(convergence): final cleanup` commit. Otherwise, no commit needed for verification.
|
|
3339
|
+
|
|
3340
|
+
---
|
|
3341
|
+
|
|
3342
|
+
## Self-Review Notes
|
|
3343
|
+
|
|
3344
|
+
**Spec coverage check (run before submitting):**
|
|
3345
|
+
|
|
3346
|
+
| Spec section | Plan task(s) |
|
|
3347
|
+
|---|---|
|
|
3348
|
+
| Public API surface (HonoAppBuilder, register, build, docs, toDocEnvelope) | Tasks 13–14 |
|
|
3349
|
+
| Stratified config (rpc, api, stream blocks) | Tasks 8, 13 |
|
|
3350
|
+
| Internal architecture (handler/doc layout) | Tasks 4–12 |
|
|
3351
|
+
| Shared error-dispatch | Tasks 2–3 |
|
|
3352
|
+
| Lazy doc computation | Task 13 |
|
|
3353
|
+
| Mixed-kind factories work natively | Task 14 (mixed-kind dispatch test) |
|
|
3354
|
+
| `pathPrefix` global | Task 1 (path test cases) |
|
|
3355
|
+
| `extendProcedureDoc` discriminated union | Task 8 (types), Task 19 (port test) |
|
|
3356
|
+
| Per-factory `streamMode` override | Task 21 (stream test cases) |
|
|
3357
|
+
| `AbortSignal` injection preserved | Task 9 (rpc test) — applies to all handlers |
|
|
3358
|
+
| `event: 'return'` SSE return value | Tasks 11, 12 (stream + http-stream tests) |
|
|
3359
|
+
| `isPrevalidated` short-circuit | Task 11 (stream handler) |
|
|
3360
|
+
| `skippedProcedures` near-empty after convergence | Task 13 + Task 19 (retargeted test) |
|
|
3361
|
+
| Tests consolidated under `hono/` | Tasks 19–21 |
|
|
3362
|
+
| Templates/docs/CLAUDE.md/README.md updated | Tasks 24–27 |
|
|
3363
|
+
| Codegen e2e verification | Task 28 |
|
|
3364
|
+
|
|
3365
|
+
No placeholders, no TBDs. Code blocks are concrete. Each task ends in a commit.
|