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