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,147 @@
|
|
|
1
|
+
import type { Context, Hono } from 'hono'
|
|
2
|
+
import type { THttpProcedureRegistration } from '../../../../types.js'
|
|
3
|
+
import type { HttpMethod } from '../../../types.js'
|
|
4
|
+
import { dispatchPreStreamError } from '../../error-dispatch.js'
|
|
5
|
+
import { buildHttpRouteDoc } from '../docs/http-doc.js'
|
|
6
|
+
import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem, QueryParser } from '../types.js'
|
|
7
|
+
|
|
8
|
+
const BODY_METHODS: HttpMethod[] = ['post', 'put', 'patch']
|
|
9
|
+
|
|
10
|
+
function defaultSuccessStatus(method: HttpMethod): number {
|
|
11
|
+
switch (method) {
|
|
12
|
+
case 'post': return 201
|
|
13
|
+
case 'delete': return 204
|
|
14
|
+
default: return 200
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseQueryNative(queryString: string): Record<string, unknown> {
|
|
19
|
+
const sp = new URLSearchParams(queryString)
|
|
20
|
+
const result: Record<string, unknown> = {}
|
|
21
|
+
for (const key of new Set(sp.keys())) {
|
|
22
|
+
const values = sp.getAll(key)
|
|
23
|
+
result[key] = values.length > 1 ? values : values[0]
|
|
24
|
+
}
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractQuery(url: string, parser: QueryParser): Record<string, unknown> {
|
|
29
|
+
const q = url.indexOf('?')
|
|
30
|
+
if (q === -1) return {}
|
|
31
|
+
const raw = url.slice(q + 1)
|
|
32
|
+
return raw ? parser(raw) : {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function extractParams(
|
|
36
|
+
c: Context,
|
|
37
|
+
method: HttpMethod,
|
|
38
|
+
reqSchema: Record<string, unknown>,
|
|
39
|
+
parser: QueryParser,
|
|
40
|
+
): Promise<Record<string, unknown>> {
|
|
41
|
+
const params: Record<string, unknown> = {}
|
|
42
|
+
for (const channel of Object.keys(reqSchema)) {
|
|
43
|
+
switch (channel) {
|
|
44
|
+
case 'pathParams':
|
|
45
|
+
params.pathParams = c.req.param()
|
|
46
|
+
break
|
|
47
|
+
case 'query':
|
|
48
|
+
params.query = extractQuery(c.req.url, parser)
|
|
49
|
+
break
|
|
50
|
+
case 'body':
|
|
51
|
+
if (BODY_METHODS.includes(method)) {
|
|
52
|
+
params.body = await c.req.json().catch(() => ({}))
|
|
53
|
+
}
|
|
54
|
+
break
|
|
55
|
+
case 'headers': {
|
|
56
|
+
const obj: Record<string, string> = {}
|
|
57
|
+
c.req.raw.headers.forEach((v, k) => { obj[k] = v })
|
|
58
|
+
params.headers = obj
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
default:
|
|
62
|
+
params[channel] = undefined
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return params
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function installHttpRoute(params: {
|
|
69
|
+
app: Hono
|
|
70
|
+
procedure: THttpProcedureRegistration<any>
|
|
71
|
+
factoryItem: HonoFactoryItem
|
|
72
|
+
cfg: HonoAppBuilderConfig
|
|
73
|
+
docs: DocAccumulator
|
|
74
|
+
}): void {
|
|
75
|
+
const { app, procedure, factoryItem, cfg, docs } = params
|
|
76
|
+
const queryParser = cfg.api?.queryParser ?? parseQueryNative
|
|
77
|
+
|
|
78
|
+
const route = buildHttpRouteDoc(
|
|
79
|
+
procedure,
|
|
80
|
+
cfg.pathPrefix,
|
|
81
|
+
factoryItem.extendProcedureDoc as any,
|
|
82
|
+
)
|
|
83
|
+
docs.push(route)
|
|
84
|
+
|
|
85
|
+
const successStatus = procedure.config.successStatus ?? defaultSuccessStatus(procedure.config.method)
|
|
86
|
+
const reqSchema = procedure.config.schema?.req
|
|
87
|
+
|
|
88
|
+
app.on(procedure.config.method.toUpperCase(), route.fullPath, async (c: Context) => {
|
|
89
|
+
try {
|
|
90
|
+
const context =
|
|
91
|
+
typeof factoryItem.factoryContext === 'function'
|
|
92
|
+
? await (factoryItem.factoryContext as (c: Context) => any)(c)
|
|
93
|
+
: factoryItem.factoryContext
|
|
94
|
+
|
|
95
|
+
const reqParams = reqSchema
|
|
96
|
+
? await extractParams(c, procedure.config.method, reqSchema, queryParser)
|
|
97
|
+
: undefined
|
|
98
|
+
|
|
99
|
+
const result = await procedure.handler(
|
|
100
|
+
{ ...context, signal: c.req.raw.signal },
|
|
101
|
+
reqParams,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
cfg.api?.onSuccess?.(procedure, c)
|
|
105
|
+
|
|
106
|
+
// 204 No Content — no body, just optional headers
|
|
107
|
+
if (successStatus === 204) {
|
|
108
|
+
if (result && typeof result === 'object' && 'headers' in result && (result as any).headers) {
|
|
109
|
+
for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
|
|
110
|
+
c.header(k, v)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return c.body(null, 204)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let body: unknown = result
|
|
117
|
+
let headers: Record<string, string> | undefined
|
|
118
|
+
|
|
119
|
+
if (result && typeof result === 'object' && 'body' in result && 'headers' in result) {
|
|
120
|
+
body = (result as any).body
|
|
121
|
+
headers = (result as any).headers as Record<string, string>
|
|
122
|
+
} else if (result && typeof result === 'object' && 'headers' in result && !('body' in result)) {
|
|
123
|
+
for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
|
|
124
|
+
c.header(k, v)
|
|
125
|
+
}
|
|
126
|
+
return c.body(null, successStatus as any)
|
|
127
|
+
} else if (result && typeof result === 'object' && 'body' in result && !('headers' in result)) {
|
|
128
|
+
body = (result as any).body
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (headers) for (const [k, v] of Object.entries(headers)) c.header(k, v)
|
|
132
|
+
return c.json(body, successStatus as any)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return dispatchPreStreamError({
|
|
135
|
+
err: error,
|
|
136
|
+
procedure,
|
|
137
|
+
raw: c,
|
|
138
|
+
cfg: {
|
|
139
|
+
errors: cfg.errors,
|
|
140
|
+
unknownError: cfg.unknownError,
|
|
141
|
+
onError: cfg.onError,
|
|
142
|
+
onRequestError: cfg.onRequestError,
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import { Type } from 'typebox'
|
|
4
|
+
import { Procedures } from '../../../../index.js'
|
|
5
|
+
import type { RPCConfig } from '../../../types.js'
|
|
6
|
+
import { installRpcRoute } from './rpc.js'
|
|
7
|
+
|
|
8
|
+
function buildApp(register: (factory: ReturnType<typeof Procedures<{ userId: string }, RPCConfig>>) => void, cfg: any = {}) {
|
|
9
|
+
const RPC = Procedures<{ userId: string }, RPCConfig>()
|
|
10
|
+
register(RPC)
|
|
11
|
+
const app = new Hono()
|
|
12
|
+
const docs: any[] = []
|
|
13
|
+
for (const proc of RPC.getProcedures().values()) {
|
|
14
|
+
if (proc.kind !== 'rpc') continue
|
|
15
|
+
installRpcRoute({
|
|
16
|
+
app,
|
|
17
|
+
procedure: proc as any,
|
|
18
|
+
factoryItem: { factory: RPC, factoryContext: () => ({ userId: '123' }) },
|
|
19
|
+
cfg,
|
|
20
|
+
docs,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
return { app, docs }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('installRpcRoute', () => {
|
|
27
|
+
test('mounts POST route at scope/name/version path', async () => {
|
|
28
|
+
const { app, docs } = buildApp((RPC) => {
|
|
29
|
+
RPC.Create('Echo', {
|
|
30
|
+
scope: 'echo',
|
|
31
|
+
version: 1,
|
|
32
|
+
schema: { params: Type.Object({ msg: Type.String() }) },
|
|
33
|
+
}, async (_ctx, params) => params)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const res = await app.request('/echo/echo/1', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ msg: 'hi' }),
|
|
40
|
+
})
|
|
41
|
+
expect(res.status).toBe(200)
|
|
42
|
+
expect(await res.json()).toEqual({ msg: 'hi' })
|
|
43
|
+
expect(docs).toHaveLength(1)
|
|
44
|
+
expect(docs[0].kind).toBe('rpc')
|
|
45
|
+
expect(docs[0].path).toBe('/echo/echo/1')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('rpc.onSuccess fires after handler', async () => {
|
|
49
|
+
const onSuccess = vi.fn()
|
|
50
|
+
const { app } = buildApp((RPC) => {
|
|
51
|
+
RPC.Create('Ping', { scope: 'p', version: 1 }, async () => ({ ok: true }))
|
|
52
|
+
}, { rpc: { onSuccess } })
|
|
53
|
+
|
|
54
|
+
await app.request('/p/ping/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
55
|
+
expect(onSuccess).toHaveBeenCalledOnce()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('thrown error → dispatchPreStreamError default 500', async () => {
|
|
59
|
+
const { app } = buildApp((RPC) => {
|
|
60
|
+
RPC.Create('Boom', { scope: 'b', version: 1 }, async () => { throw new Error('boom') })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const res = await app.request('/b/boom/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
64
|
+
expect(res.status).toBe(500)
|
|
65
|
+
const body = await res.json()
|
|
66
|
+
expect(body.error).toContain('boom')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('AbortSignal is injected into ctx', async () => {
|
|
70
|
+
let receivedSignal: AbortSignal | undefined
|
|
71
|
+
const { app } = buildApp((RPC) => {
|
|
72
|
+
RPC.Create('Sig', { scope: 's', version: 1 }, async (ctx) => {
|
|
73
|
+
receivedSignal = ctx.signal
|
|
74
|
+
return {}
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await app.request('/s/sig/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
79
|
+
expect(receivedSignal).toBeInstanceOf(AbortSignal)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Context, Hono } from 'hono'
|
|
2
|
+
import type { TProcedureRegistration } from '../../../../types.js'
|
|
3
|
+
import type { RPCConfig } from '../../../types.js'
|
|
4
|
+
import { dispatchPreStreamError } from '../../error-dispatch.js'
|
|
5
|
+
import { buildRpcRouteDoc } from '../docs/rpc-doc.js'
|
|
6
|
+
import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem } from '../types.js'
|
|
7
|
+
|
|
8
|
+
export function installRpcRoute(params: {
|
|
9
|
+
app: Hono
|
|
10
|
+
procedure: TProcedureRegistration<any, RPCConfig>
|
|
11
|
+
factoryItem: HonoFactoryItem
|
|
12
|
+
cfg: HonoAppBuilderConfig
|
|
13
|
+
docs: DocAccumulator
|
|
14
|
+
}): void {
|
|
15
|
+
const { app, procedure, factoryItem, cfg, docs } = params
|
|
16
|
+
|
|
17
|
+
const route = buildRpcRouteDoc(
|
|
18
|
+
procedure,
|
|
19
|
+
cfg.pathPrefix,
|
|
20
|
+
factoryItem.extendProcedureDoc as any,
|
|
21
|
+
)
|
|
22
|
+
docs.push(route)
|
|
23
|
+
|
|
24
|
+
app.post(route.path, async (c: Context) => {
|
|
25
|
+
try {
|
|
26
|
+
const context =
|
|
27
|
+
typeof factoryItem.factoryContext === 'function'
|
|
28
|
+
? await (factoryItem.factoryContext as (c: Context) => any)(c)
|
|
29
|
+
: factoryItem.factoryContext
|
|
30
|
+
|
|
31
|
+
const body = await c.req.json().catch(() => ({}))
|
|
32
|
+
const result = await procedure.handler(
|
|
33
|
+
{ ...context, signal: c.req.raw.signal },
|
|
34
|
+
body,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
cfg.rpc?.onSuccess?.(procedure, c)
|
|
38
|
+
|
|
39
|
+
return c.json(result)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return dispatchPreStreamError({
|
|
42
|
+
err: error,
|
|
43
|
+
procedure,
|
|
44
|
+
raw: c,
|
|
45
|
+
cfg: {
|
|
46
|
+
errors: cfg.errors,
|
|
47
|
+
unknownError: cfg.unknownError,
|
|
48
|
+
onError: cfg.onError,
|
|
49
|
+
onRequestError: cfg.onRequestError,
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import { Type } from 'typebox'
|
|
4
|
+
import { Procedures } from '../../../../index.js'
|
|
5
|
+
import type { RPCConfig } from '../../../types.js'
|
|
6
|
+
import { installRpcStreamRoute, sse } from './stream.js'
|
|
7
|
+
|
|
8
|
+
function buildApp(setup: (P: ReturnType<typeof Procedures<{ uid: string }, RPCConfig>>) => void, cfg: any = {}) {
|
|
9
|
+
const P = Procedures<{ uid: string }, RPCConfig>()
|
|
10
|
+
setup(P)
|
|
11
|
+
const app = new Hono()
|
|
12
|
+
const docs: any[] = []
|
|
13
|
+
for (const proc of P.getProcedures().values()) {
|
|
14
|
+
if (proc.kind !== 'rpc-stream') continue
|
|
15
|
+
installRpcStreamRoute({
|
|
16
|
+
app,
|
|
17
|
+
procedure: proc as any,
|
|
18
|
+
factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
|
|
19
|
+
cfg,
|
|
20
|
+
docs,
|
|
21
|
+
streamMode: cfg.stream?.defaultStreamMode ?? 'sse',
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
return { app, docs }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('installRpcStreamRoute', () => {
|
|
28
|
+
test('SSE: yields are written as data events; final return as event:return', async () => {
|
|
29
|
+
const { app, docs } = buildApp((P) => {
|
|
30
|
+
P.CreateStream('Counts', { scope: 'c', version: 1 }, async function* () {
|
|
31
|
+
yield 1
|
|
32
|
+
yield 2
|
|
33
|
+
return 'done'
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
expect(docs[0].kind).toBe('stream')
|
|
37
|
+
expect(docs[0].path).toBe('/c/counts/1')
|
|
38
|
+
|
|
39
|
+
const res = await app.request('/c/counts/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
40
|
+
expect(res.status).toBe(200)
|
|
41
|
+
const text = await res.text()
|
|
42
|
+
expect(text).toContain('data: 1')
|
|
43
|
+
expect(text).toContain('data: 2')
|
|
44
|
+
expect(text).toContain('event: return')
|
|
45
|
+
expect(text).toContain('data: done')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('mid-stream throw → dispatchMidStreamError default { error }', async () => {
|
|
49
|
+
const { app } = buildApp((P) => {
|
|
50
|
+
P.CreateStream('Boom', { scope: 'b', version: 1 }, async function* () {
|
|
51
|
+
yield 'first'
|
|
52
|
+
throw new Error('mid-boom')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
const res = await app.request('/b/boom/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
56
|
+
const text = await res.text()
|
|
57
|
+
expect(text).toContain('event: error')
|
|
58
|
+
expect(text).toContain('"error":"mid-boom"')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('pre-stream validation error → 400 via dispatchPreStreamError', async () => {
|
|
62
|
+
const { app } = buildApp((P) => {
|
|
63
|
+
P.CreateStream('NeedsParams', {
|
|
64
|
+
scope: 'n', version: 1,
|
|
65
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
66
|
+
}, async function* () { yield 'ok' })
|
|
67
|
+
})
|
|
68
|
+
const res = await app.request('/n/needs-params/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
69
|
+
expect(res.status).toBe(400)
|
|
70
|
+
const body = await res.json()
|
|
71
|
+
expect(body.name).toBe('ProcedureValidationError')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('onStreamStart and onStreamEnd fire', async () => {
|
|
75
|
+
const onStreamStart = vi.fn()
|
|
76
|
+
const onStreamEnd = vi.fn()
|
|
77
|
+
const { app } = buildApp((P) => {
|
|
78
|
+
P.CreateStream('Ev', { scope: 'e', version: 1 }, async function* () { yield 'x' })
|
|
79
|
+
}, { stream: { onStreamStart, onStreamEnd } })
|
|
80
|
+
|
|
81
|
+
const res = await app.request('/e/ev/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
82
|
+
// Consume the stream body so the SSE writer's finally block runs
|
|
83
|
+
await res.text()
|
|
84
|
+
expect(onStreamStart).toHaveBeenCalled()
|
|
85
|
+
expect(onStreamEnd).toHaveBeenCalled()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('validateYields: true → invalid yield surfaces ProcedureYieldValidationError on the wire', async () => {
|
|
89
|
+
const { app } = buildApp((P) => {
|
|
90
|
+
P.CreateStream(
|
|
91
|
+
'BadYield',
|
|
92
|
+
{
|
|
93
|
+
scope: 'y', version: 1,
|
|
94
|
+
schema: { yieldType: Type.Object({ id: Type.Number() }) },
|
|
95
|
+
validateYields: true,
|
|
96
|
+
},
|
|
97
|
+
async function* () {
|
|
98
|
+
yield { id: 1 } // valid
|
|
99
|
+
yield { id: 'not-a-number' } as any // invalid → throws ProcedureYieldValidationError
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const res = await app.request('/y/bad-yield/1', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
body: '{}',
|
|
107
|
+
headers: { 'Content-Type': 'application/json' },
|
|
108
|
+
})
|
|
109
|
+
// Stream is already open by the time validation fails on the second yield;
|
|
110
|
+
// status is committed at 200 and the error shows up as an SSE error event.
|
|
111
|
+
expect(res.status).toBe(200)
|
|
112
|
+
const text = await res.text()
|
|
113
|
+
expect(text).toContain('event: error')
|
|
114
|
+
expect(text).toContain('"name":"ProcedureYieldValidationError"')
|
|
115
|
+
expect(text).toContain('"procedureName":"BadYield"')
|
|
116
|
+
// First valid yield still made it onto the wire.
|
|
117
|
+
expect(text).toContain('data: {"id":1}')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('sse() helper propagates event/id/retry into SSE wire output', async () => {
|
|
121
|
+
const { app } = buildApp((P) => {
|
|
122
|
+
P.CreateStream('Tagged', { scope: 'tagged', version: 1 }, async function* () {
|
|
123
|
+
yield sse({ count: 1 }, { event: 'tick', id: 'evt-1', retry: 5000 })
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const res = await app.request('/tagged/tagged/1', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
body: '{}',
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
})
|
|
132
|
+
const text = await res.text()
|
|
133
|
+
expect(text).toContain('event: tick')
|
|
134
|
+
expect(text).toContain('id: evt-1')
|
|
135
|
+
expect(text).toContain('retry: 5000')
|
|
136
|
+
expect(text).toContain('data: {"count":1}')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('AbortSignal: natural completion → ctx.signal.reason === "stream-completed"', async () => {
|
|
140
|
+
let capturedSignal: AbortSignal | undefined
|
|
141
|
+
const { app } = buildApp((P) => {
|
|
142
|
+
P.CreateStream('Natural', { scope: 'sig', version: 1 }, async function* (ctx) {
|
|
143
|
+
capturedSignal = ctx.signal
|
|
144
|
+
yield 'a'
|
|
145
|
+
yield 'b'
|
|
146
|
+
return 'done'
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const res = await app.request('/sig/natural/1', {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
body: '{}',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
})
|
|
155
|
+
// Drain the body so the wrappedHandler's finally block runs.
|
|
156
|
+
await res.text()
|
|
157
|
+
|
|
158
|
+
expect(capturedSignal).toBeInstanceOf(AbortSignal)
|
|
159
|
+
expect(capturedSignal!.aborted).toBe(true)
|
|
160
|
+
expect(capturedSignal!.reason).toBe('stream-completed')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('AbortSignal: client disconnect (request signal aborted) → ctx.signal.reason !== "stream-completed"', async () => {
|
|
164
|
+
// NOTE: We can't directly simulate a real socket disconnect through
|
|
165
|
+
// `app.request()`. The closest test-friendly proxy: pre-abort the request
|
|
166
|
+
// signal so `c.req.raw.signal` is already aborted by the time the
|
|
167
|
+
// wrappedHandler in create-stream.ts builds the combined signal via
|
|
168
|
+
// `AbortSignal.any([incomingSignal, internalController.signal])`. The
|
|
169
|
+
// pre-existing reason wins over the internal 'stream-completed' that fires
|
|
170
|
+
// later in the wrappedHandler's finally clause.
|
|
171
|
+
let capturedSignal: AbortSignal | undefined
|
|
172
|
+
const { app } = buildApp((P) => {
|
|
173
|
+
P.CreateStream('Disconnect', { scope: 'sig', version: 1 }, async function* (ctx) {
|
|
174
|
+
capturedSignal = ctx.signal
|
|
175
|
+
yield 'first'
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const ac = new AbortController()
|
|
180
|
+
ac.abort('client-gone')
|
|
181
|
+
|
|
182
|
+
const req = new Request('http://localhost/sig/disconnect/1', {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
body: '{}',
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
signal: ac.signal,
|
|
187
|
+
})
|
|
188
|
+
const res = await app.request(req)
|
|
189
|
+
// Drain the body in case anything was emitted before the abort propagated.
|
|
190
|
+
await res.text().catch(() => '')
|
|
191
|
+
|
|
192
|
+
expect(capturedSignal).toBeInstanceOf(AbortSignal)
|
|
193
|
+
expect(capturedSignal!.aborted).toBe(true)
|
|
194
|
+
// The disconnect reason wins over the internal 'stream-completed' signal,
|
|
195
|
+
// so handlers can distinguish "client gave up" from "we finished normally".
|
|
196
|
+
expect(capturedSignal!.reason).not.toBe('stream-completed')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Context, Hono } from 'hono'
|
|
2
|
+
import { streamSSE, streamText } from 'hono/streaming'
|
|
3
|
+
import type { TStreamProcedureRegistration } from '../../../../types.js'
|
|
4
|
+
import type { RPCConfig, StreamMode } from '../../../types.js'
|
|
5
|
+
import { ProcedureValidationError } from '../../../../errors.js'
|
|
6
|
+
import { dispatchMidStreamError, dispatchPreStreamError } from '../../error-dispatch.js'
|
|
7
|
+
import { buildStreamRouteDoc } from '../docs/stream-doc.js'
|
|
8
|
+
import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem } from '../types.js'
|
|
9
|
+
|
|
10
|
+
export type SSEOptions = {
|
|
11
|
+
event?: string
|
|
12
|
+
id?: string
|
|
13
|
+
retry?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sseMetadata = new WeakMap<object, SSEOptions>()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Marks an object yield as an SSE event with custom metadata. Stream handlers
|
|
20
|
+
* read this metadata to set the SSE `event:`, `id:`, and `retry:` fields.
|
|
21
|
+
*/
|
|
22
|
+
export function sse<T extends object>(data: T, options?: SSEOptions): T {
|
|
23
|
+
sseMetadata.set(data, options ?? {})
|
|
24
|
+
return data
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSSEMeta(value: unknown): SSEOptions | undefined {
|
|
28
|
+
if (typeof value === 'object' && value !== null) {
|
|
29
|
+
return sseMetadata.get(value as object)
|
|
30
|
+
}
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type { MidStreamErrorResult } from '../../error-dispatch.js'
|
|
35
|
+
|
|
36
|
+
export function installRpcStreamRoute(params: {
|
|
37
|
+
app: Hono
|
|
38
|
+
procedure: TStreamProcedureRegistration<any, RPCConfig>
|
|
39
|
+
factoryItem: HonoFactoryItem
|
|
40
|
+
cfg: HonoAppBuilderConfig
|
|
41
|
+
docs: DocAccumulator
|
|
42
|
+
streamMode: StreamMode
|
|
43
|
+
}): void {
|
|
44
|
+
const { app, procedure, factoryItem, cfg, docs, streamMode } = params
|
|
45
|
+
|
|
46
|
+
const route = buildStreamRouteDoc(
|
|
47
|
+
procedure,
|
|
48
|
+
streamMode,
|
|
49
|
+
cfg.pathPrefix,
|
|
50
|
+
factoryItem.extendProcedureDoc as any,
|
|
51
|
+
)
|
|
52
|
+
docs.push(route)
|
|
53
|
+
|
|
54
|
+
const handler = async (c: Context) => {
|
|
55
|
+
try {
|
|
56
|
+
const context =
|
|
57
|
+
typeof factoryItem.factoryContext === 'function'
|
|
58
|
+
? await (factoryItem.factoryContext as (c: Context) => any)(c)
|
|
59
|
+
: factoryItem.factoryContext
|
|
60
|
+
|
|
61
|
+
const reqParams =
|
|
62
|
+
c.req.method === 'GET'
|
|
63
|
+
? Object.fromEntries(new URL(c.req.url).searchParams)
|
|
64
|
+
: await c.req.json().catch(() => ({}))
|
|
65
|
+
|
|
66
|
+
// Pre-stream validation — throw so the catch routes through dispatchPreStreamError.
|
|
67
|
+
if ((procedure.config as any).validation?.params) {
|
|
68
|
+
const { errors } = (procedure.config as any).validation.params(reqParams)
|
|
69
|
+
if (errors) {
|
|
70
|
+
throw new ProcedureValidationError(
|
|
71
|
+
procedure.name,
|
|
72
|
+
`Validation error for ${procedure.name}`,
|
|
73
|
+
errors,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cfg.stream?.onStreamStart?.(procedure, c, streamMode)
|
|
79
|
+
|
|
80
|
+
return streamMode === 'sse'
|
|
81
|
+
? handleSSE(procedure, context, reqParams, c, cfg)
|
|
82
|
+
: handleText(procedure, context, reqParams, c, cfg)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return dispatchPreStreamError({
|
|
85
|
+
err: error,
|
|
86
|
+
procedure,
|
|
87
|
+
raw: c,
|
|
88
|
+
cfg: {
|
|
89
|
+
errors: cfg.errors,
|
|
90
|
+
unknownError: cfg.unknownError,
|
|
91
|
+
onError: cfg.onError,
|
|
92
|
+
onRequestError: cfg.onRequestError,
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
app.get(route.path, handler)
|
|
99
|
+
app.post(route.path, handler)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleSSE(
|
|
103
|
+
procedure: TStreamProcedureRegistration<any, RPCConfig>,
|
|
104
|
+
context: any,
|
|
105
|
+
reqParams: any,
|
|
106
|
+
c: Context,
|
|
107
|
+
cfg: HonoAppBuilderConfig,
|
|
108
|
+
) {
|
|
109
|
+
return streamSSE(c, async (stream) => {
|
|
110
|
+
const generator = procedure.handler(
|
|
111
|
+
{ ...context, signal: c.req.raw.signal, isPrevalidated: true } as any,
|
|
112
|
+
reqParams,
|
|
113
|
+
)
|
|
114
|
+
stream.onAbort(async () => { await generator.return(undefined) })
|
|
115
|
+
|
|
116
|
+
let eventId = 0
|
|
117
|
+
try {
|
|
118
|
+
const iterator = generator[Symbol.asyncIterator]()
|
|
119
|
+
let it = await iterator.next()
|
|
120
|
+
|
|
121
|
+
while (!it.done) {
|
|
122
|
+
const value = it.value
|
|
123
|
+
const meta = getSSEMeta(value)
|
|
124
|
+
const data =
|
|
125
|
+
typeof value === 'string' ? value
|
|
126
|
+
: value != null ? JSON.stringify(value)
|
|
127
|
+
: ''
|
|
128
|
+
|
|
129
|
+
await stream.writeSSE({
|
|
130
|
+
data,
|
|
131
|
+
event: meta?.event ?? procedure.name,
|
|
132
|
+
id: meta?.id ?? String(eventId++),
|
|
133
|
+
...(meta?.retry !== undefined && { retry: meta.retry }),
|
|
134
|
+
})
|
|
135
|
+
it = await iterator.next()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (it.value !== undefined) {
|
|
139
|
+
const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
|
|
140
|
+
await stream.writeSSE({ data, event: 'return', id: String(eventId++) })
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const dispatched = await dispatchMidStreamError({
|
|
144
|
+
err: error,
|
|
145
|
+
procedure,
|
|
146
|
+
raw: c,
|
|
147
|
+
cfg: {
|
|
148
|
+
errors: cfg.errors,
|
|
149
|
+
unknownError: cfg.unknownError,
|
|
150
|
+
onMidStreamError: cfg.stream?.onMidStreamError,
|
|
151
|
+
onRequestError: cfg.onRequestError,
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const meta = getSSEMeta(dispatched.data)
|
|
156
|
+
await stream.writeSSE({
|
|
157
|
+
data: typeof dispatched.data === 'string' ? dispatched.data : JSON.stringify(dispatched.data),
|
|
158
|
+
event: meta?.event ?? dispatched.sseEvent ?? 'error',
|
|
159
|
+
id: meta?.id ?? String(eventId++),
|
|
160
|
+
...(meta?.retry !== undefined && { retry: meta.retry }),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
if (dispatched.runOnCatch) await dispatched.runOnCatch()
|
|
164
|
+
} finally {
|
|
165
|
+
cfg.stream?.onStreamEnd?.(procedure, c, 'sse')
|
|
166
|
+
cfg.onRequestEnd?.(c)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleText(
|
|
172
|
+
procedure: TStreamProcedureRegistration<any, RPCConfig>,
|
|
173
|
+
context: any,
|
|
174
|
+
reqParams: any,
|
|
175
|
+
c: Context,
|
|
176
|
+
cfg: HonoAppBuilderConfig,
|
|
177
|
+
) {
|
|
178
|
+
return streamText(c, async (stream) => {
|
|
179
|
+
const generator = procedure.handler(
|
|
180
|
+
{ ...context, signal: c.req.raw.signal, isPrevalidated: true } as any,
|
|
181
|
+
reqParams,
|
|
182
|
+
)
|
|
183
|
+
stream.onAbort(async () => { await generator.return(undefined) })
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
for await (const value of generator) {
|
|
187
|
+
await stream.writeln(JSON.stringify(value))
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const dispatched = await dispatchMidStreamError({
|
|
191
|
+
err: error,
|
|
192
|
+
procedure,
|
|
193
|
+
raw: c,
|
|
194
|
+
cfg: {
|
|
195
|
+
errors: cfg.errors,
|
|
196
|
+
unknownError: cfg.unknownError,
|
|
197
|
+
onMidStreamError: cfg.stream?.onMidStreamError,
|
|
198
|
+
onRequestError: cfg.onRequestError,
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
await stream.writeln(JSON.stringify(dispatched.data))
|
|
202
|
+
if (dispatched.runOnCatch) await dispatched.runOnCatch()
|
|
203
|
+
} finally {
|
|
204
|
+
cfg.stream?.onStreamEnd?.(procedure, c, 'text')
|
|
205
|
+
cfg.onRequestEnd?.(c)
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
}
|