ts-procedures 7.3.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +104 -53
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +205 -232
- 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 +34 -48
- package/agent_config/cursor/cursorrules +34 -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 +124 -124
- package/build/index.js +10 -221
- package/build/index.js.map +1 -1
- package/build/index.test.js +20 -919
- 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 +15 -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 +22 -1249
- package/src/index.ts +49 -485
- 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
|
@@ -5,9 +5,8 @@ import { stripPrefix } from './rewrite-request.js'
|
|
|
5
5
|
import { setAstroContext, getAstroContext } from './astro-context.js'
|
|
6
6
|
import { createAstroHandler } from './create-handler.js'
|
|
7
7
|
import { Procedures } from '../../../index.js'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import type { APIConfig, RPCConfig } from '../../types.js'
|
|
8
|
+
import { HonoAppBuilder } from '../hono/index.js'
|
|
9
|
+
import type { RPCConfig } from '../../types.js'
|
|
11
10
|
|
|
12
11
|
// Minimal stand-in for Astro's APIContext for unit tests.
|
|
13
12
|
function fakeApiContext(overrides: Record<string, unknown> = {}) {
|
|
@@ -23,15 +22,15 @@ function fakeApiContext(overrides: Record<string, unknown> = {}) {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
function buildSimpleUserApi() {
|
|
26
|
-
const API = Procedures<{ db: Map<string, { id: string; name: string }> }
|
|
27
|
-
API.
|
|
25
|
+
const API = Procedures<{ db: Map<string, { id: string; name: string }> }>()
|
|
26
|
+
API.CreateHttp(
|
|
28
27
|
'GetUser',
|
|
29
28
|
{
|
|
30
29
|
path: '/users/:id',
|
|
31
30
|
method: 'get',
|
|
32
31
|
schema: {
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
33
|
+
res: { body: Type.Object({ id: Type.String(), name: Type.String() }) },
|
|
35
34
|
},
|
|
36
35
|
},
|
|
37
36
|
async (ctx, { pathParams }) => {
|
|
@@ -41,7 +40,7 @@ function buildSimpleUserApi() {
|
|
|
41
40
|
}
|
|
42
41
|
)
|
|
43
42
|
const db = new Map([['1', { id: '1', name: 'Ada' }]])
|
|
44
|
-
return new
|
|
43
|
+
return new HonoAppBuilder().register(API, () => ({ db })).build()
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
describe('stripPrefix', () => {
|
|
@@ -199,14 +198,18 @@ describe('createAstroHandler — pathPrefix + getAstroContext', () => {
|
|
|
199
198
|
})
|
|
200
199
|
|
|
201
200
|
test('factory closure can read APIContext via getAstroContext', async () => {
|
|
202
|
-
const API = Procedures<{ user: { id: string } | null }
|
|
203
|
-
API.
|
|
201
|
+
const API = Procedures<{ user: { id: string } | null }>()
|
|
202
|
+
API.CreateHttp(
|
|
204
203
|
'WhoAmI',
|
|
205
|
-
{
|
|
204
|
+
{
|
|
205
|
+
path: '/whoami',
|
|
206
|
+
method: 'get',
|
|
207
|
+
schema: { res: { body: Type.Object({ userId: Type.Union([Type.String(), Type.Null()]) }) } },
|
|
208
|
+
},
|
|
206
209
|
async (ctx) => ({ userId: ctx.user?.id ?? null })
|
|
207
210
|
)
|
|
208
211
|
|
|
209
|
-
const app = new
|
|
212
|
+
const app = new HonoAppBuilder()
|
|
210
213
|
.register(API, (c) => {
|
|
211
214
|
const astro = getAstroContext(c)
|
|
212
215
|
return { user: (astro.locals as { user?: { id: string } }).user ?? null }
|
|
@@ -294,7 +297,7 @@ describe('createAstroHandler — streams', () => {
|
|
|
294
297
|
}
|
|
295
298
|
)
|
|
296
299
|
|
|
297
|
-
const streamApp = new
|
|
300
|
+
const streamApp = new HonoAppBuilder().register(STREAM, () => ({})).build()
|
|
298
301
|
const { ALL } = createAstroHandler({ apps: streamApp })
|
|
299
302
|
|
|
300
303
|
const apiContext = fakeApiContext({
|
|
@@ -318,10 +321,14 @@ describe('createAstroHandler — abort signal', () => {
|
|
|
318
321
|
test("aborting the incoming request aborts the procedure handler's ctx.signal", async () => {
|
|
319
322
|
let observedSignal: AbortSignal | undefined
|
|
320
323
|
const aborted = new Promise<void>((resolve) => {
|
|
321
|
-
const API = Procedures<{}
|
|
322
|
-
API.
|
|
324
|
+
const API = Procedures<{}>()
|
|
325
|
+
API.CreateHttp(
|
|
323
326
|
'Hang',
|
|
324
|
-
{
|
|
327
|
+
{
|
|
328
|
+
path: '/hang',
|
|
329
|
+
method: 'get',
|
|
330
|
+
schema: { res: { body: Type.Object({ ok: Type.Boolean() }) } },
|
|
331
|
+
},
|
|
325
332
|
async (ctx) => {
|
|
326
333
|
observedSignal = ctx.signal
|
|
327
334
|
ctx.signal?.addEventListener('abort', () => resolve(), { once: true })
|
|
@@ -330,7 +337,7 @@ describe('createAstroHandler — abort signal', () => {
|
|
|
330
337
|
return { ok: true }
|
|
331
338
|
}
|
|
332
339
|
)
|
|
333
|
-
const app = new
|
|
340
|
+
const app = new HonoAppBuilder().register(API, () => ({})).build()
|
|
334
341
|
const { ALL } = createAstroHandler({ apps: app })
|
|
335
342
|
|
|
336
343
|
const controller = new AbortController()
|
|
@@ -340,7 +347,7 @@ describe('createAstroHandler — abort signal', () => {
|
|
|
340
347
|
})
|
|
341
348
|
|
|
342
349
|
// Fire ALL but don't await — abort while the handler is still running.
|
|
343
|
-
|
|
350
|
+
Promise.resolve(ALL(apiContext)).catch(() => {})
|
|
344
351
|
setTimeout(() => controller.abort(), 20)
|
|
345
352
|
})
|
|
346
353
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, test, vi, afterEach } from 'vitest'
|
|
2
2
|
import { v } from 'suretype'
|
|
3
|
+
import { Type } from 'typebox'
|
|
3
4
|
import { Procedures } from '../../index.js'
|
|
4
|
-
import {
|
|
5
|
+
import { HonoAppBuilder } from './hono/index.js'
|
|
5
6
|
import { DocRegistry } from './doc-registry.js'
|
|
6
7
|
import { defineErrorTaxonomy } from './error-taxonomy.js'
|
|
7
8
|
import type {
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
RPCConfig,
|
|
11
12
|
APIHttpRouteDoc,
|
|
12
13
|
StreamHttpRouteDoc,
|
|
14
|
+
HttpStreamRouteDoc,
|
|
13
15
|
DocSource,
|
|
14
16
|
DocEnvelope,
|
|
15
17
|
ErrorDoc,
|
|
@@ -35,7 +37,7 @@ const apiDoc: APIHttpRouteDoc = {
|
|
|
35
37
|
path: '/users/:id',
|
|
36
38
|
method: 'get',
|
|
37
39
|
fullPath: '/api/users/:id',
|
|
38
|
-
jsonSchema: { pathParams: { type: 'object' } },
|
|
40
|
+
jsonSchema: { req: { pathParams: { type: 'object' } } },
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
const streamDoc: StreamHttpRouteDoc = {
|
|
@@ -49,6 +51,16 @@ const streamDoc: StreamHttpRouteDoc = {
|
|
|
49
51
|
jsonSchema: { yieldType: { type: 'object' } },
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
const httpStreamDoc: HttpStreamRouteDoc = {
|
|
55
|
+
kind: 'http-stream',
|
|
56
|
+
name: 'Tail',
|
|
57
|
+
path: '/streams/logs',
|
|
58
|
+
method: 'get',
|
|
59
|
+
fullPath: '/streams/logs',
|
|
60
|
+
streamMode: 'sse',
|
|
61
|
+
jsonSchema: { yield: { type: 'object' } },
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
function makeSource<T extends AnyHttpRouteDoc>(docs: T[]): DocSource<T> {
|
|
53
65
|
return { docs }
|
|
54
66
|
}
|
|
@@ -466,8 +478,13 @@ describe('DocRegistry', () => {
|
|
|
466
478
|
expect(doc.kind).toBe('stream')
|
|
467
479
|
})
|
|
468
480
|
|
|
481
|
+
it('httpStreamDoc fixture requires kind field', () => {
|
|
482
|
+
const doc = httpStreamDoc
|
|
483
|
+
expect(doc.kind).toBe('http-stream')
|
|
484
|
+
})
|
|
485
|
+
|
|
469
486
|
it('narrows AnyHttpRouteDoc union via kind', () => {
|
|
470
|
-
const routes: AnyHttpRouteDoc[] = [rpcDoc, apiDoc, streamDoc]
|
|
487
|
+
const routes: AnyHttpRouteDoc[] = [rpcDoc, apiDoc, streamDoc, httpStreamDoc]
|
|
471
488
|
for (const route of routes) {
|
|
472
489
|
expect(route.kind).toBeDefined()
|
|
473
490
|
}
|
|
@@ -580,7 +597,27 @@ describe('DocRegistry', () => {
|
|
|
580
597
|
})
|
|
581
598
|
})
|
|
582
599
|
|
|
583
|
-
|
|
600
|
+
it('aggregates http-stream routes from HonoAppBuilder', () => {
|
|
601
|
+
const procs = Procedures()
|
|
602
|
+
procs.CreateHttpStream('Tail', {
|
|
603
|
+
path: '/streams/logs',
|
|
604
|
+
method: 'get',
|
|
605
|
+
schema: {
|
|
606
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
607
|
+
yield: Type.Object({ line: Type.String() }),
|
|
608
|
+
},
|
|
609
|
+
}, async function* () {})
|
|
610
|
+
|
|
611
|
+
const builder = new HonoAppBuilder().register(procs, {})
|
|
612
|
+
builder.build()
|
|
613
|
+
|
|
614
|
+
const registry = new DocRegistry().from(builder)
|
|
615
|
+
const envelope = registry.toJSON()
|
|
616
|
+
const route = envelope.routes.find((r) => r.name === 'Tail')
|
|
617
|
+
expect(route?.kind).toBe('http-stream')
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
test('accepts a real HonoAppBuilder as source', () => {
|
|
584
621
|
const RPC = Procedures<{ userId: string }, RPCConfig>()
|
|
585
622
|
|
|
586
623
|
RPC.Create(
|
|
@@ -589,7 +626,7 @@ describe('DocRegistry', () => {
|
|
|
589
626
|
async (_ctx, params) => params,
|
|
590
627
|
)
|
|
591
628
|
|
|
592
|
-
const builder = new
|
|
629
|
+
const builder = new HonoAppBuilder()
|
|
593
630
|
builder.register(RPC, () => ({ userId: '123' }))
|
|
594
631
|
builder.build()
|
|
595
632
|
|
|
@@ -89,7 +89,7 @@ export class DocRegistry {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Surface coverage gaps — when a builder skipped procedures (e.g. a
|
|
92
|
-
// streaming procedure registered with
|
|
92
|
+
// streaming procedure registered with HonoAppBuilder), the per-builder
|
|
93
93
|
// warning fires once at `build()` time, but consumers who only call
|
|
94
94
|
// `registry.toJSON()` (e.g. to dump the envelope to disk for codegen)
|
|
95
95
|
// wouldn't see it without this aggregate. Single warning per call,
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import { dispatchMidStreamError, dispatchPreStreamError } from './error-dispatch.js'
|
|
4
|
+
import { defineErrorTaxonomy } from './error-taxonomy.js'
|
|
5
|
+
import { ProcedureValidationError } from '../../errors.js'
|
|
6
|
+
|
|
7
|
+
function makeContext() {
|
|
8
|
+
return new Promise<import('hono').Context>((resolve) => {
|
|
9
|
+
const app = new Hono()
|
|
10
|
+
app.get('/', (c) => {
|
|
11
|
+
resolve(c)
|
|
12
|
+
return c.text('ok')
|
|
13
|
+
})
|
|
14
|
+
app.request('/')
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const dummyProcedure = {
|
|
19
|
+
name: 'Test',
|
|
20
|
+
kind: 'rpc',
|
|
21
|
+
config: { scope: 'test', version: 1 },
|
|
22
|
+
handler: async () => undefined,
|
|
23
|
+
} as any
|
|
24
|
+
|
|
25
|
+
describe('dispatchPreStreamError', () => {
|
|
26
|
+
test('default taxonomy: ProcedureValidationError → 400', async () => {
|
|
27
|
+
const c = await makeContext()
|
|
28
|
+
const err = new ProcedureValidationError('Test', 'Validation error for Test', [
|
|
29
|
+
{ instancePath: '/x', message: 'expected number' } as any,
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
const res = await dispatchPreStreamError({
|
|
33
|
+
err,
|
|
34
|
+
procedure: dummyProcedure,
|
|
35
|
+
raw: c,
|
|
36
|
+
cfg: {},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(res.status).toBe(400)
|
|
40
|
+
const body = await res.json()
|
|
41
|
+
expect(body.name).toBe('ProcedureValidationError')
|
|
42
|
+
expect(body.procedureName).toBe('Test')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('user taxonomy entry takes precedence', async () => {
|
|
46
|
+
const c = await makeContext()
|
|
47
|
+
class MyError extends Error {
|
|
48
|
+
constructor(public reason: string) { super(reason) }
|
|
49
|
+
}
|
|
50
|
+
const errors = defineErrorTaxonomy({
|
|
51
|
+
MyError: { class: MyError, statusCode: 422, toResponse: (e) => ({ name: 'MyError', reason: e.reason }) },
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const res = await dispatchPreStreamError({
|
|
55
|
+
err: new MyError('boom'),
|
|
56
|
+
procedure: dummyProcedure,
|
|
57
|
+
raw: c,
|
|
58
|
+
cfg: { errors },
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(res.status).toBe(422)
|
|
62
|
+
expect(await res.json()).toEqual({ name: 'MyError', reason: 'boom' })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('falls back to onError imperative callback', async () => {
|
|
66
|
+
const c = await makeContext()
|
|
67
|
+
const onError = vi.fn().mockResolvedValue(c.json({ ok: false }, 503))
|
|
68
|
+
|
|
69
|
+
const res = await dispatchPreStreamError({
|
|
70
|
+
err: new Error('unmatched'),
|
|
71
|
+
procedure: dummyProcedure,
|
|
72
|
+
raw: c,
|
|
73
|
+
cfg: { onError: (proc, raw, e) => onError(proc, raw, e) },
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(onError).toHaveBeenCalled()
|
|
77
|
+
expect(res.status).toBe(503)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('hard default 500 when nothing matches', async () => {
|
|
81
|
+
const c = await makeContext()
|
|
82
|
+
const res = await dispatchPreStreamError({
|
|
83
|
+
err: new Error('boom'),
|
|
84
|
+
procedure: dummyProcedure,
|
|
85
|
+
raw: c,
|
|
86
|
+
cfg: {},
|
|
87
|
+
})
|
|
88
|
+
expect(res.status).toBe(500)
|
|
89
|
+
expect(await res.json()).toEqual({ error: 'boom' })
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('onRequestError observer fires before dispatch and is awaited', async () => {
|
|
93
|
+
const c = await makeContext()
|
|
94
|
+
const calls: string[] = []
|
|
95
|
+
const onRequestError = vi.fn(async () => {
|
|
96
|
+
calls.push('observer')
|
|
97
|
+
})
|
|
98
|
+
const errors = defineErrorTaxonomy({
|
|
99
|
+
AnyError: {
|
|
100
|
+
match: (e): e is Error => e instanceof Error,
|
|
101
|
+
statusCode: 500,
|
|
102
|
+
toResponse: () => {
|
|
103
|
+
calls.push('toResponse')
|
|
104
|
+
return { name: 'AnyError' }
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
await dispatchPreStreamError({
|
|
110
|
+
err: new Error('x'),
|
|
111
|
+
procedure: dummyProcedure,
|
|
112
|
+
raw: c,
|
|
113
|
+
cfg: { errors, onRequestError },
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(calls).toEqual(['observer', 'toResponse'])
|
|
117
|
+
expect(onRequestError).toHaveBeenCalledWith({ err: expect.any(Error), procedure: dummyProcedure, raw: c })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('observer throw is swallowed and logged', async () => {
|
|
121
|
+
const c = await makeContext()
|
|
122
|
+
const consoleErr = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
123
|
+
const onRequestError = vi.fn(() => { throw new Error('observer broke') })
|
|
124
|
+
|
|
125
|
+
const res = await dispatchPreStreamError({
|
|
126
|
+
err: new Error('x'),
|
|
127
|
+
procedure: dummyProcedure,
|
|
128
|
+
raw: c,
|
|
129
|
+
cfg: { onRequestError },
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(res.status).toBe(500)
|
|
133
|
+
expect(consoleErr).toHaveBeenCalled()
|
|
134
|
+
consoleErr.mockRestore()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('taxonomy onCatch is fully awaited before the response body is written', async () => {
|
|
138
|
+
const c = await makeContext()
|
|
139
|
+
const calls: string[] = []
|
|
140
|
+
class HookedError extends Error {}
|
|
141
|
+
const errors = defineErrorTaxonomy({
|
|
142
|
+
HookedError: {
|
|
143
|
+
class: HookedError,
|
|
144
|
+
statusCode: 500,
|
|
145
|
+
toResponse: () => ({ name: 'HookedError' }),
|
|
146
|
+
onCatch: async () => {
|
|
147
|
+
// Awaiting an async setTimeout here means a non-awaiting dispatcher
|
|
148
|
+
// would write the response BEFORE this push runs. The order we capture
|
|
149
|
+
// in `calls` reveals which branch happened.
|
|
150
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 5))
|
|
151
|
+
calls.push('oncatch-done')
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Spy on c.json: when the dispatcher writes the response, record where in
|
|
157
|
+
// the timeline it happened relative to onCatch's push.
|
|
158
|
+
const originalJson = c.json.bind(c) as (...args: any[]) => Response
|
|
159
|
+
const jsonSpy = vi.fn((...args: any[]) => {
|
|
160
|
+
calls.push('response-write')
|
|
161
|
+
return originalJson(...args)
|
|
162
|
+
})
|
|
163
|
+
;(c as any).json = jsonSpy
|
|
164
|
+
|
|
165
|
+
const res = await dispatchPreStreamError({
|
|
166
|
+
err: new HookedError('boom'),
|
|
167
|
+
procedure: dummyProcedure,
|
|
168
|
+
raw: c,
|
|
169
|
+
cfg: { errors },
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
expect(res.status).toBe(500)
|
|
173
|
+
expect(jsonSpy).toHaveBeenCalledTimes(1)
|
|
174
|
+
// If the dispatcher awaits onCatch first, 'oncatch-done' precedes
|
|
175
|
+
// 'response-write'. Without the await, the order would be reversed
|
|
176
|
+
// (or 'oncatch-done' would land after the dispatcher returns).
|
|
177
|
+
expect(calls).toEqual(['oncatch-done', 'response-write'])
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const streamProcedure = {
|
|
182
|
+
name: 'Tail',
|
|
183
|
+
kind: 'rpc-stream',
|
|
184
|
+
config: { scope: 'logs', version: 1 },
|
|
185
|
+
handler: async function* () {},
|
|
186
|
+
} as any
|
|
187
|
+
|
|
188
|
+
describe('dispatchMidStreamError', () => {
|
|
189
|
+
test('uses taxonomy body when configured', async () => {
|
|
190
|
+
const c = await makeContext()
|
|
191
|
+
class MidErr extends Error {}
|
|
192
|
+
const errors = defineErrorTaxonomy({
|
|
193
|
+
MidErr: { class: MidErr, statusCode: 500, toResponse: () => ({ name: 'MidErr', detail: 'x' }) },
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const result = await dispatchMidStreamError({
|
|
197
|
+
err: new MidErr('mid'),
|
|
198
|
+
procedure: streamProcedure,
|
|
199
|
+
raw: c,
|
|
200
|
+
cfg: { errors },
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
expect(result.data).toEqual({ name: 'MidErr', detail: 'x' })
|
|
204
|
+
expect(result.sseEvent).toBe('error')
|
|
205
|
+
expect(result.runOnCatch).toBeTypeOf('function')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('falls back to onMidStreamError', async () => {
|
|
209
|
+
const c = await makeContext()
|
|
210
|
+
const result = await dispatchMidStreamError({
|
|
211
|
+
err: new Error('boom'),
|
|
212
|
+
procedure: streamProcedure,
|
|
213
|
+
raw: c,
|
|
214
|
+
cfg: {
|
|
215
|
+
onMidStreamError: () => ({ data: { kind: 'fail', msg: 'boom' } }),
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
expect(result.data).toEqual({ kind: 'fail', msg: 'boom' })
|
|
220
|
+
expect(result.sseEvent).toBe('Tail') // procedure name override
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('hard default { error: msg } when nothing matches', async () => {
|
|
224
|
+
const c = await makeContext()
|
|
225
|
+
const result = await dispatchMidStreamError({
|
|
226
|
+
err: new Error('plain'),
|
|
227
|
+
procedure: streamProcedure,
|
|
228
|
+
raw: c,
|
|
229
|
+
cfg: {},
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
expect(result.data).toEqual({ error: 'plain' })
|
|
233
|
+
expect(result.sseEvent).toBe('error')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('onRequestError observer fires before dispatch (mid-stream form)', async () => {
|
|
237
|
+
const c = await makeContext()
|
|
238
|
+
const onRequestError = vi.fn(async () => {})
|
|
239
|
+
|
|
240
|
+
await dispatchMidStreamError({
|
|
241
|
+
err: new Error('x'),
|
|
242
|
+
procedure: streamProcedure,
|
|
243
|
+
raw: c,
|
|
244
|
+
cfg: { onRequestError },
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
expect(onRequestError).toHaveBeenCalled()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('returns a runOnCatch function that is invokable and awaits the taxonomy hook', async () => {
|
|
251
|
+
const c = await makeContext()
|
|
252
|
+
const calls: string[] = []
|
|
253
|
+
class StreamErr extends Error {}
|
|
254
|
+
const errors = defineErrorTaxonomy({
|
|
255
|
+
StreamErr: {
|
|
256
|
+
class: StreamErr,
|
|
257
|
+
statusCode: 500,
|
|
258
|
+
toResponse: () => ({ name: 'StreamErr' }),
|
|
259
|
+
onCatch: async () => {
|
|
260
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 5))
|
|
261
|
+
calls.push('oncatch-done')
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Mid-stream contract: the dispatcher does NOT call runOnCatch itself —
|
|
267
|
+
// the caller (stream handler) decides when to invoke it. Verify the shape
|
|
268
|
+
// and that awaiting it actually awaits the hook.
|
|
269
|
+
const result = await dispatchMidStreamError({
|
|
270
|
+
err: new StreamErr('mid'),
|
|
271
|
+
procedure: streamProcedure,
|
|
272
|
+
raw: c,
|
|
273
|
+
cfg: { errors },
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
expect(result.runOnCatch).toBeTypeOf('function')
|
|
277
|
+
// Hook has not yet run — caller hasn't invoked it.
|
|
278
|
+
expect(calls).toEqual([])
|
|
279
|
+
|
|
280
|
+
await result.runOnCatch!()
|
|
281
|
+
expect(calls).toEqual(['oncatch-done'])
|
|
282
|
+
})
|
|
283
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { Context } from 'hono'
|
|
2
|
+
import {
|
|
3
|
+
resolveErrorResponse,
|
|
4
|
+
type ErrorTaxonomy,
|
|
5
|
+
type TAnyProcedureRegistration,
|
|
6
|
+
type UnknownErrorConfig,
|
|
7
|
+
} from './error-taxonomy.js'
|
|
8
|
+
import type { TStreamProcedureRegistration, THttpStreamProcedureRegistration } from '../../types.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Public alias of {@link TAnyProcedureRegistration} — re-exported here so the
|
|
12
|
+
* hono/ module can import it from one place without taking a direct dependency
|
|
13
|
+
* on error-taxonomy internals. Single source of truth lives in
|
|
14
|
+
* error-taxonomy.ts.
|
|
15
|
+
*/
|
|
16
|
+
export type AnyProcedureRegistration = TAnyProcedureRegistration
|
|
17
|
+
|
|
18
|
+
export type OnRequestErrorContext = {
|
|
19
|
+
err: unknown
|
|
20
|
+
procedure: AnyProcedureRegistration
|
|
21
|
+
raw: Context
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type OnRequestErrorObserver = (
|
|
25
|
+
ctx: OnRequestErrorContext,
|
|
26
|
+
) => void | Promise<void>
|
|
27
|
+
|
|
28
|
+
export type PreStreamOnError = (
|
|
29
|
+
procedure: AnyProcedureRegistration,
|
|
30
|
+
raw: Context,
|
|
31
|
+
err: Error,
|
|
32
|
+
) => Response | Promise<Response>
|
|
33
|
+
|
|
34
|
+
export type PreStreamErrorConfig = {
|
|
35
|
+
errors?: ErrorTaxonomy
|
|
36
|
+
unknownError?: UnknownErrorConfig
|
|
37
|
+
onError?: PreStreamOnError
|
|
38
|
+
onRequestError?: OnRequestErrorObserver
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Used by rpc, http, and the pre-stream guard in stream / http-stream
|
|
43
|
+
* handlers. Order: onRequestError observer (awaited, throws swallowed) →
|
|
44
|
+
* taxonomy resolver → imperative onError → hard default 500.
|
|
45
|
+
*/
|
|
46
|
+
export async function dispatchPreStreamError(params: {
|
|
47
|
+
err: unknown
|
|
48
|
+
procedure: AnyProcedureRegistration
|
|
49
|
+
raw: Context
|
|
50
|
+
cfg: PreStreamErrorConfig
|
|
51
|
+
}): Promise<Response> {
|
|
52
|
+
const { err, procedure, raw, cfg } = params
|
|
53
|
+
|
|
54
|
+
if (cfg.onRequestError) {
|
|
55
|
+
try {
|
|
56
|
+
await cfg.onRequestError({ err, procedure, raw })
|
|
57
|
+
} catch (observerErr) {
|
|
58
|
+
console.error(
|
|
59
|
+
'[ts-procedures hono] onRequestError threw — swallowed:',
|
|
60
|
+
observerErr,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resolved = resolveErrorResponse({
|
|
66
|
+
err,
|
|
67
|
+
userTaxonomy: cfg.errors,
|
|
68
|
+
unknownError: cfg.unknownError,
|
|
69
|
+
procedure,
|
|
70
|
+
raw,
|
|
71
|
+
})
|
|
72
|
+
if (resolved) {
|
|
73
|
+
await resolved.runOnCatch()
|
|
74
|
+
return raw.json(resolved.body, resolved.statusCode as never)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (cfg.onError) {
|
|
78
|
+
return cfg.onError(procedure, raw, err as Error)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return raw.json({ error: (err as Error).message }, 500)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Return shape of an `onMidStreamError` callback.
|
|
86
|
+
*
|
|
87
|
+
* - `data`: the value to write to the open stream as the error payload.
|
|
88
|
+
* Decorate with the `sse(data, { event, id, retry })` helper from
|
|
89
|
+
* `hono/handlers/stream` to attach SSE metadata; the stream handler
|
|
90
|
+
* reads that metadata via `getSSEMeta` after dispatch.
|
|
91
|
+
* - `closeStream`: reserved for future use. The stream currently always
|
|
92
|
+
* closes naturally after the catch block returns; this flag is kept on
|
|
93
|
+
* the public type for forward compatibility but the dispatcher does not
|
|
94
|
+
* read it.
|
|
95
|
+
*/
|
|
96
|
+
export type MidStreamErrorResult<TErrorData = unknown> = {
|
|
97
|
+
data: TErrorData
|
|
98
|
+
closeStream?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type OnMidStreamError<TErrorData = unknown> = (
|
|
102
|
+
procedure: TStreamProcedureRegistration | THttpStreamProcedureRegistration<any>,
|
|
103
|
+
raw: Context,
|
|
104
|
+
err: Error,
|
|
105
|
+
) => MidStreamErrorResult<TErrorData> | undefined
|
|
106
|
+
|
|
107
|
+
export type MidStreamErrorConfig<TErrorData = unknown> = {
|
|
108
|
+
errors?: ErrorTaxonomy
|
|
109
|
+
unknownError?: UnknownErrorConfig
|
|
110
|
+
onMidStreamError?: OnMidStreamError<TErrorData>
|
|
111
|
+
onRequestError?: OnRequestErrorObserver
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type DispatchedMidStreamError = {
|
|
115
|
+
data: unknown
|
|
116
|
+
sseEvent?: string
|
|
117
|
+
sseId?: string
|
|
118
|
+
sseRetry?: number
|
|
119
|
+
runOnCatch?: () => Promise<void>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Used inside SSE / text-stream generator catch blocks. Returns the bytes
|
|
124
|
+
* to write to the open stream — the HTTP status is already committed, so we
|
|
125
|
+
* can't return a Response. Order matches dispatchPreStreamError: observer
|
|
126
|
+
* (awaited, throws swallowed) → taxonomy → onMidStreamError → hard default.
|
|
127
|
+
*/
|
|
128
|
+
export async function dispatchMidStreamError<TErrorData = unknown>(params: {
|
|
129
|
+
err: unknown
|
|
130
|
+
procedure: TStreamProcedureRegistration | THttpStreamProcedureRegistration<any>
|
|
131
|
+
raw: Context
|
|
132
|
+
cfg: MidStreamErrorConfig<TErrorData>
|
|
133
|
+
}): Promise<DispatchedMidStreamError> {
|
|
134
|
+
const { err, procedure, raw, cfg } = params
|
|
135
|
+
|
|
136
|
+
if (cfg.onRequestError) {
|
|
137
|
+
try {
|
|
138
|
+
await cfg.onRequestError({ err, procedure, raw })
|
|
139
|
+
} catch (observerErr) {
|
|
140
|
+
console.error(
|
|
141
|
+
'[ts-procedures hono] onRequestError (mid-stream) threw — swallowed:',
|
|
142
|
+
observerErr,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const resolved = resolveErrorResponse({
|
|
148
|
+
err,
|
|
149
|
+
userTaxonomy: cfg.errors,
|
|
150
|
+
unknownError: cfg.unknownError,
|
|
151
|
+
procedure,
|
|
152
|
+
raw,
|
|
153
|
+
})
|
|
154
|
+
if (resolved) {
|
|
155
|
+
return {
|
|
156
|
+
data: resolved.body,
|
|
157
|
+
sseEvent: 'error',
|
|
158
|
+
runOnCatch: resolved.runOnCatch,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (cfg.onMidStreamError) {
|
|
163
|
+
const result = cfg.onMidStreamError(procedure, raw, err as Error)
|
|
164
|
+
if (result?.data !== undefined) {
|
|
165
|
+
return {
|
|
166
|
+
data: result.data,
|
|
167
|
+
sseEvent: procedure.name,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
data: { error: (err as Error).message },
|
|
174
|
+
sseEvent: 'error',
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
ProcedureValidationError,
|
|
4
4
|
ProcedureYieldValidationError,
|
|
5
5
|
} from '../../errors.js'
|
|
6
|
-
import type { TProcedureRegistration, TStreamProcedureRegistration } from '../../index.js'
|
|
6
|
+
import type { TProcedureRegistration, TStreamProcedureRegistration, THttpProcedureRegistration, THttpStreamProcedureRegistration } from '../../index.js'
|
|
7
7
|
import type { ErrorDoc } from '../types.js'
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
export type TAnyProcedureRegistration = TProcedureRegistration | TStreamProcedureRegistration
|
|
9
|
+
/** Any procedure registration — accepted by taxonomy callbacks. */
|
|
10
|
+
export type TAnyProcedureRegistration = TProcedureRegistration | TStreamProcedureRegistration | THttpProcedureRegistration<any> | THttpStreamProcedureRegistration<any>
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* An entry in an {@link ErrorTaxonomy}. Describes how to recognize a thrown
|
|
@@ -35,8 +35,8 @@ export type ErrorTaxonomyEntry<TError = any, TBody = unknown> = {
|
|
|
35
35
|
toResponse?: (err: TError, meta: { key: string }) => TBody
|
|
36
36
|
/**
|
|
37
37
|
* Side effect on catch (logging, metrics). Awaited before the response is sent.
|
|
38
|
-
* `raw` is the framework-specific request context (Hono `Context`
|
|
39
|
-
*
|
|
38
|
+
* `raw` is the framework-specific request context (e.g., Hono `Context`) —
|
|
39
|
+
* cast as needed.
|
|
40
40
|
*/
|
|
41
41
|
onCatch?: (
|
|
42
42
|
err: TError,
|