@lovrozagar/honey 0.6.8 → 0.7.1
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 +514 -192
- package/bin/honey.js +25 -0
- package/dist/accepts.d.ts +7 -1
- package/dist/accepts.d.ts.map +1 -1
- package/dist/accepts.js +126 -0
- package/dist/accepts.js.map +1 -0
- package/dist/body-kind.d.ts +26 -0
- package/dist/body-kind.d.ts.map +1 -0
- package/dist/body-kind.js +70 -0
- package/dist/body-kind.js.map +1 -0
- package/dist/body-limit.d.ts +5 -1
- package/dist/body-limit.d.ts.map +1 -1
- package/dist/body-limit.js +126 -0
- package/dist/body-limit.js.map +1 -0
- package/dist/build/index.d.ts +19 -3
- package/dist/build/index.d.ts.map +1 -1
- package/dist/build/index.js +173 -0
- package/dist/build/index.js.map +1 -0
- package/dist/cli-args.d.ts +33 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +102 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-go/auth.go +26 -0
- package/dist/cli-go/config.go +192 -0
- package/dist/cli-go/errors.go +75 -0
- package/dist/cli-go/multipart.go +79 -0
- package/dist/cli-go/output.go +163 -0
- package/dist/cli-go/root.go +30 -0
- package/dist/cli-go/stream.go +88 -0
- package/dist/cli-go/version.go +5 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +209 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/defaults.js +48 -0
- package/dist/client/defaults.js.map +1 -0
- package/dist/client/error.d.ts +35 -0
- package/dist/client/error.d.ts.map +1 -1
- package/dist/client/error.js +71 -0
- package/dist/client/error.js.map +1 -0
- package/dist/client/http.d.ts +67 -8
- package/dist/client/http.d.ts.map +1 -1
- package/dist/client/http.js +571 -0
- package/dist/client/http.js.map +1 -0
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +95 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/path.d.ts +26 -0
- package/dist/client/path.d.ts.map +1 -0
- package/dist/client/path.js +163 -0
- package/dist/client/path.js.map +1 -0
- package/dist/client/redirect-policy.d.ts +12 -0
- package/dist/client/redirect-policy.d.ts.map +1 -0
- package/dist/client/redirect-policy.js +20 -0
- package/dist/client/redirect-policy.js.map +1 -0
- package/dist/client/sdk.d.ts +36 -0
- package/dist/client/sdk.d.ts.map +1 -1
- package/dist/client/sdk.js +292 -0
- package/dist/client/sdk.js.map +1 -0
- package/dist/client/sse.d.ts +13 -0
- package/dist/client/sse.d.ts.map +1 -1
- package/dist/client/sse.js +129 -0
- package/dist/client/sse.js.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/ws.d.ts +9 -2
- package/dist/client/ws.d.ts.map +1 -1
- package/dist/client/ws.js +112 -0
- package/dist/client/ws.js.map +1 -0
- package/dist/client-go/errors.go +236 -0
- package/dist/client-go/export.go +44 -0
- package/dist/client-go/invalidation.go +371 -0
- package/dist/client-go/nullable.go +62 -0
- package/dist/client-go/realtime.go +354 -0
- package/dist/client-go/result.go +20 -0
- package/dist/client-go/runtime.go +975 -0
- package/dist/client-go/sse.go +159 -0
- package/dist/client-go/transport.go +495 -0
- package/dist/client-go/ws.go +128 -0
- package/dist/client-info.d.ts +36 -0
- package/dist/client-info.d.ts.map +1 -0
- package/dist/client-info.js +53 -0
- package/dist/client-info.js.map +1 -0
- package/dist/client-mcp/README.md +10 -0
- package/dist/client-mcp/server.ts +221 -0
- package/dist/client-mcp/types.ts +19 -0
- package/dist/client-python/_errors.py +115 -0
- package/dist/client-python/_invalidation.py +494 -0
- package/dist/client-python/_realtime.py +179 -0
- package/dist/client-python/_runtime.py +860 -0
- package/dist/client-python/_sse.py +155 -0
- package/dist/client-python/_transport.py +366 -0
- package/dist/client-python/_ws.py +159 -0
- package/dist/client-python/py.typed +0 -0
- package/dist/client-rust/errors.rs +220 -0
- package/dist/client-rust/invalidation.rs +412 -0
- package/dist/client-rust/invalidation_sync.rs +93 -0
- package/dist/client-rust/nullable.rs +17 -0
- package/dist/client-rust/realtime.rs +702 -0
- package/dist/client-rust/result.rs +22 -0
- package/dist/client-rust/runtime.rs +980 -0
- package/dist/client-rust/runtime_sync.rs +484 -0
- package/dist/client-rust/sse.rs +188 -0
- package/dist/client-rust/ws.rs +167 -0
- package/dist/codegen-go-cli.d.ts.map +1 -1
- package/dist/codegen-go-cli.js +964 -0
- package/dist/codegen-go-cli.js.map +1 -0
- package/dist/codegen-go.d.ts +15 -3
- package/dist/codegen-go.d.ts.map +1 -1
- package/dist/codegen-go.js +872 -0
- package/dist/codegen-go.js.map +1 -0
- package/dist/codegen-hash.js +9 -0
- package/dist/codegen-hash.js.map +1 -0
- package/dist/codegen-ir.d.ts +69 -2
- package/dist/codegen-ir.d.ts.map +1 -1
- package/dist/codegen-ir.js +661 -0
- package/dist/codegen-ir.js.map +1 -0
- package/dist/codegen-lang.d.ts +65 -0
- package/dist/codegen-lang.d.ts.map +1 -0
- package/dist/codegen-lang.js +526 -0
- package/dist/codegen-lang.js.map +1 -0
- package/dist/codegen-load.d.ts +31 -0
- package/dist/codegen-load.d.ts.map +1 -0
- package/dist/codegen-load.js +138 -0
- package/dist/codegen-load.js.map +1 -0
- package/dist/codegen-loaders.js +55 -0
- package/dist/codegen-loaders.js.map +1 -0
- package/dist/codegen-mcp.d.ts.map +1 -1
- package/dist/codegen-mcp.js +368 -0
- package/dist/codegen-mcp.js.map +1 -0
- package/dist/codegen-python.d.ts +13 -1
- package/dist/codegen-python.d.ts.map +1 -1
- package/dist/codegen-python.js +828 -0
- package/dist/codegen-python.js.map +1 -0
- package/dist/codegen-route-tree-intern.d.ts +15 -5
- package/dist/codegen-route-tree-intern.d.ts.map +1 -1
- package/dist/codegen-route-tree-intern.js +221 -0
- package/dist/codegen-route-tree-intern.js.map +1 -0
- package/dist/codegen-rust.d.ts.map +1 -1
- package/dist/codegen-rust.js +1293 -0
- package/dist/codegen-rust.js.map +1 -0
- package/dist/codegen-sanitize.d.ts.map +1 -1
- package/dist/codegen-sanitize.js +56 -0
- package/dist/codegen-sanitize.js.map +1 -0
- package/dist/codegen-schema-naming.d.ts +5 -0
- package/dist/codegen-schema-naming.d.ts.map +1 -1
- package/dist/codegen-schema-naming.js +150 -0
- package/dist/codegen-schema-naming.js.map +1 -0
- package/dist/codegen-sdk-model.d.ts +71 -0
- package/dist/codegen-sdk-model.d.ts.map +1 -0
- package/dist/codegen-sdk-model.js +179 -0
- package/dist/codegen-sdk-model.js.map +1 -0
- package/dist/codegen.d.ts +41 -23
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +4610 -0
- package/dist/codegen.js.map +1 -0
- package/dist/context.d.ts +36 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +242 -0
- package/dist/context.js.map +1 -0
- package/dist/cookie-sign.d.ts +16 -2
- package/dist/cookie-sign.d.ts.map +1 -1
- package/dist/cookie-sign.js +104 -0
- package/dist/cookie-sign.js.map +1 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/cookie.d.ts.map +1 -1
- package/dist/cookie.js +138 -0
- package/dist/cookie.js.map +1 -0
- package/dist/cors.d.ts +1 -0
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +87 -0
- package/dist/cors.js.map +1 -0
- package/dist/crypto.d.ts +6 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +38 -0
- package/dist/crypto.js.map +1 -0
- package/dist/csrf.d.ts +19 -0
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +79 -0
- package/dist/csrf.js.map +1 -0
- package/dist/curl-logger.d.ts +14 -3
- package/dist/curl-logger.d.ts.map +1 -1
- package/dist/curl-logger.js +199 -0
- package/dist/curl-logger.js.map +1 -0
- package/dist/detect-runtime.js +13 -0
- package/dist/detect-runtime.js.map +1 -0
- package/dist/dict.d.ts +11 -0
- package/dist/dict.d.ts.map +1 -0
- package/dist/dict.js +13 -0
- package/dist/dict.js.map +1 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +44 -0
- package/dist/error.js.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/etag.d.ts +2 -0
- package/dist/etag.d.ts.map +1 -1
- package/dist/etag.js +82 -0
- package/dist/etag.js.map +1 -0
- package/dist/feature-detect.d.ts +25 -0
- package/dist/feature-detect.d.ts.map +1 -0
- package/dist/feature-detect.js +144 -0
- package/dist/feature-detect.js.map +1 -0
- package/dist/feature-load.js +36 -0
- package/dist/feature-load.js.map +1 -0
- package/dist/feature-slots.d.ts +44 -0
- package/dist/feature-slots.d.ts.map +1 -0
- package/dist/feature-slots.js +48 -0
- package/dist/feature-slots.js.map +1 -0
- package/dist/fetch-request.d.ts +10 -0
- package/dist/fetch-request.d.ts.map +1 -0
- package/dist/fetch-request.js +13 -0
- package/dist/fetch-request.js.map +1 -0
- package/dist/gen-process.d.ts +15 -0
- package/dist/gen-process.d.ts.map +1 -0
- package/dist/gen-process.js +43 -0
- package/dist/gen-process.js.map +1 -0
- package/dist/gen-write.d.ts +19 -0
- package/dist/gen-write.d.ts.map +1 -0
- package/dist/gen-write.js +120 -0
- package/dist/gen-write.js.map +1 -0
- package/dist/glob.d.ts +21 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +115 -0
- package/dist/glob.js.map +1 -0
- package/dist/go-type-emitter.d.ts +18 -4
- package/dist/go-type-emitter.d.ts.map +1 -1
- package/dist/go-type-emitter.js +594 -0
- package/dist/go-type-emitter.js.map +1 -0
- package/dist/honey-response.d.ts.map +1 -1
- package/dist/honey-response.js +338 -0
- package/dist/honey-response.js.map +1 -0
- package/dist/i18n-slot.d.ts +8 -0
- package/dist/i18n-slot.d.ts.map +1 -1
- package/dist/i18n-slot.js +14 -0
- package/dist/i18n-slot.js.map +1 -0
- package/dist/i18n.d.ts +3 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +250 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +408 -148
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2324 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +3 -5
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +140 -0
- package/dist/init.js.map +1 -0
- package/dist/input.js +4 -0
- package/dist/input.js.map +1 -0
- package/dist/invalidate-check.d.ts +11 -0
- package/dist/invalidate-check.d.ts.map +1 -1
- package/dist/invalidate-check.js +163 -0
- package/dist/invalidate-check.js.map +1 -0
- package/dist/invoke-user.d.ts +35 -0
- package/dist/invoke-user.d.ts.map +1 -0
- package/dist/invoke-user.js +73 -0
- package/dist/invoke-user.js.map +1 -0
- package/dist/ip-restrict.d.ts +13 -1
- package/dist/ip-restrict.d.ts.map +1 -1
- package/dist/ip-restrict.js +60 -0
- package/dist/ip-restrict.js.map +1 -0
- package/dist/ip.d.ts +39 -0
- package/dist/ip.d.ts.map +1 -0
- package/dist/ip.js +209 -0
- package/dist/ip.js.map +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +113 -0
- package/dist/logger.js.map +1 -0
- package/dist/media-type.d.ts +30 -0
- package/dist/media-type.d.ts.map +1 -0
- package/dist/media-type.js +97 -0
- package/dist/media-type.js.map +1 -0
- package/dist/meta-spec-merge.d.ts +32 -0
- package/dist/meta-spec-merge.d.ts.map +1 -0
- package/dist/meta-spec-merge.js +68 -0
- package/dist/meta-spec-merge.js.map +1 -0
- package/dist/meta-spec.d.ts +16 -3
- package/dist/meta-spec.d.ts.map +1 -1
- package/dist/meta-spec.js +535 -0
- package/dist/meta-spec.js.map +1 -0
- package/dist/middleware.d.ts +23 -6
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +253 -0
- package/dist/middleware.js.map +1 -0
- package/dist/node-request.d.ts +57 -12
- package/dist/node-request.d.ts.map +1 -1
- package/dist/node-request.js +407 -0
- package/dist/node-request.js.map +1 -0
- package/dist/node.d.ts +20 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +396 -0
- package/dist/node.js.map +1 -0
- package/dist/openapi/collect.d.ts +6 -3
- package/dist/openapi/collect.d.ts.map +1 -1
- package/dist/openapi/collect.js +30 -0
- package/dist/openapi/collect.js.map +1 -0
- package/dist/openapi/docs-page.d.ts +42 -0
- package/dist/openapi/docs-page.d.ts.map +1 -0
- package/dist/openapi/docs-page.js +80 -0
- package/dist/openapi/docs-page.js.map +1 -0
- package/dist/openapi/document.d.ts +4 -0
- package/dist/openapi/document.d.ts.map +1 -1
- package/dist/openapi/document.js +785 -0
- package/dist/openapi/document.js.map +1 -0
- package/dist/openapi/json-schema-slot.d.ts +7 -0
- package/dist/openapi/json-schema-slot.d.ts.map +1 -1
- package/dist/openapi/json-schema-slot.js +38 -0
- package/dist/openapi/json-schema-slot.js.map +1 -0
- package/dist/openapi/register.d.ts.map +1 -1
- package/dist/openapi/register.js +20 -0
- package/dist/openapi/register.js.map +1 -0
- package/dist/openapi/scalar.d.ts +3 -1
- package/dist/openapi/scalar.d.ts.map +1 -1
- package/dist/openapi/scalar.js +21 -0
- package/dist/openapi/scalar.js.map +1 -0
- package/dist/openapi/serve.d.ts +9 -0
- package/dist/openapi/serve.d.ts.map +1 -0
- package/dist/openapi/serve.js +36 -0
- package/dist/openapi/serve.js.map +1 -0
- package/dist/openapi/spec-factory.d.ts +30 -1
- package/dist/openapi/spec-factory.d.ts.map +1 -1
- package/dist/openapi/spec-factory.js +52 -0
- package/dist/openapi/spec-factory.js.map +1 -0
- package/dist/openapi/spec.d.ts +14 -5
- package/dist/openapi/spec.d.ts.map +1 -1
- package/dist/openapi/spec.js +50 -0
- package/dist/openapi/spec.js.map +1 -0
- package/dist/openapi/swagger.d.ts +3 -1
- package/dist/openapi/swagger.d.ts.map +1 -1
- package/dist/openapi/swagger.js +22 -0
- package/dist/openapi/swagger.js.map +1 -0
- package/dist/pattern.d.ts +71 -0
- package/dist/pattern.d.ts.map +1 -0
- package/dist/pattern.js +239 -0
- package/dist/pattern.js.map +1 -0
- package/dist/peer.d.ts +17 -0
- package/dist/peer.d.ts.map +1 -0
- package/dist/peer.js +50 -0
- package/dist/peer.js.map +1 -0
- package/dist/plugin.d.ts +67 -16
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +649 -0
- package/dist/plugin.js.map +1 -0
- package/dist/powered-by.d.ts.map +1 -1
- package/dist/powered-by.js +11 -0
- package/dist/powered-by.js.map +1 -0
- package/dist/pretty-json.d.ts.map +1 -1
- package/dist/pretty-json.js +47 -0
- package/dist/pretty-json.js.map +1 -0
- package/dist/producer-stream.d.ts +47 -0
- package/dist/producer-stream.d.ts.map +1 -0
- package/dist/producer-stream.js +267 -0
- package/dist/producer-stream.js.map +1 -0
- package/dist/proxy.d.ts +47 -8
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +274 -0
- package/dist/proxy.js.map +1 -0
- package/dist/python-type-emitter.d.ts +14 -1
- package/dist/python-type-emitter.d.ts.map +1 -1
- package/dist/python-type-emitter.js +157 -0
- package/dist/python-type-emitter.js.map +1 -0
- package/dist/realtime/bus.d.ts +17 -1
- package/dist/realtime/bus.d.ts.map +1 -1
- package/dist/realtime/bus.js +112 -0
- package/dist/realtime/bus.js.map +1 -0
- package/dist/realtime/register.d.ts +3 -0
- package/dist/realtime/register.d.ts.map +1 -0
- package/dist/realtime/register.js +10 -0
- package/dist/realtime/register.js.map +1 -0
- package/dist/realtime/route.d.ts +65 -26
- package/dist/realtime/route.d.ts.map +1 -1
- package/dist/realtime/route.js +72 -0
- package/dist/realtime/route.js.map +1 -0
- package/dist/realtime/server.d.ts +43 -0
- package/dist/realtime/server.d.ts.map +1 -0
- package/dist/realtime/server.js +255 -0
- package/dist/realtime/server.js.map +1 -0
- package/dist/request-id.d.ts +7 -0
- package/dist/request-id.d.ts.map +1 -1
- package/dist/request-id.js +16 -0
- package/dist/request-id.js.map +1 -0
- package/dist/request-limits.d.ts +3 -0
- package/dist/request-limits.d.ts.map +1 -0
- package/dist/request-limits.js +3 -0
- package/dist/request-limits.js.map +1 -0
- package/dist/request-path.d.ts +33 -0
- package/dist/request-path.d.ts.map +1 -0
- package/dist/request-path.js +178 -0
- package/dist/request-path.js.map +1 -0
- package/dist/request-to-curl.d.ts +7 -1
- package/dist/request-to-curl.d.ts.map +1 -1
- package/dist/request-to-curl.js +48 -0
- package/dist/request-to-curl.js.map +1 -0
- package/dist/response.d.ts +25 -2
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +313 -0
- package/dist/response.js.map +1 -0
- package/dist/rust-type-emitter.d.ts +14 -2
- package/dist/rust-type-emitter.d.ts.map +1 -1
- package/dist/rust-type-emitter.js +463 -0
- package/dist/rust-type-emitter.js.map +1 -0
- package/dist/secure-headers.d.ts.map +1 -1
- package/dist/secure-headers.js +53 -0
- package/dist/secure-headers.js.map +1 -0
- package/dist/serve-register.d.ts +4 -0
- package/dist/serve-register.d.ts.map +1 -1
- package/dist/serve-register.js +9 -0
- package/dist/serve-register.js.map +1 -0
- package/dist/serve-slot.d.ts +3 -0
- package/dist/serve-slot.d.ts.map +1 -1
- package/dist/serve-slot.js +52 -0
- package/dist/serve-slot.js.map +1 -0
- package/dist/serve.d.ts +18 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +157 -0
- package/dist/serve.js.map +1 -0
- package/dist/server-timing.d.ts.map +1 -1
- package/dist/server-timing.js +51 -0
- package/dist/server-timing.js.map +1 -0
- package/dist/static.d.ts +9 -0
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +102 -0
- package/dist/static.js.map +1 -0
- package/dist/telemetry/otel.d.ts +2 -0
- package/dist/telemetry/otel.d.ts.map +1 -1
- package/dist/telemetry/otel.js +135 -0
- package/dist/telemetry/otel.js.map +1 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +124 -0
- package/dist/testing.js.map +1 -0
- package/dist/timeout.d.ts +6 -0
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +36 -0
- package/dist/timeout.js.map +1 -0
- package/dist/tree.d.ts +133 -36
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +437 -0
- package/dist/tree.js.map +1 -0
- package/dist/trust.d.ts +21 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +100 -0
- package/dist/trust.js.map +1 -0
- package/dist/ts-type-emitter.d.ts.map +1 -1
- package/dist/ts-type-emitter.js +150 -0
- package/dist/ts-type-emitter.js.map +1 -0
- package/dist/type-emitter.d.ts +16 -2
- package/dist/type-emitter.d.ts.map +1 -1
- package/dist/type-emitter.js +729 -0
- package/dist/type-emitter.js.map +1 -0
- package/dist/type-extractor.d.ts +8 -4
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +819 -0
- package/dist/type-extractor.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +96 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +14 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +335 -0
- package/dist/validation.js.map +1 -0
- package/dist/with-headers.d.ts +15 -0
- package/dist/with-headers.d.ts.map +1 -0
- package/dist/with-headers.js +45 -0
- package/dist/with-headers.js.map +1 -0
- package/dist/ws/bun.d.ts +11 -5
- package/dist/ws/bun.d.ts.map +1 -1
- package/dist/ws/bun.js +82 -0
- package/dist/ws/bun.js.map +1 -0
- package/dist/ws/cloudflare.d.ts +25 -3
- package/dist/ws/cloudflare.d.ts.map +1 -1
- package/dist/ws/cloudflare.js +117 -0
- package/dist/ws/cloudflare.js.map +1 -0
- package/dist/ws/deno.d.ts +4 -4
- package/dist/ws/deno.d.ts.map +1 -1
- package/dist/ws/deno.js +133 -0
- package/dist/ws/deno.js.map +1 -0
- package/dist/ws/node.d.ts +20 -3
- package/dist/ws/node.d.ts.map +1 -1
- package/dist/ws/node.js +173 -0
- package/dist/ws/node.js.map +1 -0
- package/dist/ws/session.d.ts +11 -0
- package/dist/ws/session.d.ts.map +1 -0
- package/dist/ws/session.js +60 -0
- package/dist/ws/session.js.map +1 -0
- package/dist/ws/shared.d.ts +65 -0
- package/dist/ws/shared.d.ts.map +1 -0
- package/dist/ws/shared.js +103 -0
- package/dist/ws/shared.js.map +1 -0
- package/dist/ws-origin.d.ts +20 -0
- package/dist/ws-origin.d.ts.map +1 -0
- package/dist/ws-origin.js +41 -0
- package/dist/ws-origin.js.map +1 -0
- package/dist/yaml.d.ts +15 -1
- package/dist/yaml.d.ts.map +1 -1
- package/dist/yaml.js +101 -0
- package/dist/yaml.js.map +1 -0
- package/docs/invalidate-spec.md +13 -1
- package/docs/meta-spec.md +36 -12
- package/docs/packaging.md +43 -24
- package/docs/regression-matrix/README.md +32 -0
- package/docs/regression-matrix/review.md +44 -0
- package/docs/regression-matrix/ws1-3.md +97 -0
- package/docs/regression-matrix/ws4-8.md +107 -0
- package/docs/regression-matrix/ws9-12.md +266 -0
- package/docs/sdk.md +102 -52
- package/package.json +183 -52
- package/src/accepts.ts +91 -33
- package/src/body-kind.ts +67 -0
- package/src/body-limit.ts +102 -87
- package/src/build/index.ts +104 -54
- package/src/cli-args.ts +123 -0
- package/src/cli-go/auth.go +12 -1
- package/src/cli-go/config.go +29 -2
- package/src/cli-go/errors.go +28 -10
- package/src/cli-go/multipart.go +51 -27
- package/src/cli-go/output.go +58 -4
- package/src/cli-go/root.go +1 -1
- package/src/cli-go/version.go +2 -2
- package/src/cli.ts +154 -148
- package/src/client/error.ts +56 -1
- package/src/client/http.ts +479 -256
- package/src/client/index.ts +55 -17
- package/src/client/path.ts +169 -0
- package/src/client/redirect-policy.ts +19 -0
- package/src/client/sdk.ts +180 -101
- package/src/client/sse.ts +92 -66
- package/src/client/ws.ts +67 -31
- package/src/client-go/errors.go +41 -12
- package/src/client-go/export.go +44 -0
- package/src/client-go/invalidation.go +52 -54
- package/src/client-go/nullable.go +62 -0
- package/src/client-go/realtime.go +147 -110
- package/src/client-go/runtime.go +646 -88
- package/src/client-go/sse.go +104 -94
- package/src/client-go/transport.go +67 -27
- package/src/client-go/ws.go +49 -25
- package/src/client-info.ts +79 -0
- package/src/client-mcp/server.ts +133 -3
- package/src/client-python/_invalidation.py +207 -291
- package/src/client-python/_realtime.py +63 -40
- package/src/client-python/_runtime.py +501 -119
- package/src/client-python/_sse.py +130 -74
- package/src/client-python/_transport.py +37 -9
- package/src/client-python/_ws.py +50 -22
- package/src/client-rust/errors.rs +21 -1
- package/src/client-rust/invalidation.rs +202 -231
- package/src/client-rust/invalidation_sync.rs +16 -229
- package/src/client-rust/nullable.rs +17 -0
- package/src/client-rust/realtime.rs +217 -217
- package/src/client-rust/runtime.rs +602 -177
- package/src/client-rust/runtime_sync.rs +169 -145
- package/src/client-rust/sse.rs +142 -121
- package/src/client-rust/ws.rs +61 -37
- package/src/codegen-go-cli.ts +607 -728
- package/src/codegen-go.ts +710 -770
- package/src/codegen-ir.ts +381 -75
- package/src/codegen-lang.ts +517 -0
- package/src/codegen-load.ts +150 -0
- package/src/codegen-mcp.ts +207 -108
- package/src/codegen-python.ts +684 -816
- package/src/codegen-route-tree-intern.ts +51 -25
- package/src/codegen-rust.ts +946 -1240
- package/src/codegen-sanitize.ts +0 -4
- package/src/codegen-schema-naming.ts +19 -6
- package/src/codegen-sdk-model.ts +228 -0
- package/src/codegen.ts +1488 -1140
- package/src/context.ts +130 -11
- package/src/cookie-sign.ts +85 -16
- package/src/cookie.ts +66 -11
- package/src/cors.ts +55 -54
- package/src/crypto.ts +27 -5
- package/src/csrf.ts +57 -25
- package/src/curl-logger.ts +155 -114
- package/src/dict.ts +12 -0
- package/src/error.ts +4 -2
- package/src/etag.ts +58 -34
- package/src/feature-detect.ts +147 -0
- package/src/feature-slots.ts +70 -0
- package/src/fetch-request.ts +13 -0
- package/src/gen-process.ts +42 -0
- package/src/gen-write.ts +127 -0
- package/src/glob.ts +109 -0
- package/src/go-type-emitter.ts +406 -155
- package/src/honey-response.ts +36 -9
- package/src/i18n-slot.ts +10 -0
- package/src/i18n.ts +98 -23
- package/src/index.ts +1887 -1756
- package/src/init.ts +40 -28
- package/src/invalidate-check.ts +42 -4
- package/src/invoke-user.ts +95 -0
- package/src/ip-restrict.ts +47 -161
- package/src/ip.ts +194 -0
- package/src/logger.ts +65 -36
- package/src/media-type.ts +104 -0
- package/src/meta-spec-merge.ts +80 -0
- package/src/meta-spec.ts +0 -0
- package/src/middleware.ts +112 -12
- package/src/node-request.ts +242 -52
- package/src/node.ts +318 -75
- package/src/openapi/collect.ts +19 -44
- package/src/openapi/docs-page.ts +89 -0
- package/src/openapi/document.ts +503 -224
- package/src/openapi/json-schema-slot.ts +40 -0
- package/src/openapi/register.ts +7 -2
- package/src/openapi/scalar.ts +11 -10
- package/src/openapi/serve.ts +39 -0
- package/src/openapi/spec-factory.ts +50 -1
- package/src/openapi/spec.ts +53 -30
- package/src/openapi/swagger.ts +12 -11
- package/src/pattern.ts +245 -0
- package/src/peer.ts +53 -0
- package/src/plugin.ts +270 -185
- package/src/powered-by.ts +2 -2
- package/src/pretty-json.ts +25 -9
- package/src/producer-stream.ts +296 -0
- package/src/proxy.ts +253 -55
- package/src/python-type-emitter.ts +68 -56
- package/src/realtime/bus.ts +56 -9
- package/src/realtime/register.ts +13 -0
- package/src/realtime/route.ts +129 -80
- package/src/realtime/server.ts +298 -0
- package/src/request-id.ts +13 -3
- package/src/request-limits.ts +2 -0
- package/src/request-path.ts +165 -0
- package/src/request-to-curl.ts +28 -7
- package/src/response.ts +143 -103
- package/src/rust-type-emitter.ts +265 -170
- package/src/secure-headers.ts +8 -4
- package/src/serve-register.ts +4 -0
- package/src/serve-slot.ts +38 -0
- package/src/serve.ts +113 -25
- package/src/server-timing.ts +11 -7
- package/src/static.ts +85 -32
- package/src/telemetry/otel.ts +29 -7
- package/src/testing.ts +76 -14
- package/src/timeout.ts +15 -7
- package/src/tree.ts +405 -333
- package/src/trust.ts +117 -0
- package/src/ts-type-emitter.ts +18 -12
- package/src/type-emitter.ts +227 -89
- package/src/type-extractor.ts +603 -423
- package/src/types.ts +8 -0
- package/src/validation.ts +221 -96
- package/src/with-headers.ts +45 -0
- package/src/ws/bun.ts +53 -15
- package/src/ws/cloudflare.ts +81 -19
- package/src/ws/deno.ts +124 -36
- package/src/ws/node.ts +173 -79
- package/src/ws/session.ts +73 -0
- package/src/ws/shared.ts +140 -0
- package/src/ws-origin.ts +53 -0
- package/src/yaml.ts +45 -22
- package/dist/client-mcp/server.d.ts +0 -10
- package/dist/client-mcp/server.d.ts.map +0 -1
- package/dist/client-mcp/types.d.ts +0 -16
- package/dist/client-mcp/types.d.ts.map +0 -1
- package/dist/cloudflare/realtime.d.ts +0 -30
- package/dist/cloudflare/realtime.d.ts.map +0 -1
- package/dist/realtime/buffer.d.ts +0 -18
- package/dist/realtime/buffer.d.ts.map +0 -1
- package/dist/realtime/longpoll.d.ts +0 -23
- package/dist/realtime/longpoll.d.ts.map +0 -1
- package/dist/realtime/protocol.d.ts +0 -50
- package/dist/realtime/protocol.d.ts.map +0 -1
- package/dist/realtime/runtime.d.ts +0 -74
- package/dist/realtime/runtime.d.ts.map +0 -1
- package/src/cloudflare/realtime.ts +0 -234
- package/src/realtime/buffer.ts +0 -101
- package/src/realtime/longpoll.ts +0 -148
- package/src/realtime/protocol.ts +0 -128
- package/src/realtime/runtime.ts +0 -427
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,4610 @@
|
|
|
1
|
+
import { InternPool } from "./codegen-route-tree-intern.js";
|
|
2
|
+
import { deriveErrorEnvelopeName, deriveSchemaName, isErrorEnvelope, sanitizeComponentName, shortHash, } from "./codegen-schema-naming.js";
|
|
3
|
+
import { effectJsonSchemaFn, loadEffectJsonSchema, loadToJSONSchema, toJSONSchemaFn } from "./codegen-loaders.js";
|
|
4
|
+
import { sanitizeZodJsonSchema } from "./codegen-sanitize.js";
|
|
5
|
+
import { bodiesOf, irErrorEnvelope, irResolver, methodsOf, namespacesOf, openApiPathParams, schemaToIR, toIR, } from "./codegen-ir.js";
|
|
6
|
+
import { isJsonMedia, isTextMedia, mediaEssence } from "./codegen-sdk-model.js";
|
|
7
|
+
import { ERROR_META } from "./errors.js";
|
|
8
|
+
import { publishableMetaKeys } from "./meta-spec.js";
|
|
9
|
+
import { metaSpecOf } from "./meta-spec-merge.js";
|
|
10
|
+
import { generateOpenApiFromTree, } from "./openapi/document.js";
|
|
11
|
+
import { getJsonSchemaConverter, setJsonSchemaConverter } from "./openapi/json-schema-slot.js";
|
|
12
|
+
import { collectRoutes, extractParams } from "./openapi/collect.js";
|
|
13
|
+
import { parsePattern, patternParams } from "./pattern.js";
|
|
14
|
+
import { forEachLeaf, ROUTE_TREE_VERSION } from "./tree.js";
|
|
15
|
+
import { irToTs } from "./ts-type-emitter.js";
|
|
16
|
+
import { createTypeEmitState, emitSchemaType, quoteKey } from "./type-emitter.js";
|
|
17
|
+
import { EMPTY_OBJ, statusKeyToCode } from "./types.js";
|
|
18
|
+
import { CROSS_ORIGIN_SAFE_HEADERS } from "./client/redirect-policy.js";
|
|
19
|
+
export { prepareCodegen } from "./codegen-loaders.js";
|
|
20
|
+
export { toYaml, yamlSiblingPath } from "./yaml.js";
|
|
21
|
+
export { DEFAULT_ERROR_JSON_SCHEMA } from "./openapi/document.js";
|
|
22
|
+
/* Status → typed error subclass. Single source of truth shared between the
|
|
23
|
+
* emitted client (class declarations + status map + footer re-exports) and
|
|
24
|
+
* the index.gen.ts import/export. Order is deterministic for stable output. */
|
|
25
|
+
const STATUS_ERROR_CLASSES = [
|
|
26
|
+
{ name: "BadRequestError", status: 400 },
|
|
27
|
+
{ name: "UnauthorizedError", status: 401 },
|
|
28
|
+
{ name: "ForbiddenError", status: 403 },
|
|
29
|
+
{ name: "NotFoundError", status: 404 },
|
|
30
|
+
{ name: "ConflictError", status: 409 },
|
|
31
|
+
{ name: "UnprocessableEntityError", status: 422 },
|
|
32
|
+
{ name: "RateLimitError", status: 429 },
|
|
33
|
+
{ name: "InternalServerError", status: 500 },
|
|
34
|
+
{ name: "BadGatewayError", status: 502 },
|
|
35
|
+
{ name: "ServiceUnavailableError", status: 503 },
|
|
36
|
+
{ name: "GatewayTimeoutError", status: 504 },
|
|
37
|
+
];
|
|
38
|
+
const ERROR_EXPORT_NAMES = [
|
|
39
|
+
"ClientError",
|
|
40
|
+
...STATUS_ERROR_CLASSES.map((c) => c.name),
|
|
41
|
+
"isClientError",
|
|
42
|
+
];
|
|
43
|
+
/** Wrappers that make an object entry optional. */
|
|
44
|
+
const VALIBOT_OPTIONAL = new Set(["exact_optional", "nullish", "optional", "undefinedable"]);
|
|
45
|
+
/** Pipe actions with a JSON Schema equivalent. Anything else constrains nothing in the document. */
|
|
46
|
+
function applyValibotPipe(schema, json) {
|
|
47
|
+
const pipe = schema.pipe;
|
|
48
|
+
if (!Array.isArray(pipe))
|
|
49
|
+
return json;
|
|
50
|
+
const formats = {
|
|
51
|
+
email: "email",
|
|
52
|
+
ipv4: "ipv4",
|
|
53
|
+
ipv6: "ipv6",
|
|
54
|
+
iso_date: "date",
|
|
55
|
+
iso_date_time: "date-time",
|
|
56
|
+
iso_timestamp: "date-time",
|
|
57
|
+
url: "uri",
|
|
58
|
+
uuid: "uuid",
|
|
59
|
+
};
|
|
60
|
+
for (const action of pipe.slice(1)) {
|
|
61
|
+
const type = action.type;
|
|
62
|
+
const req = action.requirement;
|
|
63
|
+
if (type in formats)
|
|
64
|
+
json.format = formats[type];
|
|
65
|
+
else if (type === "integer")
|
|
66
|
+
json.type = "integer";
|
|
67
|
+
else if (type === "regex" && req instanceof RegExp)
|
|
68
|
+
json.pattern = req.source;
|
|
69
|
+
else if (type === "min_length" && typeof req === "number")
|
|
70
|
+
json[json.type === "array" ? "minItems" : "minLength"] = req;
|
|
71
|
+
else if (type === "max_length" && typeof req === "number")
|
|
72
|
+
json[json.type === "array" ? "maxItems" : "maxLength"] = req;
|
|
73
|
+
else if (type === "length" && typeof req === "number") {
|
|
74
|
+
json[json.type === "array" ? "minItems" : "minLength"] = req;
|
|
75
|
+
json[json.type === "array" ? "maxItems" : "maxLength"] = req;
|
|
76
|
+
}
|
|
77
|
+
else if (type === "min_value" && typeof req === "number")
|
|
78
|
+
json.minimum = req;
|
|
79
|
+
else if (type === "max_value" && typeof req === "number")
|
|
80
|
+
json.maximum = req;
|
|
81
|
+
else if (type === "description" && typeof action.description === "string")
|
|
82
|
+
json.description = action.description;
|
|
83
|
+
}
|
|
84
|
+
return json;
|
|
85
|
+
}
|
|
86
|
+
function valibotObject(s, depth) {
|
|
87
|
+
const entries = s.entries;
|
|
88
|
+
if (Object.keys(entries).length === 0 && s.type === "object")
|
|
89
|
+
return { type: "object" };
|
|
90
|
+
const properties = {};
|
|
91
|
+
const required = [];
|
|
92
|
+
for (const k of Object.keys(entries)) {
|
|
93
|
+
const entry = entries[k];
|
|
94
|
+
properties[k] = valibotToJsonSchema(entry, depth + 1);
|
|
95
|
+
if (!VALIBOT_OPTIONAL.has(entry.type))
|
|
96
|
+
required.push(k);
|
|
97
|
+
}
|
|
98
|
+
const result = { properties, type: "object" };
|
|
99
|
+
if (required.length > 0)
|
|
100
|
+
result.required = required;
|
|
101
|
+
if (s.type === "strict_object")
|
|
102
|
+
result.additionalProperties = false;
|
|
103
|
+
else if (s.type === "object_with_rest")
|
|
104
|
+
result.additionalProperties = valibotToJsonSchema(s.rest, depth + 1);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function valibotToJsonSchema(schema, depth = 0) {
|
|
108
|
+
/* a lazy schema can recurse without end; past this depth the node is unconstrained */
|
|
109
|
+
if (depth > 32)
|
|
110
|
+
return {};
|
|
111
|
+
const s = schema;
|
|
112
|
+
const walk = (child) => valibotToJsonSchema(child, depth + 1);
|
|
113
|
+
switch (s.type) {
|
|
114
|
+
case "string":
|
|
115
|
+
return applyValibotPipe(s, { type: "string" });
|
|
116
|
+
case "number":
|
|
117
|
+
return applyValibotPipe(s, { type: "number" });
|
|
118
|
+
case "boolean":
|
|
119
|
+
return { type: "boolean" };
|
|
120
|
+
case "bigint":
|
|
121
|
+
return { format: "int64", type: "integer" };
|
|
122
|
+
case "date":
|
|
123
|
+
return { format: "date-time", type: "string" };
|
|
124
|
+
case "file":
|
|
125
|
+
case "blob":
|
|
126
|
+
return { contentEncoding: "binary", format: "binary", type: "string" };
|
|
127
|
+
case "undefined":
|
|
128
|
+
case "void":
|
|
129
|
+
return {};
|
|
130
|
+
case "null":
|
|
131
|
+
return { type: "null" };
|
|
132
|
+
case "any":
|
|
133
|
+
case "unknown":
|
|
134
|
+
return {};
|
|
135
|
+
case "literal":
|
|
136
|
+
return { const: s.literal };
|
|
137
|
+
case "object":
|
|
138
|
+
case "strict_object":
|
|
139
|
+
case "loose_object":
|
|
140
|
+
case "object_with_rest":
|
|
141
|
+
return valibotObject(s, depth);
|
|
142
|
+
case "array":
|
|
143
|
+
return applyValibotPipe(s, { items: walk(s.item), type: "array" });
|
|
144
|
+
case "optional":
|
|
145
|
+
case "exact_optional":
|
|
146
|
+
case "undefinedable":
|
|
147
|
+
case "non_nullable":
|
|
148
|
+
case "non_nullish":
|
|
149
|
+
case "non_optional": {
|
|
150
|
+
const inner = walk(s.wrapped);
|
|
151
|
+
const fallback = s.default;
|
|
152
|
+
return fallback === undefined || typeof fallback === "function" ? inner : { ...inner, default: fallback };
|
|
153
|
+
}
|
|
154
|
+
case "nullable":
|
|
155
|
+
case "nullish":
|
|
156
|
+
return { anyOf: [walk(s.wrapped), { type: "null" }] };
|
|
157
|
+
case "union":
|
|
158
|
+
case "variant":
|
|
159
|
+
return { anyOf: s.options.map(walk) };
|
|
160
|
+
case "intersect":
|
|
161
|
+
return { allOf: s.options.map(walk) };
|
|
162
|
+
case "picklist":
|
|
163
|
+
return { enum: s.options };
|
|
164
|
+
case "enum": {
|
|
165
|
+
const enumObj = s.enum;
|
|
166
|
+
/* a TS numeric enum carries reverse mappings — keep the values */
|
|
167
|
+
return {
|
|
168
|
+
enum: Object.keys(enumObj)
|
|
169
|
+
.filter((k) => typeof enumObj[enumObj[k]] !== "number")
|
|
170
|
+
.map((k) => enumObj[k]),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
case "record":
|
|
174
|
+
return { additionalProperties: walk(s.value), type: "object" };
|
|
175
|
+
case "tuple":
|
|
176
|
+
case "strict_tuple":
|
|
177
|
+
case "loose_tuple":
|
|
178
|
+
case "tuple_with_rest": {
|
|
179
|
+
const items = s.items.map(walk);
|
|
180
|
+
const result = { minItems: items.length, prefixItems: items, type: "array" };
|
|
181
|
+
if (s.type === "tuple_with_rest")
|
|
182
|
+
result.items = walk(s.rest);
|
|
183
|
+
else if (s.type !== "loose_tuple") {
|
|
184
|
+
result.items = false;
|
|
185
|
+
result.maxItems = items.length;
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
case "lazy": {
|
|
190
|
+
const getter = s.getter;
|
|
191
|
+
return typeof getter === "function" ? walk(getter(undefined)) : {};
|
|
192
|
+
}
|
|
193
|
+
default:
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/* ---- ArkType → JSON Schema converter ---- */
|
|
198
|
+
function arkTypeToJsonSchema(schema) {
|
|
199
|
+
const s = schema;
|
|
200
|
+
if (typeof s.toJsonSchema === "function") {
|
|
201
|
+
try {
|
|
202
|
+
return s.toJsonSchema();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* fall through */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {};
|
|
209
|
+
}
|
|
210
|
+
function yupToJsonSchema(schema) {
|
|
211
|
+
const s = schema;
|
|
212
|
+
if (typeof s.describe !== "function")
|
|
213
|
+
return {};
|
|
214
|
+
const desc = s.describe();
|
|
215
|
+
return yupDescToJsonSchema(desc);
|
|
216
|
+
}
|
|
217
|
+
function yupDescToJsonSchema(desc) {
|
|
218
|
+
const base = yupDescBase(desc);
|
|
219
|
+
if (desc.nullable) {
|
|
220
|
+
return { anyOf: [base, { type: "null" }] };
|
|
221
|
+
}
|
|
222
|
+
return base;
|
|
223
|
+
}
|
|
224
|
+
function yupDescBase(desc) {
|
|
225
|
+
switch (desc.type) {
|
|
226
|
+
case "string":
|
|
227
|
+
return { type: "string" };
|
|
228
|
+
case "number":
|
|
229
|
+
return { type: "number" };
|
|
230
|
+
case "boolean":
|
|
231
|
+
return { type: "boolean" };
|
|
232
|
+
case "date":
|
|
233
|
+
return { format: "date-time", type: "string" };
|
|
234
|
+
case "object": {
|
|
235
|
+
if (!desc.fields)
|
|
236
|
+
return { type: "object" };
|
|
237
|
+
const keys = Object.keys(desc.fields);
|
|
238
|
+
if (keys.length === 0)
|
|
239
|
+
return { type: "object" };
|
|
240
|
+
const properties = {};
|
|
241
|
+
const required = [];
|
|
242
|
+
for (const k of keys) {
|
|
243
|
+
const field = desc.fields[k];
|
|
244
|
+
properties[k] = yupDescToJsonSchema(field);
|
|
245
|
+
if (!field.optional)
|
|
246
|
+
required.push(k);
|
|
247
|
+
}
|
|
248
|
+
const result = { properties, type: "object" };
|
|
249
|
+
if (required.length > 0)
|
|
250
|
+
result.required = required;
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
case "array": {
|
|
254
|
+
if (!desc.innerType || Array.isArray(desc.innerType))
|
|
255
|
+
return { type: "array" };
|
|
256
|
+
return { items: yupDescToJsonSchema(desc.innerType), type: "array" };
|
|
257
|
+
}
|
|
258
|
+
case "tuple": {
|
|
259
|
+
if (!Array.isArray(desc.innerType))
|
|
260
|
+
return { type: "array" };
|
|
261
|
+
const items = desc.innerType.map((i) => yupDescToJsonSchema(i));
|
|
262
|
+
return {
|
|
263
|
+
items: false,
|
|
264
|
+
maxItems: items.length,
|
|
265
|
+
minItems: items.length,
|
|
266
|
+
prefixItems: items,
|
|
267
|
+
type: "array",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
case "mixed": {
|
|
271
|
+
if (desc.oneOf && desc.oneOf.length > 0)
|
|
272
|
+
return { enum: desc.oneOf };
|
|
273
|
+
return {};
|
|
274
|
+
}
|
|
275
|
+
default:
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/* ---- Effect → JSON Schema converter ---- */
|
|
280
|
+
function effectToJsonSchema(schema) {
|
|
281
|
+
if (effectJsonSchemaFn) {
|
|
282
|
+
try {
|
|
283
|
+
return effectJsonSchemaFn(schema);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
/* fall through */
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
function isMetaInternal(handler) {
|
|
292
|
+
return handler.mt?.internal === true;
|
|
293
|
+
}
|
|
294
|
+
/* Longest suffix first so `*_not_found` wins over `*_found` (302). */
|
|
295
|
+
const STATUS_KEYS_BY_LENGTH = Object.keys(statusKeyToCode).sort((a, b) => b.length - a.length);
|
|
296
|
+
/*
|
|
297
|
+
* Composed apps keep declared keys on the handler but may not carry the worker
|
|
298
|
+
* factory. Infer HTTP status from a trailing status-key suffix so the document
|
|
299
|
+
* still lists what `c.errors.<key>` actually writes.
|
|
300
|
+
*/
|
|
301
|
+
function inferErrorInfo(errorKey) {
|
|
302
|
+
const exact = statusKeyToCode[errorKey];
|
|
303
|
+
if (exact !== undefined) {
|
|
304
|
+
if (exact < 400)
|
|
305
|
+
return null;
|
|
306
|
+
return { errorKey, status: exact, statusKey: errorKey };
|
|
307
|
+
}
|
|
308
|
+
for (const statusKey of STATUS_KEYS_BY_LENGTH) {
|
|
309
|
+
if (!errorKey.endsWith(`_${statusKey}`))
|
|
310
|
+
continue;
|
|
311
|
+
const status = statusKeyToCode[statusKey];
|
|
312
|
+
if (status < 400)
|
|
313
|
+
continue;
|
|
314
|
+
return { errorKey, status, statusKey };
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
function resolveErrorInfo(errorKey, factory) {
|
|
319
|
+
if (factory?.[errorKey]) {
|
|
320
|
+
try {
|
|
321
|
+
const err = factory[errorKey]();
|
|
322
|
+
return { errorKey, status: err.status, statusKey: err.statusKey };
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
/* factory call failed — fall through to suffix inference */
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return inferErrorInfo(errorKey) ?? { errorKey, status: 0, statusKey: "unknown" };
|
|
329
|
+
}
|
|
330
|
+
function getErrorFactory(app) {
|
|
331
|
+
return app._errorFactory;
|
|
332
|
+
}
|
|
333
|
+
function getErrorMeta(factory) {
|
|
334
|
+
if (!factory)
|
|
335
|
+
return null;
|
|
336
|
+
return factory[ERROR_META] ?? null;
|
|
337
|
+
}
|
|
338
|
+
function unwrapEntry(entry) {
|
|
339
|
+
if ("_tag" in entry) {
|
|
340
|
+
return entry.schema;
|
|
341
|
+
}
|
|
342
|
+
return entry;
|
|
343
|
+
}
|
|
344
|
+
function introspectSchema(schema) {
|
|
345
|
+
const std = schema["~standard"];
|
|
346
|
+
return {
|
|
347
|
+
types: std.types,
|
|
348
|
+
vendor: std.vendor,
|
|
349
|
+
version: std.version,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
export { normalizeSecurity } from "./meta-spec.js";
|
|
353
|
+
/** Locale-independent string order (UTF-16 code units), so output is the same on every machine. */
|
|
354
|
+
export function compareCodeUnits(a, b) {
|
|
355
|
+
if (a === b)
|
|
356
|
+
return 0;
|
|
357
|
+
return a < b ? -1 : 1;
|
|
358
|
+
}
|
|
359
|
+
export function canonicalizeSchema(schema) {
|
|
360
|
+
return JSON.stringify(schema, (_, value) => {
|
|
361
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
362
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => compareCodeUnits(a, b)));
|
|
363
|
+
}
|
|
364
|
+
return value;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function isObjectSchema(schema) {
|
|
368
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
369
|
+
return false;
|
|
370
|
+
const s = schema;
|
|
371
|
+
if (typeof s.$ref === "string")
|
|
372
|
+
return false;
|
|
373
|
+
return s.type === "object" || (typeof s.properties === "object" && s.properties !== null);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* A response key as a number: `"200"` → 200, `"default"` → 0, `"4XX"` → 400. Never `NaN`, which
|
|
377
|
+
* would name a schema `…ResponseNaN` and make every `default` response collide.
|
|
378
|
+
*/
|
|
379
|
+
function statusNumber(key) {
|
|
380
|
+
if (/^[1-5][0-9][0-9]$/.test(key))
|
|
381
|
+
return Number(key);
|
|
382
|
+
if (/^[1-5]XX$/i.test(key))
|
|
383
|
+
return Number(key[0]) * 100;
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
function collectSchemaSlots(paths) {
|
|
387
|
+
const slots = [];
|
|
388
|
+
for (const [pathKey, methods] of Object.entries(paths)) {
|
|
389
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
390
|
+
const op = operation;
|
|
391
|
+
const m = method.toLowerCase();
|
|
392
|
+
const requestBody = op.requestBody;
|
|
393
|
+
if (requestBody) {
|
|
394
|
+
const content = requestBody.content;
|
|
395
|
+
if (content) {
|
|
396
|
+
for (const [contentType, mediaType] of Object.entries(content)) {
|
|
397
|
+
const schema = mediaType.schema;
|
|
398
|
+
if (schema && !schema.$ref) {
|
|
399
|
+
const context = { method: m, path: pathKey, role: "request" };
|
|
400
|
+
slots.push({ canonical: canonicalizeSchema(schema), contentType, context, pathKey, schema });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const responses = op.responses;
|
|
406
|
+
if (responses) {
|
|
407
|
+
for (const [status, response] of Object.entries(responses)) {
|
|
408
|
+
const content = response.content;
|
|
409
|
+
if (content) {
|
|
410
|
+
for (const [contentType, mediaType] of Object.entries(content)) {
|
|
411
|
+
const schema = mediaType.schema;
|
|
412
|
+
if (schema && !schema.$ref) {
|
|
413
|
+
const statusNum = statusNumber(status);
|
|
414
|
+
const context = {
|
|
415
|
+
method: m,
|
|
416
|
+
path: pathKey,
|
|
417
|
+
role: "response",
|
|
418
|
+
status: statusNum,
|
|
419
|
+
};
|
|
420
|
+
slots.push({ canonical: canonicalizeSchema(schema), contentType, context, pathKey, schema });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return slots;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* A component name nobody holds yet. A clash gets the content hash, and a clash on the
|
|
432
|
+
* hashed name (24 bits collide) a counter — a name is never handed out twice.
|
|
433
|
+
*/
|
|
434
|
+
function uniqueName(base, canonical, taken) {
|
|
435
|
+
if (!taken(base))
|
|
436
|
+
return base;
|
|
437
|
+
const hashed = `${base}_${shortHash(canonical)}`;
|
|
438
|
+
if (!taken(hashed))
|
|
439
|
+
return hashed;
|
|
440
|
+
for (let n = 2;; n++) {
|
|
441
|
+
const candidate = `${hashed}_${n}`;
|
|
442
|
+
if (!taken(candidate))
|
|
443
|
+
return candidate;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function walkNestedObjects(schema, slotName, fieldPath, nestedMap, isEnvelopeOwner) {
|
|
447
|
+
const props = schema.properties;
|
|
448
|
+
if (!props)
|
|
449
|
+
return;
|
|
450
|
+
for (const key of Object.keys(props)) {
|
|
451
|
+
const val = props[key];
|
|
452
|
+
if (!isObjectSchema(val))
|
|
453
|
+
continue;
|
|
454
|
+
const child = val;
|
|
455
|
+
const childPath = [...fieldPath, key];
|
|
456
|
+
const canonical = canonicalizeSchema(child);
|
|
457
|
+
const existing = nestedMap.get(canonical);
|
|
458
|
+
if (existing) {
|
|
459
|
+
existing.count++;
|
|
460
|
+
existing.samples.push({ fieldPath: childPath, isEnvelopeOwner, slotName });
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
nestedMap.set(canonical, {
|
|
464
|
+
count: 1,
|
|
465
|
+
samples: [{ fieldPath: childPath, isEnvelopeOwner, slotName }],
|
|
466
|
+
schema: child,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
walkNestedObjects(child, slotName, childPath, nestedMap, isEnvelopeOwner);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function collectHoistedSchemas(slots, slotNames, taken) {
|
|
473
|
+
const nestedMap = new Map();
|
|
474
|
+
for (const slot of slots) {
|
|
475
|
+
const slotName = slotNames.get(slot);
|
|
476
|
+
/* Mark slots whose schema is an error envelope — their nested objects (e.g. `fields`)
|
|
477
|
+
* are env-specific and must not be named after the parent envelope's owner slot. */
|
|
478
|
+
if (slotName)
|
|
479
|
+
walkNestedObjects(slot.schema, slotName, [], nestedMap, isErrorEnvelope(slot.schema));
|
|
480
|
+
}
|
|
481
|
+
const schemas = {};
|
|
482
|
+
const canonicalToName = new Map();
|
|
483
|
+
for (const [canonical, entry] of nestedMap) {
|
|
484
|
+
if (entry.count < 2)
|
|
485
|
+
continue;
|
|
486
|
+
/* Skip hoisting nested objects that live exclusively inside Tier 2 error envelopes.
|
|
487
|
+
* Their names would be derived from whichever envelope "wins" the lex sort, and that
|
|
488
|
+
* winner differs per-spec — causing merge conflicts when multiple specs are combined. */
|
|
489
|
+
if (entry.samples.every((s) => s.isEnvelopeOwner))
|
|
490
|
+
continue;
|
|
491
|
+
const owner = entry.samples.slice().sort((a, b) => {
|
|
492
|
+
const na = a.slotName + a.fieldPath.join("");
|
|
493
|
+
const nb = b.slotName + b.fieldPath.join("");
|
|
494
|
+
return compareCodeUnits(na, nb);
|
|
495
|
+
})[0];
|
|
496
|
+
const fieldPart = owner.fieldPath
|
|
497
|
+
.map((f) => sanitizeComponentName(f))
|
|
498
|
+
.map((f) => f.charAt(0).toUpperCase() + f.slice(1))
|
|
499
|
+
.join("");
|
|
500
|
+
const name = uniqueName(sanitizeComponentName(`${owner.slotName}${fieldPart}`), canonical, (n) => taken(n) || Object.hasOwn(schemas, n));
|
|
501
|
+
schemas[name] = entry.schema;
|
|
502
|
+
canonicalToName.set(canonical, name);
|
|
503
|
+
}
|
|
504
|
+
/* nested hoists inside a hoisted schema point at their component too */
|
|
505
|
+
for (const name of Object.keys(schemas))
|
|
506
|
+
schemas[name] = rewriteNestedRefs(schemas[name], canonicalToName);
|
|
507
|
+
return { canonicalToName, schemas };
|
|
508
|
+
}
|
|
509
|
+
/*
|
|
510
|
+
* Builds a new paths object by shallow-copying only the containers that need schema
|
|
511
|
+
* replacement. Avoids structuredClone (which fails on arktype schemas containing
|
|
512
|
+
* native functions). Original schema objects are dropped; only $ref objects are new.
|
|
513
|
+
* Also rewrites nested inline object properties that were hoisted (canonicalToName),
|
|
514
|
+
* at any depth — a hoisted name nothing points at would be an orphan component.
|
|
515
|
+
*/
|
|
516
|
+
function rewriteNestedRefs(schema, canonicalToName) {
|
|
517
|
+
const props = schema.properties;
|
|
518
|
+
if (!props)
|
|
519
|
+
return schema;
|
|
520
|
+
let newProps;
|
|
521
|
+
for (const key of Object.keys(props)) {
|
|
522
|
+
const val = props[key];
|
|
523
|
+
if (!isObjectSchema(val))
|
|
524
|
+
continue;
|
|
525
|
+
const child = val;
|
|
526
|
+
const name = canonicalToName.get(canonicalizeSchema(child));
|
|
527
|
+
const next = name ? { $ref: `#/components/schemas/${name}` } : rewriteNestedRefs(child, canonicalToName);
|
|
528
|
+
if (next === child)
|
|
529
|
+
continue;
|
|
530
|
+
if (!newProps)
|
|
531
|
+
newProps = { ...props };
|
|
532
|
+
newProps[key] = next;
|
|
533
|
+
}
|
|
534
|
+
if (!newProps)
|
|
535
|
+
return schema;
|
|
536
|
+
return { ...schema, properties: newProps };
|
|
537
|
+
}
|
|
538
|
+
function slotKey(pathKey, method, role, status, contentType) {
|
|
539
|
+
return `${pathKey}::${method}::${role}::${status ?? ""}::${contentType}`;
|
|
540
|
+
}
|
|
541
|
+
function rewritePathsToRefs(paths, slots, slotNames, hoistedCanonicalToName) {
|
|
542
|
+
/* keyed by operation + status + content type: JSON and XML bodies of one status are two slots */
|
|
543
|
+
const lookup = new Map();
|
|
544
|
+
for (const slot of slots) {
|
|
545
|
+
const name = slotNames.get(slot);
|
|
546
|
+
if (!name)
|
|
547
|
+
continue;
|
|
548
|
+
const status = slot.context.role === "response" ? slot.context.status : undefined;
|
|
549
|
+
lookup.set(slotKey(slot.pathKey, slot.context.method, slot.context.role, status, slot.contentType), name);
|
|
550
|
+
}
|
|
551
|
+
const rewriteContent = (content, keyFor) => {
|
|
552
|
+
const newContent = {};
|
|
553
|
+
let changed = false;
|
|
554
|
+
for (const [ct, mediaType] of Object.entries(content)) {
|
|
555
|
+
const schema = mediaType.schema;
|
|
556
|
+
if (!schema || schema.$ref) {
|
|
557
|
+
newContent[ct] = mediaType;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const name = lookup.get(keyFor(ct));
|
|
561
|
+
if (name) {
|
|
562
|
+
newContent[ct] = { ...mediaType, schema: { $ref: `#/components/schemas/${name}` } };
|
|
563
|
+
changed = true;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const rewritten = hoistedCanonicalToName.size > 0 ? rewriteNestedRefs(schema, hoistedCanonicalToName) : schema;
|
|
567
|
+
if (rewritten !== schema) {
|
|
568
|
+
newContent[ct] = { ...mediaType, schema: rewritten };
|
|
569
|
+
changed = true;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
newContent[ct] = mediaType;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return changed ? newContent : null;
|
|
576
|
+
};
|
|
577
|
+
const newPaths = {};
|
|
578
|
+
for (const [pathKey, methods] of Object.entries(paths)) {
|
|
579
|
+
const newMethods = {};
|
|
580
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
581
|
+
const op = operation;
|
|
582
|
+
const m = method.toLowerCase();
|
|
583
|
+
let newOp = op;
|
|
584
|
+
const requestBody = op.requestBody;
|
|
585
|
+
const reqContent = requestBody?.content;
|
|
586
|
+
if (requestBody && reqContent) {
|
|
587
|
+
const next = rewriteContent(reqContent, (ct) => slotKey(pathKey, m, "request", undefined, ct));
|
|
588
|
+
if (next)
|
|
589
|
+
newOp = { ...op, requestBody: { ...requestBody, content: next } };
|
|
590
|
+
}
|
|
591
|
+
const responses = newOp.responses;
|
|
592
|
+
if (responses) {
|
|
593
|
+
let newResponses;
|
|
594
|
+
for (const [status, response] of Object.entries(responses)) {
|
|
595
|
+
const content = response.content;
|
|
596
|
+
if (!content)
|
|
597
|
+
continue;
|
|
598
|
+
const statusNum = statusNumber(status);
|
|
599
|
+
const next = rewriteContent(content, (ct) => slotKey(pathKey, m, "response", statusNum, ct));
|
|
600
|
+
if (!next)
|
|
601
|
+
continue;
|
|
602
|
+
if (!newResponses)
|
|
603
|
+
newResponses = { ...responses };
|
|
604
|
+
newResponses[status] = { ...response, content: next };
|
|
605
|
+
}
|
|
606
|
+
if (newResponses)
|
|
607
|
+
newOp = { ...newOp, responses: newResponses };
|
|
608
|
+
}
|
|
609
|
+
newMethods[method] = newOp;
|
|
610
|
+
}
|
|
611
|
+
newPaths[pathKey] = newMethods;
|
|
612
|
+
}
|
|
613
|
+
return newPaths;
|
|
614
|
+
}
|
|
615
|
+
/** Every `#/components/schemas/<name>` a value refers to. */
|
|
616
|
+
function collectComponentRefs(value, out) {
|
|
617
|
+
if (Array.isArray(value)) {
|
|
618
|
+
for (const item of value)
|
|
619
|
+
collectComponentRefs(item, out);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (value === null || typeof value !== "object")
|
|
623
|
+
return;
|
|
624
|
+
for (const [key, child] of Object.entries(value)) {
|
|
625
|
+
if (key === "$ref" && typeof child === "string" && child.startsWith("#/components/schemas/")) {
|
|
626
|
+
out.add(child.slice("#/components/schemas/".length));
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
collectComponentRefs(child, out);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
export function deduplicateSchemas(spec) {
|
|
634
|
+
/* Sort paths lexicographically for determinism — input order must not affect output. */
|
|
635
|
+
const sortedPaths = {};
|
|
636
|
+
for (const key of Object.keys(spec.paths).sort())
|
|
637
|
+
sortedPaths[key] = spec.paths[key];
|
|
638
|
+
const slots = collectSchemaSlots(sortedPaths);
|
|
639
|
+
const existing = spec.components?.schemas ?? {};
|
|
640
|
+
/* Group slots by canonical shape — same shape = one component. */
|
|
641
|
+
const groups = new Map();
|
|
642
|
+
for (const slot of slots) {
|
|
643
|
+
const group = groups.get(slot.canonical);
|
|
644
|
+
if (group)
|
|
645
|
+
group.push(slot);
|
|
646
|
+
else
|
|
647
|
+
groups.set(slot.canonical, [slot]);
|
|
648
|
+
}
|
|
649
|
+
/*
|
|
650
|
+
* Tier resolution per canonical group:
|
|
651
|
+
* Tier 1: unique shape (group.length === 1) → operation-derived name
|
|
652
|
+
* Tier 2: shared error envelope → Err{status}* name
|
|
653
|
+
* Tier 3: shared non-error → first-slot operation-derived name
|
|
654
|
+
* Collision (two canonicals → same tier name, or a name an existing component holds) →
|
|
655
|
+
* hash suffix, re-checked until free.
|
|
656
|
+
*/
|
|
657
|
+
const canonicalToName = new Map();
|
|
658
|
+
const nameToCanonical = new Map();
|
|
659
|
+
for (const [canonical, groupSlots] of groups) {
|
|
660
|
+
const firstSlot = groupSlots[0];
|
|
661
|
+
let base;
|
|
662
|
+
if (groupSlots.length > 1 && isErrorEnvelope(firstSlot.schema)) {
|
|
663
|
+
const fallbackStatus = firstSlot.context.role === "response" ? firstSlot.context.status : undefined;
|
|
664
|
+
base = deriveErrorEnvelopeName(firstSlot.schema, fallbackStatus);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
base = deriveSchemaName(firstSlot.context);
|
|
668
|
+
}
|
|
669
|
+
const name = uniqueName(base, canonical, (n) => Object.hasOwn(existing, n) || (nameToCanonical.has(n) && nameToCanonical.get(n) !== canonical));
|
|
670
|
+
nameToCanonical.set(name, canonical);
|
|
671
|
+
canonicalToName.set(canonical, name);
|
|
672
|
+
}
|
|
673
|
+
/* Build slotNames map (SchemaSlot → resolved name) for hoisting + path rewriting. */
|
|
674
|
+
const slotNames = new Map();
|
|
675
|
+
for (const slot of slots) {
|
|
676
|
+
const name = canonicalToName.get(slot.canonical);
|
|
677
|
+
if (name)
|
|
678
|
+
slotNames.set(slot, name);
|
|
679
|
+
}
|
|
680
|
+
/* Nested hoisting: collect shared nested objects, then rewrite inline → $ref.
|
|
681
|
+
* Error envelope slots (any tier) mark their nested schemas as envelope-owned so
|
|
682
|
+
* envelope-exclusive nesting (e.g. `fields`) is not hoisted under a per-spec name. */
|
|
683
|
+
const { schemas: hoistedSchemas, canonicalToName: hoistedCanonicalToName } = collectHoistedSchemas(slots, slotNames, (n) => Object.hasOwn(existing, n) || nameToCanonical.has(n));
|
|
684
|
+
/* Build components map, rewriting nested fields → $refs where hoisted */
|
|
685
|
+
const extractedSchemas = {};
|
|
686
|
+
for (const slot of slots) {
|
|
687
|
+
const name = slotNames.get(slot);
|
|
688
|
+
if (name && !Object.hasOwn(extractedSchemas, name)) {
|
|
689
|
+
extractedSchemas[name] =
|
|
690
|
+
hoistedCanonicalToName.size > 0 ? rewriteNestedRefs(slot.schema, hoistedCanonicalToName) : slot.schema;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const hasNewSchemas = Object.keys(extractedSchemas).length > 0 || Object.keys(hoistedSchemas).length > 0;
|
|
694
|
+
/* Nothing to extract and no existing components — return unchanged */
|
|
695
|
+
if (!hasNewSchemas && !spec.components)
|
|
696
|
+
return spec;
|
|
697
|
+
const paths = slotNames.size > 0 || hoistedCanonicalToName.size > 0
|
|
698
|
+
? rewritePathsToRefs(sortedPaths, slots, slotNames, hoistedCanonicalToName)
|
|
699
|
+
: sortedPaths;
|
|
700
|
+
const mergedSchemas = { ...existing, ...extractedSchemas };
|
|
701
|
+
/* a hoisted nested schema whose every user became a component $ref is still referenced from
|
|
702
|
+
that component; one referenced from nowhere is dropped */
|
|
703
|
+
const referenced = new Set();
|
|
704
|
+
collectComponentRefs(paths, referenced);
|
|
705
|
+
collectComponentRefs(mergedSchemas, referenced);
|
|
706
|
+
for (const [name, schema] of Object.entries(hoistedSchemas)) {
|
|
707
|
+
if (referenced.has(name))
|
|
708
|
+
mergedSchemas[name] = schema;
|
|
709
|
+
}
|
|
710
|
+
const components = {
|
|
711
|
+
...spec.components,
|
|
712
|
+
...(Object.keys(mergedSchemas).length > 0 ? { schemas: mergedSchemas } : {}),
|
|
713
|
+
};
|
|
714
|
+
return { ...spec, components, paths };
|
|
715
|
+
}
|
|
716
|
+
export function sanitizeOpenApiSpec(spec, options) {
|
|
717
|
+
const result = cloneJson(spec);
|
|
718
|
+
if (options.stripSecuritySchemes?.length && result.components?.securitySchemes) {
|
|
719
|
+
for (const name of options.stripSecuritySchemes) {
|
|
720
|
+
delete result.components.securitySchemes[name];
|
|
721
|
+
}
|
|
722
|
+
if (Object.keys(result.components.securitySchemes).length === 0) {
|
|
723
|
+
delete result.components.securitySchemes;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
for (const methods of Object.values(result.paths)) {
|
|
727
|
+
for (const operation of Object.values(methods)) {
|
|
728
|
+
const op = operation;
|
|
729
|
+
if (options.stripSecurityRequirements?.length && Array.isArray(op.security)) {
|
|
730
|
+
op.security = op.security.filter((s) => !options.stripSecurityRequirements?.some((name) => name in s));
|
|
731
|
+
if (op.security.length === 0)
|
|
732
|
+
delete op.security;
|
|
733
|
+
}
|
|
734
|
+
if (options.stripXExtensions) {
|
|
735
|
+
const targets = options.stripXExtensions === true
|
|
736
|
+
? Object.keys(op).filter((k) => k.startsWith("x-"))
|
|
737
|
+
: options.stripXExtensions;
|
|
738
|
+
for (const ext of targets)
|
|
739
|
+
delete op[ext];
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
function cloneJson(value) {
|
|
746
|
+
try {
|
|
747
|
+
return structuredClone(value);
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return JSON.parse(JSON.stringify(value));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/* Inlining expands each $ref; a graph of shared refs can grow exponentially (`L30` with two
|
|
754
|
+
fields that both point at `L29`, …), so the output is bounded and generation fails loudly. */
|
|
755
|
+
const MAX_INLINED_SCHEMA_NODES = 200_000;
|
|
756
|
+
function chargeInline(budget) {
|
|
757
|
+
if (++budget.nodes > MAX_INLINED_SCHEMA_NODES) {
|
|
758
|
+
throw new Error(`SDK codegen: inlined schemas exceed ${MAX_INLINED_SCHEMA_NODES} nodes (deeply shared $refs); ` +
|
|
759
|
+
"flatten the shared component chain");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function resolveSchema(schema, schemas, budget, visited = new Set()) {
|
|
763
|
+
if (typeof schema.$ref === "string" && schema.$ref.startsWith("#/components/schemas/")) {
|
|
764
|
+
const name = schema.$ref.slice("#/components/schemas/".length);
|
|
765
|
+
/* circular ref guard — return empty schema which maps to unknown in IR */
|
|
766
|
+
if (visited.has(name))
|
|
767
|
+
return {};
|
|
768
|
+
const resolved = Object.hasOwn(schemas, name) ? schemas[name] : undefined;
|
|
769
|
+
if (!resolved)
|
|
770
|
+
throw new Error(`$ref points to nonexistent component: ${schema.$ref}`);
|
|
771
|
+
const next = new Set(visited);
|
|
772
|
+
next.add(name);
|
|
773
|
+
return resolveSchemaDeep(cloneJson(resolved), schemas, budget, next);
|
|
774
|
+
}
|
|
775
|
+
return resolveSchemaDeep(schema, schemas, budget, visited);
|
|
776
|
+
}
|
|
777
|
+
function resolveSchemaDeep(schema, schemas, budget, visited) {
|
|
778
|
+
chargeInline(budget);
|
|
779
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
780
|
+
const arr = schema[key];
|
|
781
|
+
if (Array.isArray(arr)) {
|
|
782
|
+
schema[key] = arr.map((item) => {
|
|
783
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
784
|
+
return resolveSchema(item, schemas, budget, visited);
|
|
785
|
+
}
|
|
786
|
+
return item;
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/* resolve array items */
|
|
791
|
+
if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
|
|
792
|
+
schema.items = resolveSchema(schema.items, schemas, budget, visited);
|
|
793
|
+
}
|
|
794
|
+
/* resolve object properties */
|
|
795
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
796
|
+
const props = schema.properties;
|
|
797
|
+
for (const [k, v] of Object.entries(props)) {
|
|
798
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
799
|
+
props[k] = resolveSchema(v, schemas, budget, visited);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/* resolve additionalProperties */
|
|
804
|
+
if (schema.additionalProperties &&
|
|
805
|
+
typeof schema.additionalProperties === "object" &&
|
|
806
|
+
!Array.isArray(schema.additionalProperties)) {
|
|
807
|
+
schema.additionalProperties = resolveSchema(schema.additionalProperties, schemas, budget, visited);
|
|
808
|
+
}
|
|
809
|
+
return schema;
|
|
810
|
+
}
|
|
811
|
+
export function resolveRefs(spec) {
|
|
812
|
+
const schemas = spec.components?.schemas;
|
|
813
|
+
if (!schemas || Object.keys(schemas).length === 0)
|
|
814
|
+
return spec;
|
|
815
|
+
const paths = cloneJson(spec.paths);
|
|
816
|
+
const budget = { nodes: 0 };
|
|
817
|
+
for (const methods of Object.values(paths)) {
|
|
818
|
+
for (const operation of Object.values(methods)) {
|
|
819
|
+
const op = operation;
|
|
820
|
+
const requestBody = op.requestBody;
|
|
821
|
+
if (requestBody) {
|
|
822
|
+
const content = requestBody.content;
|
|
823
|
+
if (content) {
|
|
824
|
+
for (const [ct, mediaType] of Object.entries(content)) {
|
|
825
|
+
const schema = mediaType.schema;
|
|
826
|
+
if (schema)
|
|
827
|
+
content[ct] = { ...mediaType, schema: resolveSchema(schema, schemas, budget) };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const responses = op.responses;
|
|
832
|
+
if (responses) {
|
|
833
|
+
for (const [status, response] of Object.entries(responses)) {
|
|
834
|
+
const content = response.content;
|
|
835
|
+
if (!content)
|
|
836
|
+
continue;
|
|
837
|
+
/* every content type resolved, not only the last one written */
|
|
838
|
+
const resolved = {};
|
|
839
|
+
for (const [ct, mediaType] of Object.entries(content)) {
|
|
840
|
+
const schema = mediaType.schema;
|
|
841
|
+
resolved[ct] = schema ? { ...mediaType, schema: resolveSchema(schema, schemas, budget) } : mediaType;
|
|
842
|
+
}
|
|
843
|
+
responses[status] = { ...response, content: resolved };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const { schemas: _schemas, ...restComponents } = spec.components ?? {};
|
|
849
|
+
const components = Object.keys(restComponents).length > 0 ? restComponents : undefined;
|
|
850
|
+
return { ...spec, components, paths };
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Zod node kinds JSON Schema has no word for. Zod emits `{}` for each of them under
|
|
854
|
+
* `unrepresentable: "any"`; the degrade is per node, and the rest of the schema survives.
|
|
855
|
+
*/
|
|
856
|
+
const ZOD_UNREPRESENTABLE = new Set(["custom", "function", "map", "nan", "promise", "set", "symbol", "transform"]);
|
|
857
|
+
/** `override` for Zod's converter: give `Date` and `BigInt` their wire shape, record what degraded. */
|
|
858
|
+
function zodOverride(degraded) {
|
|
859
|
+
return (ctx) => {
|
|
860
|
+
const def = zodDefOf(ctx.zodSchema);
|
|
861
|
+
const kind = String(def?.type ?? "");
|
|
862
|
+
if (kind === "date") {
|
|
863
|
+
/* JSON.stringify writes a Date as an ISO string */
|
|
864
|
+
ctx.jsonSchema.format = "date-time";
|
|
865
|
+
ctx.jsonSchema.type = "string";
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (kind === "bigint") {
|
|
869
|
+
ctx.jsonSchema.format = "int64";
|
|
870
|
+
ctx.jsonSchema.type = "integer";
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (ZOD_UNREPRESENTABLE.has(kind) && Object.keys(ctx.jsonSchema).length === 0)
|
|
874
|
+
degraded.add(kind);
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/** Zod 4 puts converters on the schema instance. Use those first so Workers
|
|
878
|
+
* (where `import("zod")` does not resolve) still emit real JSON Schema. */
|
|
879
|
+
function zodInstanceToJsonSchema(schema, io, degraded) {
|
|
880
|
+
const libraryOptions = { override: zodOverride(degraded), unrepresentable: "any" };
|
|
881
|
+
const jsonSchema = schema["~standard"].jsonSchema;
|
|
882
|
+
const fromStandard = jsonSchema?.[io];
|
|
883
|
+
if (typeof fromStandard === "function") {
|
|
884
|
+
const result = fromStandard({ libraryOptions, target: "draft-2020-12" });
|
|
885
|
+
if (result && typeof result === "object")
|
|
886
|
+
return result;
|
|
887
|
+
}
|
|
888
|
+
const inst = schema.toJSONSchema;
|
|
889
|
+
if (typeof inst === "function") {
|
|
890
|
+
const result = inst.call(schema, { ...libraryOptions, io });
|
|
891
|
+
if (result && typeof result === "object")
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
if (toJSONSchemaFn) {
|
|
895
|
+
const convert = toJSONSchemaFn;
|
|
896
|
+
const result = convert(schema, { ...libraryOptions, io });
|
|
897
|
+
if (result && typeof result === "object")
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
return undefined;
|
|
901
|
+
}
|
|
902
|
+
function applyZodBag(schema, json) {
|
|
903
|
+
const bag = schema._zod?.bag;
|
|
904
|
+
if (!bag)
|
|
905
|
+
return json;
|
|
906
|
+
if (typeof bag.format === "string") {
|
|
907
|
+
if (bag.format === "safeint")
|
|
908
|
+
json.type = "integer";
|
|
909
|
+
else
|
|
910
|
+
json.format = bag.format;
|
|
911
|
+
}
|
|
912
|
+
if (json.type === "string") {
|
|
913
|
+
if (typeof bag.minimum === "number")
|
|
914
|
+
json.minLength = bag.minimum;
|
|
915
|
+
if (typeof bag.maximum === "number")
|
|
916
|
+
json.maxLength = bag.maximum;
|
|
917
|
+
}
|
|
918
|
+
if (json.type === "number" || json.type === "integer") {
|
|
919
|
+
if (typeof bag.minimum === "number")
|
|
920
|
+
json.minimum = bag.minimum;
|
|
921
|
+
if (typeof bag.maximum === "number")
|
|
922
|
+
json.maximum = bag.maximum;
|
|
923
|
+
}
|
|
924
|
+
return json;
|
|
925
|
+
}
|
|
926
|
+
function zodDefOf(schema) {
|
|
927
|
+
const s = schema;
|
|
928
|
+
return s._def ?? s.def ?? s._zod?.def;
|
|
929
|
+
}
|
|
930
|
+
function zodDefIsOptional(def, io) {
|
|
931
|
+
const t = String(def.typeName ?? def.type ?? "");
|
|
932
|
+
if (t === "ZodOptional" || t === "optional")
|
|
933
|
+
return true;
|
|
934
|
+
/* a default fills the value in: optional on the way in, always present on the way out */
|
|
935
|
+
if ((t === "ZodDefault" || t === "default" || t === "prefault") && io === "input")
|
|
936
|
+
return true;
|
|
937
|
+
if (t === "ZodNullable" ||
|
|
938
|
+
t === "nullable" ||
|
|
939
|
+
t === "ZodDefault" ||
|
|
940
|
+
t === "default" ||
|
|
941
|
+
t === "prefault" ||
|
|
942
|
+
t === "ZodCatch" ||
|
|
943
|
+
t === "catch" ||
|
|
944
|
+
t === "ZodReadonly" ||
|
|
945
|
+
t === "readonly") {
|
|
946
|
+
const inner = def.innerType;
|
|
947
|
+
if (inner) {
|
|
948
|
+
const innerDef = zodDefOf(inner);
|
|
949
|
+
if (innerDef)
|
|
950
|
+
return zodDefIsOptional(innerDef, io);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
function literalSchema(values) {
|
|
956
|
+
if (values.length === 1)
|
|
957
|
+
return { const: values[0] };
|
|
958
|
+
return { enum: [...values] };
|
|
959
|
+
}
|
|
960
|
+
/** Walk Zod 3/4 internals. Survives Workers bundles that drop `toJSONSchema`. */
|
|
961
|
+
function zodDefToJsonSchema(schema, io = "output", depth = 0) {
|
|
962
|
+
if (depth > 24)
|
|
963
|
+
return {};
|
|
964
|
+
const def = zodDefOf(schema);
|
|
965
|
+
if (!def)
|
|
966
|
+
return undefined;
|
|
967
|
+
const typeName = String(def.typeName ?? def.type ?? "");
|
|
968
|
+
const walk = (child) => zodDefToJsonSchema(child, io, depth + 1) ?? {};
|
|
969
|
+
switch (typeName) {
|
|
970
|
+
case "ZodString":
|
|
971
|
+
case "string":
|
|
972
|
+
return applyZodBag(schema, { type: "string" });
|
|
973
|
+
case "ZodNumber":
|
|
974
|
+
case "number":
|
|
975
|
+
return applyZodBag(schema, { type: "number" });
|
|
976
|
+
case "ZodBoolean":
|
|
977
|
+
case "boolean":
|
|
978
|
+
return { type: "boolean" };
|
|
979
|
+
case "ZodBigInt":
|
|
980
|
+
case "bigint":
|
|
981
|
+
return { format: "int64", type: "integer" };
|
|
982
|
+
case "ZodDate":
|
|
983
|
+
case "date":
|
|
984
|
+
return { format: "date-time", type: "string" };
|
|
985
|
+
case "ZodFile":
|
|
986
|
+
case "file":
|
|
987
|
+
/* so form emit can see format:binary when toJSONSchema is unavailable */
|
|
988
|
+
return { contentEncoding: "binary", format: "binary", type: "string" };
|
|
989
|
+
case "ZodNull":
|
|
990
|
+
case "null":
|
|
991
|
+
return { type: "null" };
|
|
992
|
+
case "ZodUndefined":
|
|
993
|
+
case "ZodVoid":
|
|
994
|
+
case "undefined":
|
|
995
|
+
case "void":
|
|
996
|
+
case "ZodAny":
|
|
997
|
+
case "ZodUnknown":
|
|
998
|
+
case "any":
|
|
999
|
+
case "unknown":
|
|
1000
|
+
return {};
|
|
1001
|
+
case "ZodLiteral":
|
|
1002
|
+
case "literal": {
|
|
1003
|
+
const values = Array.isArray(def.values) ? def.values : [def.value];
|
|
1004
|
+
return literalSchema(values.map((v) => (typeof v === "bigint" ? Number(v) : v)));
|
|
1005
|
+
}
|
|
1006
|
+
case "ZodEnum":
|
|
1007
|
+
case "enum": {
|
|
1008
|
+
const raw = (def.values ?? def.entries);
|
|
1009
|
+
let list;
|
|
1010
|
+
if (Array.isArray(raw))
|
|
1011
|
+
list = raw;
|
|
1012
|
+
else {
|
|
1013
|
+
/* a TS numeric enum carries reverse mappings: { A: 0, "0": "A" } — keep the values */
|
|
1014
|
+
const entries = raw;
|
|
1015
|
+
list = Object.keys(entries)
|
|
1016
|
+
.filter((k) => typeof entries[entries[k]] !== "number")
|
|
1017
|
+
.map((k) => entries[k]);
|
|
1018
|
+
}
|
|
1019
|
+
const types = new Set(list.map((v) => typeof v));
|
|
1020
|
+
if (types.size === 1 && types.has("string"))
|
|
1021
|
+
return { enum: list, type: "string" };
|
|
1022
|
+
if (types.size === 1 && types.has("number"))
|
|
1023
|
+
return { enum: list, type: "number" };
|
|
1024
|
+
return { enum: list };
|
|
1025
|
+
}
|
|
1026
|
+
case "ZodObject":
|
|
1027
|
+
case "object": {
|
|
1028
|
+
const rawShape = def.shape;
|
|
1029
|
+
const shape = typeof rawShape === "function" ? rawShape() : rawShape;
|
|
1030
|
+
if (!shape)
|
|
1031
|
+
return { type: "object" };
|
|
1032
|
+
const properties = {};
|
|
1033
|
+
const required = [];
|
|
1034
|
+
for (const key of Object.keys(shape)) {
|
|
1035
|
+
const prop = shape[key];
|
|
1036
|
+
properties[key] = walk(prop);
|
|
1037
|
+
const propDef = zodDefOf(prop);
|
|
1038
|
+
if (!propDef || !zodDefIsOptional(propDef, io))
|
|
1039
|
+
required.push(key);
|
|
1040
|
+
}
|
|
1041
|
+
const result = { properties, type: "object" };
|
|
1042
|
+
if (io === "output")
|
|
1043
|
+
result.additionalProperties = false;
|
|
1044
|
+
if (required.length > 0)
|
|
1045
|
+
result.required = required;
|
|
1046
|
+
return result;
|
|
1047
|
+
}
|
|
1048
|
+
case "ZodArray":
|
|
1049
|
+
case "array":
|
|
1050
|
+
return { items: walk(def.element ?? def.type), type: "array" };
|
|
1051
|
+
case "ZodOptional":
|
|
1052
|
+
case "optional":
|
|
1053
|
+
return zodDefToJsonSchema(def.innerType, io, depth + 1);
|
|
1054
|
+
case "ZodNullable":
|
|
1055
|
+
case "nullable":
|
|
1056
|
+
return { anyOf: [walk(def.innerType), { type: "null" }] };
|
|
1057
|
+
case "ZodUnion":
|
|
1058
|
+
case "ZodDiscriminatedUnion":
|
|
1059
|
+
case "union":
|
|
1060
|
+
return { anyOf: (def.options ?? []).map(walk) };
|
|
1061
|
+
case "ZodIntersection":
|
|
1062
|
+
case "intersection":
|
|
1063
|
+
return { allOf: [walk(def.left), walk(def.right)] };
|
|
1064
|
+
case "ZodRecord":
|
|
1065
|
+
case "record":
|
|
1066
|
+
return { additionalProperties: walk(def.valueType), type: "object" };
|
|
1067
|
+
case "ZodTuple":
|
|
1068
|
+
case "tuple": {
|
|
1069
|
+
const items = (def.items ?? []).map(walk);
|
|
1070
|
+
const out = { prefixItems: items, type: "array" };
|
|
1071
|
+
if (def.rest)
|
|
1072
|
+
out.items = walk(def.rest);
|
|
1073
|
+
else {
|
|
1074
|
+
out.items = false;
|
|
1075
|
+
out.maxItems = items.length;
|
|
1076
|
+
}
|
|
1077
|
+
out.minItems = items.length;
|
|
1078
|
+
return out;
|
|
1079
|
+
}
|
|
1080
|
+
case "ZodDefault":
|
|
1081
|
+
case "default":
|
|
1082
|
+
case "prefault": {
|
|
1083
|
+
const inner = walk(def.innerType);
|
|
1084
|
+
const value = def.defaultValue;
|
|
1085
|
+
const resolved = typeof value === "function" ? undefined : value;
|
|
1086
|
+
return resolved === undefined ? inner : { ...inner, default: resolved };
|
|
1087
|
+
}
|
|
1088
|
+
case "ZodCatch":
|
|
1089
|
+
case "catch":
|
|
1090
|
+
case "ZodReadonly":
|
|
1091
|
+
case "readonly":
|
|
1092
|
+
return zodDefToJsonSchema(def.innerType, io, depth + 1);
|
|
1093
|
+
case "ZodPipeline":
|
|
1094
|
+
case "pipe":
|
|
1095
|
+
/* a pipe validates `in` and returns `out` */
|
|
1096
|
+
return zodDefToJsonSchema(io === "input" ? def.in : def.out, io, depth + 1);
|
|
1097
|
+
case "ZodBranded":
|
|
1098
|
+
return zodDefToJsonSchema(def.type, io, depth + 1);
|
|
1099
|
+
case "ZodLazy":
|
|
1100
|
+
case "lazy": {
|
|
1101
|
+
const getter = def.getter;
|
|
1102
|
+
return typeof getter === "function" ? zodDefToJsonSchema(getter(), io, depth + 1) : {};
|
|
1103
|
+
}
|
|
1104
|
+
default:
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/** How a schema that cannot be converted at all is handled. Set per `generateOpenApi` call. */
|
|
1109
|
+
let schemaFailureMode = "throw";
|
|
1110
|
+
function schemaConversionFailed(what, io, err) {
|
|
1111
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1112
|
+
const text = `[honey:codegen] schemaToJsonSchema could not convert ${what} (io=${io}): ${msg}`;
|
|
1113
|
+
if (schemaFailureMode === "throw")
|
|
1114
|
+
throw new Error(text);
|
|
1115
|
+
console.warn(`${text}. Documented as an unconstrained schema.`);
|
|
1116
|
+
return {};
|
|
1117
|
+
}
|
|
1118
|
+
function schemaToJsonSchema(schema, io = "output") {
|
|
1119
|
+
/* Pre-serialized JSON-schema blob or non-schema value — no ~standard marker.
|
|
1120
|
+
* Arktype schemas are functions but still carry the marker, so pass those through too. */
|
|
1121
|
+
if (schema === null ||
|
|
1122
|
+
(typeof schema !== "object" && typeof schema !== "function") ||
|
|
1123
|
+
!("~standard" in schema)) {
|
|
1124
|
+
return schema;
|
|
1125
|
+
}
|
|
1126
|
+
const vendor = schema["~standard"].vendor;
|
|
1127
|
+
if (vendor === "zod") {
|
|
1128
|
+
const kind = schema?._zod?.def?.type ?? "unknown";
|
|
1129
|
+
const degraded = new Set();
|
|
1130
|
+
let result;
|
|
1131
|
+
try {
|
|
1132
|
+
result = zodInstanceToJsonSchema(schema, io, degraded) ?? zodDefToJsonSchema(schema, io);
|
|
1133
|
+
}
|
|
1134
|
+
catch (err) {
|
|
1135
|
+
return schemaConversionFailed(`zod/${kind}`, io, err);
|
|
1136
|
+
}
|
|
1137
|
+
if (!result)
|
|
1138
|
+
return schemaConversionFailed(`zod/${kind}`, io, new Error("no converter available"));
|
|
1139
|
+
if (degraded.size > 0) {
|
|
1140
|
+
console.warn(`[honey:codegen] schemaToJsonSchema: zod/${kind} (io=${io}) has ${[...degraded].sort().join(", ")} ` +
|
|
1141
|
+
"node(s) JSON Schema cannot express; each is documented as an unconstrained schema. " +
|
|
1142
|
+
'Describe them with .meta({ ... }) or z.toJSONSchema\'s "override".');
|
|
1143
|
+
}
|
|
1144
|
+
return sanitizeZodJsonSchema(result);
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
if (vendor === "valibot")
|
|
1148
|
+
return valibotToJsonSchema(schema);
|
|
1149
|
+
if (vendor === "arktype")
|
|
1150
|
+
return arkTypeToJsonSchema(schema);
|
|
1151
|
+
if (vendor === "yup")
|
|
1152
|
+
return yupToJsonSchema(schema);
|
|
1153
|
+
if (vendor === "effect")
|
|
1154
|
+
return effectToJsonSchema(schema);
|
|
1155
|
+
}
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
return schemaConversionFailed(vendor, io, err);
|
|
1158
|
+
}
|
|
1159
|
+
/* no converter for this vendor: say so once per schema, never emit the introspection object */
|
|
1160
|
+
console.warn(`[honey:codegen] schemaToJsonSchema: no JSON Schema converter for vendor "${vendor}"; ` +
|
|
1161
|
+
"documented as an unconstrained schema.");
|
|
1162
|
+
return {};
|
|
1163
|
+
}
|
|
1164
|
+
export function generateManifest(app, options = {}) {
|
|
1165
|
+
const factory = getErrorFactory(app);
|
|
1166
|
+
const published = options.visibility === "published";
|
|
1167
|
+
const publishable = published ? publishableMetaKeys(metaSpecOf(app)) : null;
|
|
1168
|
+
const collected = collectRoutes(app).filter(({ handler, method, path }) => {
|
|
1169
|
+
if (published && isMetaInternal(handler))
|
|
1170
|
+
return false;
|
|
1171
|
+
return !options.filterRoutes || options.filterRoutes({ meta: handler.mt ?? EMPTY_OBJ, method, path });
|
|
1172
|
+
});
|
|
1173
|
+
/* each key resolved against the factory of a route that declares it — a mounted sub-app keeps its own */
|
|
1174
|
+
const allErrorKeys = new Map();
|
|
1175
|
+
const routes = collected.map(({ handler, method, path }) => {
|
|
1176
|
+
const entry = {
|
|
1177
|
+
errors: Array.from(handler.ek),
|
|
1178
|
+
meta: publishable
|
|
1179
|
+
? Object.fromEntries(Object.entries(handler.mt ?? EMPTY_OBJ).filter(([key]) => publishable(key)))
|
|
1180
|
+
: (handler.mt ?? EMPTY_OBJ),
|
|
1181
|
+
method,
|
|
1182
|
+
middleware: handler.mw.map((mw) => mw.name || "anonymous"),
|
|
1183
|
+
params: extractParams(path),
|
|
1184
|
+
path,
|
|
1185
|
+
};
|
|
1186
|
+
const routeFactory = handler.fac ?? factory;
|
|
1187
|
+
for (const ek of handler.ek) {
|
|
1188
|
+
if (!allErrorKeys.has(ek))
|
|
1189
|
+
allErrorKeys.set(ek, routeFactory);
|
|
1190
|
+
}
|
|
1191
|
+
if (handler.iv) {
|
|
1192
|
+
entry.input = Object.fromEntries(Object.entries(handler.iv)
|
|
1193
|
+
.filter(([, v]) => v !== undefined)
|
|
1194
|
+
.map(([k, v]) => [k, introspectSchema(unwrapEntry(v))]));
|
|
1195
|
+
}
|
|
1196
|
+
if (handler.os) {
|
|
1197
|
+
entry.output = Object.fromEntries(Object.entries(handler.os)
|
|
1198
|
+
.filter(([, v]) => v !== undefined)
|
|
1199
|
+
.map(([contentType, schemas]) => {
|
|
1200
|
+
if (contentType === "redirect") {
|
|
1201
|
+
return [contentType, schemas];
|
|
1202
|
+
}
|
|
1203
|
+
return [
|
|
1204
|
+
contentType,
|
|
1205
|
+
Object.fromEntries(Object.entries(schemas)
|
|
1206
|
+
.filter(([, s]) => s !== undefined)
|
|
1207
|
+
.map(([statusKey, s]) => [statusKey, introspectSchema(s)])),
|
|
1208
|
+
];
|
|
1209
|
+
}));
|
|
1210
|
+
}
|
|
1211
|
+
return entry;
|
|
1212
|
+
});
|
|
1213
|
+
return {
|
|
1214
|
+
errors: Array.from(allErrorKeys).map(([key, keyFactory]) => resolveErrorInfo(key, keyFactory)),
|
|
1215
|
+
routes,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/* ---- OpenAPI generation ---- */
|
|
1219
|
+
export async function generateOpenApi(app, options) {
|
|
1220
|
+
await Promise.all([loadToJSONSchema(), loadEffectJsonSchema()]);
|
|
1221
|
+
const prev = getJsonSchemaConverter();
|
|
1222
|
+
const prevMode = schemaFailureMode;
|
|
1223
|
+
schemaFailureMode = options.onSchemaError ?? "throw";
|
|
1224
|
+
setJsonSchemaConverter((schema, io) => schemaToJsonSchema(schema, io));
|
|
1225
|
+
try {
|
|
1226
|
+
const { onSchemaError: _mode, ...rest } = options;
|
|
1227
|
+
const spec = generateOpenApiFromTree(app, rest);
|
|
1228
|
+
return deduplicateSchemas(spec);
|
|
1229
|
+
}
|
|
1230
|
+
finally {
|
|
1231
|
+
setJsonSchemaConverter(prev);
|
|
1232
|
+
schemaFailureMode = prevMode;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
export function extractSchemas(app) {
|
|
1236
|
+
const collected = collectRoutes(app);
|
|
1237
|
+
const result = {};
|
|
1238
|
+
for (const { handler, method, path } of collected) {
|
|
1239
|
+
const key = `${method} ${path}`;
|
|
1240
|
+
result[key] = {};
|
|
1241
|
+
if (handler.iv) {
|
|
1242
|
+
result[key].input = Object.fromEntries(Object.entries(handler.iv)
|
|
1243
|
+
.filter(([, v]) => v !== undefined)
|
|
1244
|
+
.map(([k, v]) => [k, introspectSchema(unwrapEntry(v))]));
|
|
1245
|
+
}
|
|
1246
|
+
if (handler.os) {
|
|
1247
|
+
result[key].output = Object.fromEntries(Object.entries(handler.os)
|
|
1248
|
+
.filter(([, v]) => v !== undefined)
|
|
1249
|
+
.map(([contentType, schemas]) => [
|
|
1250
|
+
contentType,
|
|
1251
|
+
Object.fromEntries(Object.entries(schemas)
|
|
1252
|
+
.filter(([, s]) => s !== undefined)
|
|
1253
|
+
.map(([statusKey, s]) => [statusKey, introspectSchema(s)])),
|
|
1254
|
+
]));
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return result;
|
|
1258
|
+
}
|
|
1259
|
+
/* ---- Static route tree codegen ---- */
|
|
1260
|
+
/**
|
|
1261
|
+
* Reject meta a generated file cannot reproduce. A route tree is JSON plus topology: a value
|
|
1262
|
+
* JSON cannot round-trip (`Date`, `RegExp`, `NaN`, `BigInt`, class instances) would be
|
|
1263
|
+
* emitted as something else, or interned together with a different value.
|
|
1264
|
+
*/
|
|
1265
|
+
function assertJsonValue(value, where, path) {
|
|
1266
|
+
const fail = (what) => {
|
|
1267
|
+
throw new Error(`${where}: ${path} is ${what} — route trees carry JSON values only`);
|
|
1268
|
+
};
|
|
1269
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
1270
|
+
return;
|
|
1271
|
+
if (typeof value === "number") {
|
|
1272
|
+
if (!Number.isFinite(value))
|
|
1273
|
+
fail(String(value));
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (value === undefined)
|
|
1277
|
+
return;
|
|
1278
|
+
if (typeof value !== "object")
|
|
1279
|
+
fail(`a ${typeof value}`);
|
|
1280
|
+
if (Array.isArray(value)) {
|
|
1281
|
+
for (let k = 0; k < value.length; k++)
|
|
1282
|
+
assertJsonValue(value[k], where, `${path}[${k}]`);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const proto = Object.getPrototypeOf(value);
|
|
1286
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
1287
|
+
const name = value.constructor?.name ?? "an object";
|
|
1288
|
+
fail(`a ${name}`);
|
|
1289
|
+
}
|
|
1290
|
+
for (const key of Object.keys(value)) {
|
|
1291
|
+
assertJsonValue(value[key], where, `${path}.${key}`);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
/** Object-literal key that defines an own property even for `__proto__`. */
|
|
1295
|
+
function keyExpr(key) {
|
|
1296
|
+
return key === "__proto__" ? '["__proto__"]' : JSON.stringify(key);
|
|
1297
|
+
}
|
|
1298
|
+
function methodMapExpr(m, intern) {
|
|
1299
|
+
const parts = Object.keys(m).map((method) => `${keyExpr(method)}: ${intern.expr(m[method])}`);
|
|
1300
|
+
return `S({ ${parts.join(", ")} })`;
|
|
1301
|
+
}
|
|
1302
|
+
function serializeNode(node, intern) {
|
|
1303
|
+
const statics = Object.keys(node.s);
|
|
1304
|
+
const sExpr = statics.length > 0
|
|
1305
|
+
? `S({ ${statics.map((k) => `${keyExpr(k)}: ${serializeNode(node.s[k], intern)}`).join(", ")} })`
|
|
1306
|
+
: "undefined";
|
|
1307
|
+
const args = [
|
|
1308
|
+
sExpr,
|
|
1309
|
+
node.m !== null ? methodMapExpr(node.m, intern) : "null",
|
|
1310
|
+
node.d !== null ? `{ n: ${JSON.stringify(node.d.n)}, c: ${serializeNode(node.d.c, intern)} }` : "null",
|
|
1311
|
+
node.w !== null ? `{ n: ${JSON.stringify(node.w.n)}, m: ${methodMapExpr(node.w.m, intern)} }` : "null",
|
|
1312
|
+
node.ws !== null ? intern.expr(node.ws) : "null",
|
|
1313
|
+
];
|
|
1314
|
+
while (args.length > 0 && (args[args.length - 1] === "null" || args[args.length - 1] === "undefined"))
|
|
1315
|
+
args.pop();
|
|
1316
|
+
return `N(${args.join(", ")})`;
|
|
1317
|
+
}
|
|
1318
|
+
function collectMetaShape(entries) {
|
|
1319
|
+
const allKeys = new Map();
|
|
1320
|
+
const keyCount = new Map();
|
|
1321
|
+
let metaHandlerCount = 0;
|
|
1322
|
+
for (const { mt } of entries) {
|
|
1323
|
+
if (mt === null || mt === undefined)
|
|
1324
|
+
continue;
|
|
1325
|
+
metaHandlerCount++;
|
|
1326
|
+
for (const [k, v] of Object.entries(mt)) {
|
|
1327
|
+
if (!allKeys.has(k))
|
|
1328
|
+
allKeys.set(k, new Set());
|
|
1329
|
+
allKeys.get(k)?.add(emitLiteral(v));
|
|
1330
|
+
keyCount.set(k, (keyCount.get(k) ?? 0) + 1);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return { allKeys, keyCount, metaHandlerCount };
|
|
1334
|
+
}
|
|
1335
|
+
function emitMetaShapeType(shape) {
|
|
1336
|
+
if (shape.allKeys.size === 0)
|
|
1337
|
+
return "";
|
|
1338
|
+
const props = [...shape.allKeys.entries()]
|
|
1339
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
1340
|
+
.map(([key, types]) => {
|
|
1341
|
+
const optional = (shape.keyCount.get(key) ?? 0) < shape.metaHandlerCount ? "?" : "";
|
|
1342
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
1343
|
+
return `\t${safeKey}${optional}: ${[...types].join(" | ")}`;
|
|
1344
|
+
});
|
|
1345
|
+
return `export type MetaShape = {\n${props.join("\n")}\n} & Record<string, unknown>\n`;
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Emit a route tree module: topology with `RouteId` leaves plus per-route data (meta, error
|
|
1349
|
+
* keys, boundary key). No handler fields — the app that loads the tree registers its routes,
|
|
1350
|
+
* which bind to these ids; leaves it does not register are delegated to its catch-all.
|
|
1351
|
+
*/
|
|
1352
|
+
function emitRouteTree(tree) {
|
|
1353
|
+
const order = [];
|
|
1354
|
+
const seen = new Set();
|
|
1355
|
+
forEachLeaf(tree.root, (_method, _path, id) => {
|
|
1356
|
+
if (!seen.has(id)) {
|
|
1357
|
+
seen.add(id);
|
|
1358
|
+
order.push(id);
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
const entries = [];
|
|
1362
|
+
for (const id of order) {
|
|
1363
|
+
const entry = tree.routes[id];
|
|
1364
|
+
if (entry === undefined)
|
|
1365
|
+
throw new Error(`route tree leaf ${id} has no route entry`);
|
|
1366
|
+
const mt = entry.mt ?? null;
|
|
1367
|
+
if (mt !== null)
|
|
1368
|
+
assertJsonValue(mt, `Route meta for ${id}`, "meta");
|
|
1369
|
+
entries.push({
|
|
1370
|
+
bek: entry.bek ?? null,
|
|
1371
|
+
ek: [...(entry.ek ?? [])],
|
|
1372
|
+
id,
|
|
1373
|
+
internalMeta: mt?.internal === true,
|
|
1374
|
+
mt,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
const intern = new InternPool();
|
|
1378
|
+
for (const e of entries) {
|
|
1379
|
+
intern.count(e.bek);
|
|
1380
|
+
if (e.ek.length > 0)
|
|
1381
|
+
intern.count(e.ek);
|
|
1382
|
+
intern.count(e.mt);
|
|
1383
|
+
intern.count(e.id);
|
|
1384
|
+
}
|
|
1385
|
+
for (const e of entries) {
|
|
1386
|
+
intern.force(e.mt, "M");
|
|
1387
|
+
intern.force(e.id, "P");
|
|
1388
|
+
}
|
|
1389
|
+
intern.seal();
|
|
1390
|
+
const metaShape = collectMetaShape(entries);
|
|
1391
|
+
const hasMetaShape = metaShape.allKeys.size > 0;
|
|
1392
|
+
const lines = [];
|
|
1393
|
+
lines.push('import type { RouteTree, TreeNode } from "@lovrozagar/honey/tree"');
|
|
1394
|
+
lines.push("");
|
|
1395
|
+
lines.push("function S<T>(o: Record<string, T>): Record<string, T> {");
|
|
1396
|
+
lines.push("\treturn Object.assign(Object.create(null) as Record<string, T>, o)");
|
|
1397
|
+
lines.push("}");
|
|
1398
|
+
lines.push('function N(s: Record<string, TreeNode> = S({}), m: Record<string, string> | null = null, d: TreeNode["d"] = null, w: TreeNode["w"] = null, ws: string | null = null): TreeNode {');
|
|
1399
|
+
lines.push("\treturn { d, m, s, w, ws }");
|
|
1400
|
+
lines.push("}");
|
|
1401
|
+
const consts = intern.emitConstLines();
|
|
1402
|
+
if (consts.length > 0) {
|
|
1403
|
+
lines.push("");
|
|
1404
|
+
for (const line of consts) {
|
|
1405
|
+
if (hasMetaShape && /^const M\d+ = /.test(line))
|
|
1406
|
+
lines.push(`${line} as unknown as MetaShape`);
|
|
1407
|
+
else
|
|
1408
|
+
lines.push(line);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
lines.push("");
|
|
1412
|
+
lines.push(`export const tree: TreeNode = ${serializeNode(tree.root, intern)}`);
|
|
1413
|
+
lines.push("");
|
|
1414
|
+
const routeLines = entries.map((e) => {
|
|
1415
|
+
const parts = [];
|
|
1416
|
+
if (e.bek !== null)
|
|
1417
|
+
parts.push(`bek: ${intern.expr(e.bek)}`);
|
|
1418
|
+
if (e.ek.length > 0)
|
|
1419
|
+
parts.push(`ek: ${intern.expr(e.ek)}`);
|
|
1420
|
+
if (e.mt !== null)
|
|
1421
|
+
parts.push(`mt: ${intern.expr(e.mt)}`);
|
|
1422
|
+
return `\t[${intern.expr(e.id)}]: ${parts.length > 0 ? `{ ${parts.join(", ")} }` : "{}"}`;
|
|
1423
|
+
});
|
|
1424
|
+
lines.push(`export const routes: RouteTree["routes"] = S({\n${routeLines.join(",\n")}\n})`);
|
|
1425
|
+
lines.push("");
|
|
1426
|
+
let code = lines.join("\n");
|
|
1427
|
+
if (hasMetaShape)
|
|
1428
|
+
code += `\n${emitMetaShapeType(metaShape)}\n`;
|
|
1429
|
+
const selectors = entries.filter((e) => !e.id.startsWith("WS ") && !e.internalMeta).map((e) => e.id);
|
|
1430
|
+
if (selectors.length > 0) {
|
|
1431
|
+
const sorted = [...new Set(selectors)].sort();
|
|
1432
|
+
const parts = sorted.map((sel) => {
|
|
1433
|
+
const ref = intern.id(sel);
|
|
1434
|
+
return ref !== undefined ? `typeof ${ref}` : JSON.stringify(sel);
|
|
1435
|
+
});
|
|
1436
|
+
code += `\nexport type RouteSelector = ${parts.join(" | ")}\n`;
|
|
1437
|
+
}
|
|
1438
|
+
const metaEntries = entries
|
|
1439
|
+
.filter((e) => e.mt !== null && Object.keys(e.mt).length > 0)
|
|
1440
|
+
.map((e) => `\t[${intern.expr(e.id)}]: ${intern.expr(e.mt)}`);
|
|
1441
|
+
const metaType = hasMetaShape ? "Record<string, MetaShape>" : "Record<string, Record<string, unknown>>";
|
|
1442
|
+
if (metaEntries.length > 0) {
|
|
1443
|
+
code += `export const meta: ${metaType} = S({\n${metaEntries.join(",\n")}\n})\n`;
|
|
1444
|
+
}
|
|
1445
|
+
code += `export const routeTree: RouteTree = { v: ${ROUTE_TREE_VERSION}, root: tree, routes, ${metaEntries.length > 0 ? "meta" : "meta: {}"} }\n`;
|
|
1446
|
+
return code;
|
|
1447
|
+
}
|
|
1448
|
+
/** Emit a route tree module from a tree (a `mergeTree` result or an `app.toRouteTree()` snapshot). */
|
|
1449
|
+
export function generateRouteTree(tree) {
|
|
1450
|
+
return emitRouteTree(tree);
|
|
1451
|
+
}
|
|
1452
|
+
export function generateRouteTreeFromApp(app) {
|
|
1453
|
+
return emitRouteTree(app.toRouteTree());
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Generate static route tree code from a pre-built RouteTree.
|
|
1457
|
+
* Supports gateway patterns: import service trees → enrich meta → mergeTree → generate.
|
|
1458
|
+
*/
|
|
1459
|
+
export function generateRouteTreeFromRouteTree(rt) {
|
|
1460
|
+
return emitRouteTree(rt);
|
|
1461
|
+
}
|
|
1462
|
+
function emitInputType(handler, state) {
|
|
1463
|
+
if (!handler.iv)
|
|
1464
|
+
return "{}";
|
|
1465
|
+
const entries = [];
|
|
1466
|
+
for (const [source, schema] of Object.entries(handler.iv)) {
|
|
1467
|
+
if (schema === undefined)
|
|
1468
|
+
continue;
|
|
1469
|
+
const unwrapped = unwrapEntry(schema);
|
|
1470
|
+
entries.push(`${source}: ${emitSchemaType(unwrapped, state)}`);
|
|
1471
|
+
}
|
|
1472
|
+
if (entries.length === 0)
|
|
1473
|
+
return "{}";
|
|
1474
|
+
return `{ ${entries.join("; ")} }`;
|
|
1475
|
+
}
|
|
1476
|
+
function emitOutputType(handler, state) {
|
|
1477
|
+
if (!handler.os)
|
|
1478
|
+
return "{}";
|
|
1479
|
+
const ctEntries = [];
|
|
1480
|
+
for (const [contentType, schemas] of Object.entries(handler.os)) {
|
|
1481
|
+
if (schemas === undefined)
|
|
1482
|
+
continue;
|
|
1483
|
+
if (contentType === "redirect") {
|
|
1484
|
+
const keys = Object.keys(schemas).filter((k) => schemas[k]);
|
|
1485
|
+
if (keys.length > 0) {
|
|
1486
|
+
ctEntries.push(`"redirect": { ${keys.map((k) => `${quoteKey(k)}: true`).join("; ")} }`);
|
|
1487
|
+
}
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
const statusEntries = [];
|
|
1491
|
+
for (const [statusKey, schema] of Object.entries(schemas)) {
|
|
1492
|
+
if (schema === undefined)
|
|
1493
|
+
continue;
|
|
1494
|
+
statusEntries.push(`${quoteKey(statusKey)}: ${emitSchemaType(schema, state)}`);
|
|
1495
|
+
}
|
|
1496
|
+
if (statusEntries.length > 0) {
|
|
1497
|
+
ctEntries.push(`${JSON.stringify(contentType)}: { ${statusEntries.join("; ")} }`);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (ctEntries.length === 0)
|
|
1501
|
+
return "{}";
|
|
1502
|
+
return `{ ${ctEntries.join("; ")} }`;
|
|
1503
|
+
}
|
|
1504
|
+
function emitMetaType(handler) {
|
|
1505
|
+
if (!handler.mt)
|
|
1506
|
+
return "{}";
|
|
1507
|
+
const entries = [];
|
|
1508
|
+
for (const [k, v] of Object.entries(handler.mt)) {
|
|
1509
|
+
entries.push(`${quoteKey(k)}: ${emitLiteral(v)}`);
|
|
1510
|
+
}
|
|
1511
|
+
if (entries.length === 0)
|
|
1512
|
+
return "{}";
|
|
1513
|
+
return `{ ${entries.join("; ")} }`;
|
|
1514
|
+
}
|
|
1515
|
+
function emitLiteral(value) {
|
|
1516
|
+
if (value === null)
|
|
1517
|
+
return "null";
|
|
1518
|
+
if (value === undefined)
|
|
1519
|
+
return "undefined";
|
|
1520
|
+
if (typeof value === "string")
|
|
1521
|
+
return JSON.stringify(value);
|
|
1522
|
+
if (typeof value === "number")
|
|
1523
|
+
return Number.isFinite(value) ? String(value) : "number";
|
|
1524
|
+
if (typeof value === "boolean")
|
|
1525
|
+
return String(value);
|
|
1526
|
+
if (typeof value === "bigint")
|
|
1527
|
+
return `${value}n`;
|
|
1528
|
+
if (Array.isArray(value)) {
|
|
1529
|
+
if (value.length === 0)
|
|
1530
|
+
return "[]";
|
|
1531
|
+
return `[${value.map((v) => emitLiteral(v)).join(", ")}]`;
|
|
1532
|
+
}
|
|
1533
|
+
if (typeof value === "object") {
|
|
1534
|
+
const obj = value;
|
|
1535
|
+
const keys = Object.keys(obj);
|
|
1536
|
+
if (keys.length === 0)
|
|
1537
|
+
return "{}";
|
|
1538
|
+
return `{ ${keys.map((k) => `${JSON.stringify(k)}: ${emitLiteral(obj[k])}`).join("; ")} }`;
|
|
1539
|
+
}
|
|
1540
|
+
return "unknown";
|
|
1541
|
+
}
|
|
1542
|
+
function emitErrorType(handler) {
|
|
1543
|
+
if (handler.ek.size === 0)
|
|
1544
|
+
return "never";
|
|
1545
|
+
return Array.from(handler.ek)
|
|
1546
|
+
.sort()
|
|
1547
|
+
.map((k) => JSON.stringify(k))
|
|
1548
|
+
.join(" | ");
|
|
1549
|
+
}
|
|
1550
|
+
function emitErrorShapes(handler, meta, state) {
|
|
1551
|
+
if (handler.ek.size === 0)
|
|
1552
|
+
return null;
|
|
1553
|
+
const entries = [];
|
|
1554
|
+
for (const key of Array.from(handler.ek).sort()) {
|
|
1555
|
+
const entry = meta?.[key];
|
|
1556
|
+
if (entry?.schema) {
|
|
1557
|
+
const schemaType = emitSchemaType(entry.schema, state);
|
|
1558
|
+
entries.push(`${quoteKey(key)}: ${schemaType}`);
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
entries.push(`${quoteKey(key)}: null`);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return `{ ${entries.join("; ")} }`;
|
|
1565
|
+
}
|
|
1566
|
+
function emitErrorsByStatus(handler, meta, factory, state) {
|
|
1567
|
+
if (handler.ek.size === 0)
|
|
1568
|
+
return null;
|
|
1569
|
+
const byStatus = new Map();
|
|
1570
|
+
for (const key of handler.ek) {
|
|
1571
|
+
const info = resolveErrorInfo(key, factory);
|
|
1572
|
+
if (info.status > 0) {
|
|
1573
|
+
let keys = byStatus.get(info.status);
|
|
1574
|
+
if (!keys) {
|
|
1575
|
+
keys = [];
|
|
1576
|
+
byStatus.set(info.status, keys);
|
|
1577
|
+
}
|
|
1578
|
+
keys.push(key);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
const entries = [];
|
|
1582
|
+
for (const [status, keys] of Array.from(byStatus.entries()).sort((a, b) => a[0] - b[0])) {
|
|
1583
|
+
/* build shape union for this status code */
|
|
1584
|
+
const shapes = [];
|
|
1585
|
+
for (const key of keys.sort()) {
|
|
1586
|
+
const entry = meta?.[key];
|
|
1587
|
+
if (entry?.schema) {
|
|
1588
|
+
shapes.push(emitSchemaType(entry.schema, state));
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
shapes.push("null");
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const shapeType = shapes.length === 1 ? shapes[0] : shapes.join(" | ");
|
|
1595
|
+
entries.push(`${status}: ${shapeType}`);
|
|
1596
|
+
}
|
|
1597
|
+
return `{ ${entries.join("; ")} }`;
|
|
1598
|
+
}
|
|
1599
|
+
function emitErrorsCtxType(handler) {
|
|
1600
|
+
if (handler.ek.size === 0)
|
|
1601
|
+
return "";
|
|
1602
|
+
const entries = Array.from(handler.ek)
|
|
1603
|
+
.sort()
|
|
1604
|
+
.map((k) => `${quoteKey(k)}: (opts?: { cause?: unknown; fields?: Record<string, { error_key: string; message: string; path: string }[]>; headers?: Record<string, string>; vars?: Record<string, string | number> }) => HoneyError`);
|
|
1605
|
+
return `readonly errors: { ${entries.join("; ")} }`;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Per-route middleware type additions. The type extractor keys routes by their full path —
|
|
1609
|
+
* basePath included — exactly as the router registers them, so the lookup is exact: a route
|
|
1610
|
+
* with no entry gets the base ctx, never another route's additions.
|
|
1611
|
+
*/
|
|
1612
|
+
function resolveRouteMiddleware(routeMiddleware, method, fullPath) {
|
|
1613
|
+
if (!routeMiddleware)
|
|
1614
|
+
return undefined;
|
|
1615
|
+
const key = `${method} ${fullPath}`;
|
|
1616
|
+
return Object.hasOwn(routeMiddleware, key) ? routeMiddleware[key] : undefined;
|
|
1617
|
+
}
|
|
1618
|
+
export function generateTypes(app, options) {
|
|
1619
|
+
const collected = collectRoutes(app, true);
|
|
1620
|
+
const factory = getErrorFactory(app);
|
|
1621
|
+
const errorMeta = getErrorMeta(factory);
|
|
1622
|
+
const baseCtxName = options.baseCtxName ?? "BaseCtx";
|
|
1623
|
+
const hasErrors = collected.some(({ handler }) => handler.ek.size > 0);
|
|
1624
|
+
const lines = [];
|
|
1625
|
+
const importParts = ["HoneyContext", "WithOutput"];
|
|
1626
|
+
if (hasErrors)
|
|
1627
|
+
importParts.push("HoneyError");
|
|
1628
|
+
lines.push(`import type { ${importParts.join(", ")} } from "@lovrozagar/honey"`);
|
|
1629
|
+
lines.push("");
|
|
1630
|
+
const envType = options.inlineEnvType ?? "Record<string, unknown>";
|
|
1631
|
+
const mwPart = options.inlineMiddlewareType ? ` & ${options.inlineMiddlewareType}` : "";
|
|
1632
|
+
if (options.inlineTapsType) {
|
|
1633
|
+
lines.push(`type TapMap = ${options.inlineTapsType}`);
|
|
1634
|
+
lines.push("");
|
|
1635
|
+
}
|
|
1636
|
+
const ctxBase = options.inlineTapsType ? `Omit<HoneyContext<${envType}>, "tap">` : `HoneyContext<${envType}>`;
|
|
1637
|
+
const tapPart = options.inlineTapsType ? " & { tap<K extends keyof TapMap>(key: K, payload: TapMap[K]): void }" : "";
|
|
1638
|
+
lines.push(`export type ${baseCtxName} = ${ctxBase}${mwPart}${tapPart}`);
|
|
1639
|
+
lines.push("");
|
|
1640
|
+
/* group routes by path */
|
|
1641
|
+
const byPath = new Map();
|
|
1642
|
+
for (const { handler, method, path } of collected) {
|
|
1643
|
+
let group = byPath.get(path);
|
|
1644
|
+
if (!group) {
|
|
1645
|
+
group = [];
|
|
1646
|
+
byPath.set(path, group);
|
|
1647
|
+
}
|
|
1648
|
+
group.push({ handler, method: method.toLowerCase() });
|
|
1649
|
+
}
|
|
1650
|
+
/* emit RouteSelector union from route graph */
|
|
1651
|
+
const selectors = new Set();
|
|
1652
|
+
for (const { handler, method, path } of collected) {
|
|
1653
|
+
if (handler._skip || isMetaInternal(handler))
|
|
1654
|
+
continue;
|
|
1655
|
+
selectors.add(`${method} ${path}`);
|
|
1656
|
+
}
|
|
1657
|
+
if (selectors.size > 0) {
|
|
1658
|
+
const sorted = [...selectors].sort();
|
|
1659
|
+
lines.push(`export type RouteSelector = ${sorted.map((s) => JSON.stringify(s)).join(" | ")}`);
|
|
1660
|
+
lines.push("");
|
|
1661
|
+
/* augment the module consumers import — an augmentation of any other name silently applies to nothing */
|
|
1662
|
+
lines.push('declare module "@lovrozagar/honey" {');
|
|
1663
|
+
lines.push("\tinterface HoneyCodegen {");
|
|
1664
|
+
lines.push(`\t\trouteSelector: RouteSelector`);
|
|
1665
|
+
lines.push("\t}");
|
|
1666
|
+
lines.push("}");
|
|
1667
|
+
lines.push("");
|
|
1668
|
+
}
|
|
1669
|
+
/* deduplicate per-route middleware additions into named types,
|
|
1670
|
+
* with sub-property dedup for large repeated property types (e.g. Drizzle DB schemas) */
|
|
1671
|
+
const mwTypeMap = new Map();
|
|
1672
|
+
let mwTypeCounter = 0;
|
|
1673
|
+
/* use structured property data from type-extractor for sub-type dedup */
|
|
1674
|
+
const routeProps = options.routeMiddlewareProps;
|
|
1675
|
+
const propTypeCount = new Map();
|
|
1676
|
+
if (routeProps) {
|
|
1677
|
+
for (const props of Object.values(routeProps)) {
|
|
1678
|
+
for (const prop of props) {
|
|
1679
|
+
if (prop.type.length >= 512) {
|
|
1680
|
+
propTypeCount.set(prop.type, (propTypeCount.get(prop.type) ?? 0) + 1);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/* emit shared sub-type aliases for duplicated property types */
|
|
1686
|
+
const subTypeMap = new Map();
|
|
1687
|
+
let subTypeCounter = 0;
|
|
1688
|
+
for (const [typeStr, count] of propTypeCount) {
|
|
1689
|
+
if (count >= 2) {
|
|
1690
|
+
const alias = `_MW${subTypeCounter++}`;
|
|
1691
|
+
subTypeMap.set(typeStr, alias);
|
|
1692
|
+
lines.push(`type ${alias} = ${typeStr}`);
|
|
1693
|
+
lines.push("");
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
function buildMwType(method, path) {
|
|
1697
|
+
const key = `${method} ${path}`;
|
|
1698
|
+
const props = routeProps && Object.hasOwn(routeProps, key) ? routeProps[key] : undefined;
|
|
1699
|
+
if (props?.length) {
|
|
1700
|
+
const entries = props.map((p) => {
|
|
1701
|
+
const typeStr = subTypeMap.get(p.type) ?? p.type;
|
|
1702
|
+
return `${p.name}${p.opt ? "?" : ""}: ${typeStr}`;
|
|
1703
|
+
});
|
|
1704
|
+
return `{ ${entries.join("; ")} }`;
|
|
1705
|
+
}
|
|
1706
|
+
/* fallback to raw string if no structured data */
|
|
1707
|
+
return resolveRouteMiddleware(options.routeMiddleware, method, path);
|
|
1708
|
+
}
|
|
1709
|
+
function dedupeRouteMiddleware(method, path) {
|
|
1710
|
+
const mwType = buildMwType(method, path);
|
|
1711
|
+
if (!mwType)
|
|
1712
|
+
return undefined;
|
|
1713
|
+
let alias = mwTypeMap.get(mwType);
|
|
1714
|
+
if (!alias) {
|
|
1715
|
+
alias = `MwCtx${mwTypeCounter++}`;
|
|
1716
|
+
mwTypeMap.set(mwType, alias);
|
|
1717
|
+
lines.push(`type ${alias} = ${mwType}`);
|
|
1718
|
+
lines.push("");
|
|
1719
|
+
}
|
|
1720
|
+
return alias;
|
|
1721
|
+
}
|
|
1722
|
+
/* pre-scan all routes to emit deduplicated middleware types before Routes */
|
|
1723
|
+
for (const [path, methods] of byPath) {
|
|
1724
|
+
for (const { method } of methods) {
|
|
1725
|
+
dedupeRouteMiddleware(method, path);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
const emitState = createTypeEmitState();
|
|
1729
|
+
const routesLines = [];
|
|
1730
|
+
routesLines.push("export type Routes = {");
|
|
1731
|
+
for (const [path, methods] of byPath) {
|
|
1732
|
+
routesLines.push(`\t${JSON.stringify(path)}: {`);
|
|
1733
|
+
for (const { handler, method } of methods) {
|
|
1734
|
+
const mwType = buildMwType(method, path);
|
|
1735
|
+
const mwAlias = mwType ? mwTypeMap.get(mwType) : undefined;
|
|
1736
|
+
const basePart = mwAlias ? `${baseCtxName} & ${mwAlias}` : baseCtxName;
|
|
1737
|
+
const additions = [];
|
|
1738
|
+
const inputType = emitInputType(handler, emitState);
|
|
1739
|
+
if (inputType !== "{}")
|
|
1740
|
+
additions.push(`input: ${inputType}`);
|
|
1741
|
+
/* the names ctx.params carries at runtime — an unnamed wildcard is "*" */
|
|
1742
|
+
const params = patternParams(parsePattern(path));
|
|
1743
|
+
if (params.length > 0) {
|
|
1744
|
+
const paramEntries = params.map((p) => `${quoteKey(p)}: string`).join("; ");
|
|
1745
|
+
additions.push(`readonly params: { ${paramEntries} }`);
|
|
1746
|
+
}
|
|
1747
|
+
const errorsCtx = emitErrorsCtxType(handler);
|
|
1748
|
+
if (errorsCtx)
|
|
1749
|
+
additions.push(errorsCtx);
|
|
1750
|
+
const ctxType = additions.length === 0 ? basePart : `${basePart} & { ${additions.join("; ")} }`;
|
|
1751
|
+
const errorType = emitErrorType(handler);
|
|
1752
|
+
const metaType = emitMetaType(handler);
|
|
1753
|
+
const outputType = emitOutputType(handler, emitState);
|
|
1754
|
+
routesLines.push(`\t\t${quoteKey(method)}: {`);
|
|
1755
|
+
routesLines.push(`\t\t\tctx: WithOutput<${ctxType}, ${outputType}>`);
|
|
1756
|
+
routesLines.push(`\t\t\terrors: ${errorType}`);
|
|
1757
|
+
/* a mounted sub-app's route keeps its own error factory */
|
|
1758
|
+
const routeFactory = handler.fac ?? factory;
|
|
1759
|
+
const routeErrorMeta = routeFactory === factory ? errorMeta : getErrorMeta(routeFactory);
|
|
1760
|
+
const shapesType = emitErrorShapes(handler, routeErrorMeta, emitState);
|
|
1761
|
+
if (shapesType) {
|
|
1762
|
+
routesLines.push(`\t\t\terrorShapes: ${shapesType}`);
|
|
1763
|
+
const byStatusType = emitErrorsByStatus(handler, routeErrorMeta, routeFactory, emitState);
|
|
1764
|
+
if (byStatusType) {
|
|
1765
|
+
routesLines.push(`\t\t\terrorsByStatus: ${byStatusType}`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
routesLines.push(`\t\t\tinput: ${inputType}`);
|
|
1769
|
+
routesLines.push(`\t\t\tmeta: ${metaType}`);
|
|
1770
|
+
routesLines.push(`\t\t\toutput: ${outputType}`);
|
|
1771
|
+
routesLines.push("\t\t}");
|
|
1772
|
+
}
|
|
1773
|
+
routesLines.push("\t}");
|
|
1774
|
+
}
|
|
1775
|
+
routesLines.push("}");
|
|
1776
|
+
routesLines.push("");
|
|
1777
|
+
for (const [name, body] of emitState.aliases) {
|
|
1778
|
+
lines.push(`type ${name} = ${body}`);
|
|
1779
|
+
lines.push("");
|
|
1780
|
+
}
|
|
1781
|
+
lines.push(...routesLines);
|
|
1782
|
+
/* route-specific type extractors — services use these instead of typeof app */
|
|
1783
|
+
lines.push("export type RouteCtx<");
|
|
1784
|
+
lines.push("\tTPath extends keyof Routes,");
|
|
1785
|
+
lines.push("\tTMethod extends keyof Routes[TPath] & string");
|
|
1786
|
+
lines.push("> = Routes[TPath][TMethod] extends { ctx: infer C } ? C : never");
|
|
1787
|
+
lines.push("");
|
|
1788
|
+
lines.push("export type RouteInput<");
|
|
1789
|
+
lines.push("\tTPath extends keyof Routes,");
|
|
1790
|
+
lines.push("\tTMethod extends keyof Routes[TPath] & string");
|
|
1791
|
+
lines.push("> = Routes[TPath][TMethod] extends { input: infer I } ? I : never");
|
|
1792
|
+
lines.push("");
|
|
1793
|
+
lines.push("export type RouteOutput<");
|
|
1794
|
+
lines.push("\tTPath extends keyof Routes,");
|
|
1795
|
+
lines.push("\tTMethod extends keyof Routes[TPath] & string");
|
|
1796
|
+
lines.push("> = Routes[TPath][TMethod] extends { output: infer O } ? O : never");
|
|
1797
|
+
lines.push("");
|
|
1798
|
+
lines.push("export type RouteErrors<");
|
|
1799
|
+
lines.push("\tTPath extends keyof Routes,");
|
|
1800
|
+
lines.push("\tTMethod extends keyof Routes[TPath] & string");
|
|
1801
|
+
lines.push("> = Routes[TPath][TMethod] extends { errors: infer E } ? E : never");
|
|
1802
|
+
lines.push("");
|
|
1803
|
+
lines.push("export type RouteErrorShapes<");
|
|
1804
|
+
lines.push("\tTPath extends keyof Routes,");
|
|
1805
|
+
lines.push("\tTMethod extends keyof Routes[TPath] & string");
|
|
1806
|
+
lines.push("> = Routes[TPath][TMethod] extends { errorShapes: infer S } ? S : never");
|
|
1807
|
+
lines.push("");
|
|
1808
|
+
lines.push("export type RouteErrorsByStatus<");
|
|
1809
|
+
lines.push("\tTPath extends keyof Routes,");
|
|
1810
|
+
lines.push("\tTMethod extends keyof Routes[TPath] & string");
|
|
1811
|
+
lines.push("> = Routes[TPath][TMethod] extends { errorsByStatus: infer S } ? S : never");
|
|
1812
|
+
lines.push("");
|
|
1813
|
+
lines.push("export type RouteMeta<");
|
|
1814
|
+
lines.push("\tTPath extends keyof Routes,");
|
|
1815
|
+
lines.push("\tTMethod extends keyof Routes[TPath] & string");
|
|
1816
|
+
lines.push("> = Routes[TPath][TMethod] extends { meta: infer M } ? M : never");
|
|
1817
|
+
lines.push("");
|
|
1818
|
+
return lines.join("\n");
|
|
1819
|
+
}
|
|
1820
|
+
export function mergeSpecs(...specs) {
|
|
1821
|
+
const merged = {
|
|
1822
|
+
info: specs[0]?.info ?? { title: "", version: "" },
|
|
1823
|
+
openapi: specs[0]?.openapi ?? "3.1.0",
|
|
1824
|
+
paths: {},
|
|
1825
|
+
};
|
|
1826
|
+
let mergedComponents;
|
|
1827
|
+
for (const spec of specs) {
|
|
1828
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
1829
|
+
if (merged.paths[path] === undefined) {
|
|
1830
|
+
merged.paths[path] = {};
|
|
1831
|
+
}
|
|
1832
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
1833
|
+
if (merged.paths[path][method] !== undefined) {
|
|
1834
|
+
throw new Error(`Merge conflict: duplicate ${method.toUpperCase()} ${path}`);
|
|
1835
|
+
}
|
|
1836
|
+
merged.paths[path][method] = operation;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (spec.components) {
|
|
1840
|
+
if (!mergedComponents)
|
|
1841
|
+
mergedComponents = {};
|
|
1842
|
+
if (spec.components.schemas) {
|
|
1843
|
+
if (!mergedComponents.schemas)
|
|
1844
|
+
mergedComponents.schemas = {};
|
|
1845
|
+
for (const [name, schema] of Object.entries(spec.components.schemas)) {
|
|
1846
|
+
if (mergedComponents.schemas[name] !== undefined) {
|
|
1847
|
+
/* allow identical schemas (same content-hash name from same app) */
|
|
1848
|
+
if (canonicalizeSchema(mergedComponents.schemas[name]) !== canonicalizeSchema(schema)) {
|
|
1849
|
+
throw new Error(`Merge conflict: duplicate component schema "${name}"`);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
else {
|
|
1853
|
+
mergedComponents.schemas[name] = schema;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (spec.components.securitySchemes) {
|
|
1858
|
+
if (!mergedComponents.securitySchemes)
|
|
1859
|
+
mergedComponents.securitySchemes = {};
|
|
1860
|
+
for (const [name, scheme] of Object.entries(spec.components.securitySchemes)) {
|
|
1861
|
+
mergedComponents.securitySchemes[name] = scheme;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
if (mergedComponents)
|
|
1867
|
+
merged.components = mergedComponents;
|
|
1868
|
+
return merged;
|
|
1869
|
+
}
|
|
1870
|
+
export function scopeSpec(spec, filter) {
|
|
1871
|
+
const result = {
|
|
1872
|
+
info: spec.info,
|
|
1873
|
+
openapi: spec.openapi,
|
|
1874
|
+
paths: {},
|
|
1875
|
+
};
|
|
1876
|
+
const hasFilter = filter.tags !== undefined ||
|
|
1877
|
+
filter.excludeTags !== undefined ||
|
|
1878
|
+
filter.pathPrefix !== undefined ||
|
|
1879
|
+
filter.operationIds !== undefined;
|
|
1880
|
+
if (spec.components)
|
|
1881
|
+
result.components = spec.components;
|
|
1882
|
+
if (!hasFilter) {
|
|
1883
|
+
result.paths = { ...spec.paths };
|
|
1884
|
+
return result;
|
|
1885
|
+
}
|
|
1886
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
1887
|
+
if (filter.pathPrefix && !path.startsWith(filter.pathPrefix))
|
|
1888
|
+
continue;
|
|
1889
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
1890
|
+
const op = operation;
|
|
1891
|
+
const opTags = (op.tags ?? []);
|
|
1892
|
+
const opId = op.operationId;
|
|
1893
|
+
if (filter.tags && !opTags.some((t) => filter.tags?.includes(t)))
|
|
1894
|
+
continue;
|
|
1895
|
+
if (filter.excludeTags && opTags.some((t) => filter.excludeTags?.includes(t)))
|
|
1896
|
+
continue;
|
|
1897
|
+
if (filter.operationIds && (!opId || !filter.operationIds.includes(opId)))
|
|
1898
|
+
continue;
|
|
1899
|
+
if (result.paths[path] === undefined) {
|
|
1900
|
+
result.paths[path] = {};
|
|
1901
|
+
}
|
|
1902
|
+
result.paths[path][method] = operation;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return result;
|
|
1906
|
+
}
|
|
1907
|
+
export function extractOpenApiPathParams(path) {
|
|
1908
|
+
const params = [];
|
|
1909
|
+
/* any name between braces — `{user-id}` is one parameter, not `{user}` and a literal `-id` */
|
|
1910
|
+
const re = /\{([^{}/]+)\}/g;
|
|
1911
|
+
let match = re.exec(path);
|
|
1912
|
+
while (match !== null) {
|
|
1913
|
+
params.push(match[1]);
|
|
1914
|
+
match = re.exec(path);
|
|
1915
|
+
}
|
|
1916
|
+
return params;
|
|
1917
|
+
}
|
|
1918
|
+
export function isSSEOperation(operation) {
|
|
1919
|
+
const responses = operation.responses;
|
|
1920
|
+
if (!responses)
|
|
1921
|
+
return false;
|
|
1922
|
+
for (const response of Object.values(responses)) {
|
|
1923
|
+
const content = response.content;
|
|
1924
|
+
if (content && "text/event-stream" in content)
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
return false;
|
|
1928
|
+
}
|
|
1929
|
+
/** JSON Schema → TypeScript type string (shim — delegates to IR pipeline). */
|
|
1930
|
+
export function jsonSchemaToTS(schema, depth = 0, binary = "string") {
|
|
1931
|
+
if (!schema || depth > 8)
|
|
1932
|
+
return "unknown";
|
|
1933
|
+
return irToTs(schemaToIR(schema), depth, binary);
|
|
1934
|
+
}
|
|
1935
|
+
/* a file part of a multipart body is sent as a Blob/File; the runtime appends it to FormData as is */
|
|
1936
|
+
const FORM_BINARY_TS = "Blob";
|
|
1937
|
+
/* the TS SDK input field a request body fills, by its media type */
|
|
1938
|
+
function sdkBodyField(body) {
|
|
1939
|
+
if (body.kind === "stream")
|
|
1940
|
+
return "body";
|
|
1941
|
+
if (body.kind === "multipart")
|
|
1942
|
+
return "form";
|
|
1943
|
+
const essence = mediaEssence(body.contentType);
|
|
1944
|
+
if (isJsonMedia(body.contentType))
|
|
1945
|
+
return "json";
|
|
1946
|
+
if (essence === "multipart/form-data" || essence === "application/x-www-form-urlencoded")
|
|
1947
|
+
return "form";
|
|
1948
|
+
return null;
|
|
1949
|
+
}
|
|
1950
|
+
/* extract input type for an operation */
|
|
1951
|
+
function emitSDKInputType(op) {
|
|
1952
|
+
const mandatoryParts = [];
|
|
1953
|
+
const optionalParts = [];
|
|
1954
|
+
/* path params — always required */
|
|
1955
|
+
if (op.params.path.length > 0) {
|
|
1956
|
+
const entries = op.params.path.map((p) => `${sdkSafeName(p.name)}: ${irToTs(p.schema)}`).join("; ");
|
|
1957
|
+
mandatoryParts.push(`params: { ${entries} }`);
|
|
1958
|
+
}
|
|
1959
|
+
/* query params */
|
|
1960
|
+
if (op.params.query.length > 0) {
|
|
1961
|
+
const entries = op.params.query
|
|
1962
|
+
.slice()
|
|
1963
|
+
.sort((a, b) => compareCodeUnits(a.name, b.name))
|
|
1964
|
+
.map((p) => `${sdkSafeName(p.name)}${p.required === true ? "" : "?"}: ${irToTs(p.schema)}`);
|
|
1965
|
+
const searchEntry = `search: { ${entries.join("; ")} }`;
|
|
1966
|
+
if (op.params.query.some((p) => p.required === true))
|
|
1967
|
+
mandatoryParts.push(searchEntry);
|
|
1968
|
+
else
|
|
1969
|
+
optionalParts.push(searchEntry);
|
|
1970
|
+
}
|
|
1971
|
+
/* header and cookie params — typed on top of the free-form per-call `headers` / `cookies` */
|
|
1972
|
+
for (const [declared, field] of [
|
|
1973
|
+
[op.params.header, "headers"],
|
|
1974
|
+
[op.params.cookie ?? [], "cookies"],
|
|
1975
|
+
]) {
|
|
1976
|
+
if (declared.length === 0)
|
|
1977
|
+
continue;
|
|
1978
|
+
const entries = declared
|
|
1979
|
+
.slice()
|
|
1980
|
+
.sort((a, b) => compareCodeUnits(a.name, b.name))
|
|
1981
|
+
.map((p) => {
|
|
1982
|
+
/* sent as text: a header or cookie value is a string on the wire */
|
|
1983
|
+
const tsType = p.schema.kind === "scalar" && p.schema.enum ? irToTs(p.schema) : "string";
|
|
1984
|
+
return `${quoteKey(p.name)}${p.required === true ? "" : "?"}: ${tsType}`;
|
|
1985
|
+
});
|
|
1986
|
+
const entry = `${field}: { ${entries.join("; ")} }`;
|
|
1987
|
+
if (declared.some((p) => p.required === true))
|
|
1988
|
+
mandatoryParts.push(entry);
|
|
1989
|
+
else
|
|
1990
|
+
optionalParts.push(entry);
|
|
1991
|
+
}
|
|
1992
|
+
/* request body: one input field per kind; the preferred content type of each kind wins */
|
|
1993
|
+
const byField = new Map();
|
|
1994
|
+
for (const body of bodiesOf(op)) {
|
|
1995
|
+
const field = sdkBodyField(body);
|
|
1996
|
+
if (field && !byField.has(field))
|
|
1997
|
+
byField.set(field, body);
|
|
1998
|
+
}
|
|
1999
|
+
/* raw bytes first, then json, then form: the order the fields always had */
|
|
2000
|
+
for (const field of ["body", "json", "form"]) {
|
|
2001
|
+
const body = byField.get(field);
|
|
2002
|
+
if (!body)
|
|
2003
|
+
continue;
|
|
2004
|
+
let entry;
|
|
2005
|
+
if (field === "body")
|
|
2006
|
+
entry = "body: ReadableStream<Uint8Array> | Blob | ArrayBuffer | Uint8Array";
|
|
2007
|
+
else if (field === "json")
|
|
2008
|
+
entry = `json: ${body.kind === "raw" ? irToTs(body.schema) : "unknown"}`;
|
|
2009
|
+
else {
|
|
2010
|
+
const schema = body.kind === "raw" ? body.schema : body.kind === "multipart" ? body.schema : undefined;
|
|
2011
|
+
entry = `form: ${schema ? irToTs(schema, 0, FORM_BINARY_TS) : "Record<string, unknown>"}`;
|
|
2012
|
+
}
|
|
2013
|
+
if (body.required)
|
|
2014
|
+
mandatoryParts.push(entry);
|
|
2015
|
+
else
|
|
2016
|
+
optionalParts.push(entry);
|
|
2017
|
+
}
|
|
2018
|
+
const allParts = [...mandatoryParts, ...optionalParts];
|
|
2019
|
+
const type = allParts.length === 0 ? "{}" : `{ ${allParts.join("; ")} }`;
|
|
2020
|
+
return { hasMandatory: mandatoryParts.length > 0, type };
|
|
2021
|
+
}
|
|
2022
|
+
/* extract response type for an operation */
|
|
2023
|
+
function emitSDKResponseType(op) {
|
|
2024
|
+
/* SSE — shape mirrors runtime _SSEEvent (event/id/retry all optional) */
|
|
2025
|
+
if (op.extensions.sse)
|
|
2026
|
+
return "AsyncIterable<{ data: string; event?: string; id?: string; retry?: number }>";
|
|
2027
|
+
/* collect success response types (2xx) — `default` and `4XX`-style keys are never success */
|
|
2028
|
+
const successTypes = [];
|
|
2029
|
+
for (const [status, response] of Object.entries(op.responses)) {
|
|
2030
|
+
if (!/^2(?:[0-9][0-9]|XX)$/i.test(status))
|
|
2031
|
+
continue;
|
|
2032
|
+
const code = /^2XX$/i.test(status) ? 200 : Number(status);
|
|
2033
|
+
if (code === 204 || code === 205 || !response.contentType) {
|
|
2034
|
+
successTypes.push("null");
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
/* the same classes the runtime's body parser reads: JSON, text, otherwise bytes */
|
|
2038
|
+
if (isJsonMedia(response.contentType)) {
|
|
2039
|
+
successTypes.push(response.schema ? irToTs(response.schema) : "unknown");
|
|
2040
|
+
}
|
|
2041
|
+
else if (isTextMedia(response.contentType)) {
|
|
2042
|
+
successTypes.push("string");
|
|
2043
|
+
}
|
|
2044
|
+
else {
|
|
2045
|
+
successTypes.push("ArrayBuffer");
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (successTypes.length === 0)
|
|
2049
|
+
return "void";
|
|
2050
|
+
return [...new Set(successTypes)].join(" | ");
|
|
2051
|
+
}
|
|
2052
|
+
/* detect whether a JSON schema matches the standard error envelope shape */
|
|
2053
|
+
export function isStandardErrEnvelope(schema) {
|
|
2054
|
+
const props = schema.properties;
|
|
2055
|
+
if (!props)
|
|
2056
|
+
return null;
|
|
2057
|
+
const successConst = props.success?.const;
|
|
2058
|
+
if (successConst !== false)
|
|
2059
|
+
return null;
|
|
2060
|
+
const msg = props.message;
|
|
2061
|
+
if (msg?.type !== "string")
|
|
2062
|
+
return null;
|
|
2063
|
+
const sk = props.status_key;
|
|
2064
|
+
if (sk?.type !== "string")
|
|
2065
|
+
return null;
|
|
2066
|
+
const fieldsSchema = props.fields;
|
|
2067
|
+
if (fieldsSchema?.type !== "object")
|
|
2068
|
+
return null;
|
|
2069
|
+
const addl = fieldsSchema?.additionalProperties;
|
|
2070
|
+
if (addl?.type !== "array")
|
|
2071
|
+
return null;
|
|
2072
|
+
const itemSchema = addl?.items;
|
|
2073
|
+
if (itemSchema?.type !== "object")
|
|
2074
|
+
return null;
|
|
2075
|
+
const itemProps = itemSchema?.properties;
|
|
2076
|
+
for (const k of ["error_key", "message", "path"]) {
|
|
2077
|
+
if (itemProps?.[k]?.type !== "string")
|
|
2078
|
+
return null;
|
|
2079
|
+
}
|
|
2080
|
+
const statusEnum = props.status?.enum;
|
|
2081
|
+
if (!Array.isArray(statusEnum) || statusEnum.length !== 1)
|
|
2082
|
+
return null;
|
|
2083
|
+
const statusVal = statusEnum[0];
|
|
2084
|
+
if (typeof statusVal !== "number")
|
|
2085
|
+
return null;
|
|
2086
|
+
const errKeyEnum = props.error_key?.enum;
|
|
2087
|
+
if (!Array.isArray(errKeyEnum) || errKeyEnum.length === 0)
|
|
2088
|
+
return null;
|
|
2089
|
+
if (!errKeyEnum.every((k) => typeof k === "string"))
|
|
2090
|
+
return null;
|
|
2091
|
+
return { keys: errKeyEnum, status: statusVal };
|
|
2092
|
+
}
|
|
2093
|
+
/* extract error types by status code for an operation (non-2xx responses) */
|
|
2094
|
+
function emitSDKErrorsByStatusType(op, resolve) {
|
|
2095
|
+
const entries = [];
|
|
2096
|
+
for (const [status, response] of Object.entries(op.responses)) {
|
|
2097
|
+
/* only concrete error statuses: `default` and `4XX` have no single status to key on */
|
|
2098
|
+
if (!/^[45][0-9][0-9]$/.test(status))
|
|
2099
|
+
continue;
|
|
2100
|
+
if (!response.schema || !response.contentType || !isJsonMedia(response.contentType))
|
|
2101
|
+
continue;
|
|
2102
|
+
const code = Number(status);
|
|
2103
|
+
const envelope = irErrorEnvelope(response.schema, resolve);
|
|
2104
|
+
if (envelope) {
|
|
2105
|
+
const keyUnion = envelope.keys.map((k) => JSON.stringify(k)).join(" | ");
|
|
2106
|
+
entries.push(`${code}: _ErrEnvelope<${envelope.status}, ${keyUnion}>`);
|
|
2107
|
+
}
|
|
2108
|
+
else {
|
|
2109
|
+
entries.push(`${code}: ${irToTs(response.schema)}`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (entries.length === 0)
|
|
2113
|
+
return null;
|
|
2114
|
+
return `{ ${entries.join("; ")} }`;
|
|
2115
|
+
}
|
|
2116
|
+
/*
|
|
2117
|
+
* Whether a response type string is worth hoisting into a `_Res\d` alias.
|
|
2118
|
+
* Skip primitives, void/null, ArrayBuffer, and plain strings — only object or
|
|
2119
|
+
* array shapes benefit from dedupe.
|
|
2120
|
+
*/
|
|
2121
|
+
function isHoistableResponseType(t) {
|
|
2122
|
+
if (!t)
|
|
2123
|
+
return false;
|
|
2124
|
+
if (t === "void" || t === "null" || t === "string" || t === "ArrayBuffer")
|
|
2125
|
+
return false;
|
|
2126
|
+
const head = t.trimStart()[0];
|
|
2127
|
+
return head === "{" || head === "[";
|
|
2128
|
+
}
|
|
2129
|
+
function buildMethodSig(m, inpAliases, errAliases, resAliases) {
|
|
2130
|
+
let suffixType;
|
|
2131
|
+
if (m.ws)
|
|
2132
|
+
suffixType = "_WsOpts";
|
|
2133
|
+
else if (m.sse)
|
|
2134
|
+
suffixType = "_SseOpts";
|
|
2135
|
+
else
|
|
2136
|
+
suffixType = "_HttpOpts";
|
|
2137
|
+
const inpAlias = inpAliases.get(m.inputType);
|
|
2138
|
+
const resolvedInp = inpAlias ? `_Expand<${inpAlias}>` : m.inputType;
|
|
2139
|
+
const fullInputType = resolvedInp === "{}" ? suffixType : `${resolvedInp} & ${suffixType}`;
|
|
2140
|
+
const inputArg = `${m.inputHasMandatory ? "input" : "input?"}: ${fullInputType}`;
|
|
2141
|
+
let resolvedErr = null;
|
|
2142
|
+
if (m.errorsByStatusType) {
|
|
2143
|
+
const errAlias = errAliases.get(m.errorsByStatusType);
|
|
2144
|
+
resolvedErr = errAlias ? `_Expand<${errAlias}>` : m.errorsByStatusType;
|
|
2145
|
+
}
|
|
2146
|
+
const resAlias = resAliases.get(m.responseType);
|
|
2147
|
+
const resolvedRes = resAlias ? `_Expand<${resAlias}>` : m.responseType;
|
|
2148
|
+
let returnType;
|
|
2149
|
+
if (m.ws)
|
|
2150
|
+
returnType = "TypedWebSocket";
|
|
2151
|
+
else if (m.sse)
|
|
2152
|
+
returnType = m.responseType;
|
|
2153
|
+
else if (resolvedErr)
|
|
2154
|
+
returnType = `TThrow extends true ? Promise<${resolvedRes}> : Promise<SDKResult<${resolvedRes}, ${resolvedErr}>>`;
|
|
2155
|
+
else
|
|
2156
|
+
returnType = `TThrow extends true ? Promise<${resolvedRes}> : Promise<SDKResult<${resolvedRes}>>`;
|
|
2157
|
+
return { inputArg, returnType };
|
|
2158
|
+
}
|
|
2159
|
+
function buildTypeAliases(types, prefix, minCount) {
|
|
2160
|
+
const counts = new Map();
|
|
2161
|
+
for (const t of types)
|
|
2162
|
+
counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
2163
|
+
const aliases = new Map();
|
|
2164
|
+
const lines = [];
|
|
2165
|
+
let idx = 0;
|
|
2166
|
+
for (const [typeStr, count] of [...counts.entries()].sort(([a], [b]) => compareCodeUnits(a, b))) {
|
|
2167
|
+
if (count >= minCount || typeStr.length > 200) {
|
|
2168
|
+
const alias = `${prefix}${idx++}`;
|
|
2169
|
+
aliases.set(typeStr, alias);
|
|
2170
|
+
lines.push(`type ${alias} = ${typeStr}`);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
return { aliases, lines };
|
|
2174
|
+
}
|
|
2175
|
+
/* TS reserved words that must be quoted when used as object/interface property keys */
|
|
2176
|
+
const SDK_TS_RESERVED = new Set([
|
|
2177
|
+
"break",
|
|
2178
|
+
"case",
|
|
2179
|
+
"catch",
|
|
2180
|
+
"class",
|
|
2181
|
+
"const",
|
|
2182
|
+
"continue",
|
|
2183
|
+
"debugger",
|
|
2184
|
+
"default",
|
|
2185
|
+
"delete",
|
|
2186
|
+
"do",
|
|
2187
|
+
"else",
|
|
2188
|
+
"enum",
|
|
2189
|
+
"export",
|
|
2190
|
+
"extends",
|
|
2191
|
+
"false",
|
|
2192
|
+
"finally",
|
|
2193
|
+
"for",
|
|
2194
|
+
"function",
|
|
2195
|
+
"if",
|
|
2196
|
+
"import",
|
|
2197
|
+
"in",
|
|
2198
|
+
"instanceof",
|
|
2199
|
+
"new",
|
|
2200
|
+
"null",
|
|
2201
|
+
"return",
|
|
2202
|
+
"super",
|
|
2203
|
+
"switch",
|
|
2204
|
+
"this",
|
|
2205
|
+
"throw",
|
|
2206
|
+
"true",
|
|
2207
|
+
"try",
|
|
2208
|
+
"typeof",
|
|
2209
|
+
"var",
|
|
2210
|
+
"void",
|
|
2211
|
+
"while",
|
|
2212
|
+
"with",
|
|
2213
|
+
"yield",
|
|
2214
|
+
"let",
|
|
2215
|
+
"static",
|
|
2216
|
+
"implements",
|
|
2217
|
+
"interface",
|
|
2218
|
+
"package",
|
|
2219
|
+
"private",
|
|
2220
|
+
"protected",
|
|
2221
|
+
"public",
|
|
2222
|
+
"abstract",
|
|
2223
|
+
"as",
|
|
2224
|
+
"async",
|
|
2225
|
+
"await",
|
|
2226
|
+
"constructor",
|
|
2227
|
+
"declare",
|
|
2228
|
+
"from",
|
|
2229
|
+
"get",
|
|
2230
|
+
"infer",
|
|
2231
|
+
"is",
|
|
2232
|
+
"keyof",
|
|
2233
|
+
"module",
|
|
2234
|
+
"namespace",
|
|
2235
|
+
"never",
|
|
2236
|
+
"of",
|
|
2237
|
+
"readonly",
|
|
2238
|
+
"require",
|
|
2239
|
+
"set",
|
|
2240
|
+
"satisfies",
|
|
2241
|
+
"symbol",
|
|
2242
|
+
"type",
|
|
2243
|
+
"unique",
|
|
2244
|
+
"unknown",
|
|
2245
|
+
"override",
|
|
2246
|
+
]);
|
|
2247
|
+
function sdkSafeName(name) {
|
|
2248
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) || SDK_TS_RESERVED.has(name)) {
|
|
2249
|
+
return JSON.stringify(name);
|
|
2250
|
+
}
|
|
2251
|
+
return name;
|
|
2252
|
+
}
|
|
2253
|
+
function buildSDKTypes(sdkName, sdkMethods, tree, methodLookup) {
|
|
2254
|
+
const n = sdkName;
|
|
2255
|
+
const l = [];
|
|
2256
|
+
/* shared error aliases (reduce repetition for standard envelope shape) */
|
|
2257
|
+
l.push("type _ErrField = { error_key: string; message: string; path: string }");
|
|
2258
|
+
l.push("type _ErrEnvelope<TStatus extends number, TKey extends string> = {");
|
|
2259
|
+
l.push("\terror_key: TKey");
|
|
2260
|
+
l.push("\tfields: Record<string, _ErrField[]>");
|
|
2261
|
+
l.push("\tmessage: string");
|
|
2262
|
+
l.push("\tstatus: TStatus");
|
|
2263
|
+
l.push("\tstatus_key: string");
|
|
2264
|
+
l.push("\tsuccess: false");
|
|
2265
|
+
l.push("}");
|
|
2266
|
+
l.push("");
|
|
2267
|
+
/* dedupe repeated errorsByStatusType strings — the primary source of file bloat */
|
|
2268
|
+
const { aliases: errAliases, lines: errLines } = buildTypeAliases(sdkMethods.flatMap((m) => (m.errorsByStatusType ? [m.errorsByStatusType] : [])), "_Errs", 2);
|
|
2269
|
+
if (errLines.length > 0) {
|
|
2270
|
+
for (const line of errLines)
|
|
2271
|
+
l.push(line);
|
|
2272
|
+
l.push("");
|
|
2273
|
+
}
|
|
2274
|
+
/*
|
|
2275
|
+
* Dedupe response type shapes used by non-SSE/WS methods. Each method inlines
|
|
2276
|
+
* its response type TWICE (throw branch + safe branch), so hoisting once per
|
|
2277
|
+
* shape roughly halves the per-method cost. Hoist when count >= 2 OR when a
|
|
2278
|
+
* single literal is long enough (>200 chars) to dominate the line.
|
|
2279
|
+
*/
|
|
2280
|
+
const { aliases: resAliases, lines: resLines } = buildTypeAliases(sdkMethods.filter((m) => !m.ws && !m.sse && isHoistableResponseType(m.responseType)).map((m) => m.responseType), "_Res", 2);
|
|
2281
|
+
const { aliases: inpAliases, lines: inpLines } = buildTypeAliases(sdkMethods.filter((m) => m.inputType !== "{}" && isHoistableResponseType(m.inputType)).map((m) => m.inputType), "_Inp", 2);
|
|
2282
|
+
if (resLines.length > 0 || errAliases.size > 0 || inpLines.length > 0) {
|
|
2283
|
+
/*
|
|
2284
|
+
* _Expand forces TS to eagerly resolve aliases in hover tooltips. Without
|
|
2285
|
+
* it, `const b = await sdk.x.y(...)` would show `SDKResult<_Res33, _Errs0>`
|
|
2286
|
+
* instead of the full shape. The mapped type + `& {}` intersection is a
|
|
2287
|
+
* well-known trick: TS evaluates the mapped type for display but keeps the
|
|
2288
|
+
* emitted reference compact, so byte-dedupe is preserved. Two levels deep
|
|
2289
|
+
* so error records (`{ 400: _ErrEnvelope<...>; ... }`) expand their
|
|
2290
|
+
* envelope instantiations too, not just the top-level status keys.
|
|
2291
|
+
*/
|
|
2292
|
+
l.push("type _ExpandShallow<T> = T extends object ? { [K in keyof T]: T[K] } & {} : T");
|
|
2293
|
+
l.push("type _Expand<T> = T extends object ? { [K in keyof T]: _ExpandShallow<T[K]> } & {} : T");
|
|
2294
|
+
for (const line of resLines)
|
|
2295
|
+
l.push(line);
|
|
2296
|
+
for (const line of inpLines)
|
|
2297
|
+
l.push(line);
|
|
2298
|
+
l.push("");
|
|
2299
|
+
}
|
|
2300
|
+
/* SDKResult — typed branch trusts TErrorsByStatus to cover every error status (no unknown fallback) */
|
|
2301
|
+
l.push("export type SDKResult<T, TErrorsByStatus = never> =");
|
|
2302
|
+
l.push("\t| { data: T; error: null; response: Response; status: number }");
|
|
2303
|
+
l.push("\t| ([TErrorsByStatus] extends [never]");
|
|
2304
|
+
l.push("\t\t? { data: null; error: unknown; response: Response; status: number }");
|
|
2305
|
+
l.push("\t\t: { [S in keyof TErrorsByStatus & number]: { data: null; error: TErrorsByStatus[S]; response: Response; status: S } }[keyof TErrorsByStatus & number])");
|
|
2306
|
+
l.push("");
|
|
2307
|
+
/* TypedWebSocket */
|
|
2308
|
+
l.push("export type TypedWebSocket = {");
|
|
2309
|
+
l.push("\tclose(code?: number, reason?: string): void");
|
|
2310
|
+
l.push('\toff(event: "close" | "error" | "message" | "open", handler: (...args: never[]) => void): void');
|
|
2311
|
+
l.push('\ton(event: "close", handler: (code: number, reason: string) => void): void');
|
|
2312
|
+
l.push('\ton(event: "error", handler: (error: unknown) => void): void');
|
|
2313
|
+
l.push('\ton(event: "message", handler: (data: string) => void): void');
|
|
2314
|
+
l.push('\ton(event: "open", handler: () => void): void');
|
|
2315
|
+
l.push("\treadonly readyState: number");
|
|
2316
|
+
l.push("\tsend(data: ArrayBuffer | ArrayBufferView | object | string): void");
|
|
2317
|
+
l.push("}");
|
|
2318
|
+
l.push("");
|
|
2319
|
+
/* config type */
|
|
2320
|
+
l.push(`export type ${n}Config<TThrow extends boolean = false> = {`);
|
|
2321
|
+
l.push("\tbaseURL: string");
|
|
2322
|
+
l.push("\tbuildSearchParams?: (query: Record<string, unknown>) => URLSearchParams");
|
|
2323
|
+
l.push("\tcredentials?: RequestCredentials");
|
|
2324
|
+
l.push("\tfetch?: typeof fetch");
|
|
2325
|
+
l.push(`\theaders?: Record<string, string> | ((ctx: { method: string; path: string }) => Record<string, string | undefined> | Promise<Record<string, string | undefined>>)`);
|
|
2326
|
+
l.push("\tinvalidation?: { maxSourcesPerTarget?: number; staleMaxEntries?: number; staleTime: number }");
|
|
2327
|
+
l.push("\tmaxErrorMessageChars?: number");
|
|
2328
|
+
l.push("\tmode?: RequestMode");
|
|
2329
|
+
l.push(`\tonRequest?: Array<(ctx: { body?: BodyInit; headers: Headers; invalidatedBy?: string[]; isStale?: boolean; method: string; path: string; selector?: string; state: Record<string, unknown>; url: string }) => void | Promise<void>>`);
|
|
2330
|
+
l.push(`\tonResponse?: Array<(ctx: { invalidatedBy?: string[]; isRetry: boolean; isStale?: boolean; method: string; path: string; request: Request; response: Response; retry: () => Promise<Response>; selector?: string; state: Record<string, unknown>; url: string }) => Response | undefined | Promise<Response | undefined>>`);
|
|
2331
|
+
l.push("\tonAuthExpired?: (ctx: { rejectedToken: string | null }) => Promise<string | null>");
|
|
2332
|
+
l.push("\tauthHeaderName?: string");
|
|
2333
|
+
l.push("\tauthHeaderPrefix?: string");
|
|
2334
|
+
l.push("\tonLog?: (entry: _LogEntry) => void");
|
|
2335
|
+
l.push('\tredirect?: "error" | "follow" | "manual" | "same-origin"');
|
|
2336
|
+
l.push("\tsortSearchParams?: boolean");
|
|
2337
|
+
l.push("\tsseMaxBufferChars?: number");
|
|
2338
|
+
l.push("\tstate?: Record<string, unknown>");
|
|
2339
|
+
l.push("\tthrowOnError?: TThrow");
|
|
2340
|
+
l.push("\ttimeout?: number");
|
|
2341
|
+
l.push("}");
|
|
2342
|
+
l.push("");
|
|
2343
|
+
l.push(`export type _LogEntry = { level: "debug" | "info" | "warn" | "error"; event: "request_start" | "response_received" | "error" | "hook_executed"; operation: string; duration_ms: number; status?: number; error?: unknown }`);
|
|
2344
|
+
l.push("export type _HttpOpts = { cookies?: Record<string, string>; headers?: Record<string, string>; idempotencyKey?: string; signal?: AbortSignal; timeout?: number }");
|
|
2345
|
+
l.push("export type _SseOpts = { cookies?: Record<string, string>; headers?: Record<string, string>; lastEventId?: string; signal?: AbortSignal; timeout?: number }");
|
|
2346
|
+
l.push("export type _WsOpts = { protocols?: string | string[]; reconnectToken?: string }");
|
|
2347
|
+
l.push("");
|
|
2348
|
+
/* recursive interface emitter — walks IRNamespace tree */
|
|
2349
|
+
function emitNsInterface(ns, indent) {
|
|
2350
|
+
const methods = methodsOf(ns);
|
|
2351
|
+
const namespaces = namespacesOf(ns);
|
|
2352
|
+
for (const [name, op] of methods) {
|
|
2353
|
+
const m = methodLookup.get(op.id);
|
|
2354
|
+
if (!m)
|
|
2355
|
+
continue;
|
|
2356
|
+
/* #R6-27: single _call action → promote resource to callable at top level */
|
|
2357
|
+
if (name === "_call")
|
|
2358
|
+
continue; /* handled by namespace _call promotion below */
|
|
2359
|
+
const { inputArg, returnType } = buildMethodSig(m, inpAliases, errAliases, resAliases);
|
|
2360
|
+
l.push(`${indent}${sdkSafeName(name)}(${inputArg}): ${returnType}`);
|
|
2361
|
+
}
|
|
2362
|
+
for (const [name, childNs] of namespaces) {
|
|
2363
|
+
const childMethods = methodsOf(childNs);
|
|
2364
|
+
/* #R6-27: single _call method in namespace → promote namespace to callable */
|
|
2365
|
+
if (childMethods.length === 1 && childMethods[0] && childMethods[0][0] === "_call") {
|
|
2366
|
+
const [, op] = childMethods[0];
|
|
2367
|
+
const m = methodLookup.get(op.id);
|
|
2368
|
+
if (m) {
|
|
2369
|
+
const { inputArg, returnType } = buildMethodSig(m, inpAliases, errAliases, resAliases);
|
|
2370
|
+
l.push(`${indent}${sdkSafeName(name)}(${inputArg}): ${returnType}`);
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
l.push(`${indent}${sdkSafeName(name)}: {`);
|
|
2375
|
+
emitNsInterface(childNs, `${indent}\t`);
|
|
2376
|
+
l.push(`${indent}}`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
l.push(`export interface ${n}<TThrow extends boolean = false> {`);
|
|
2380
|
+
l.push("\tstate: Record<string, unknown>");
|
|
2381
|
+
l.push("\tdispose(): void");
|
|
2382
|
+
emitNsInterface(tree, "\t");
|
|
2383
|
+
l.push("}");
|
|
2384
|
+
l.push("");
|
|
2385
|
+
return l.join("\n");
|
|
2386
|
+
}
|
|
2387
|
+
function emitNestedMapNode(node, indent) {
|
|
2388
|
+
if (node.kind === "leaf") {
|
|
2389
|
+
const entry = node.entry;
|
|
2390
|
+
const parts = [`method: ${JSON.stringify(entry.method)}`, `path: ${JSON.stringify(entry.path)}`];
|
|
2391
|
+
if (entry.params)
|
|
2392
|
+
parts.push(`params: ${JSON.stringify(entry.params)}`);
|
|
2393
|
+
if (entry.sse)
|
|
2394
|
+
parts.push("sse: true");
|
|
2395
|
+
if (entry.wildcard)
|
|
2396
|
+
parts.push(`wildcard: ${JSON.stringify(entry.wildcard)}`);
|
|
2397
|
+
if (entry.ws)
|
|
2398
|
+
parts.push("ws: true");
|
|
2399
|
+
if (entry.idempotent)
|
|
2400
|
+
parts.push("idempotent: true");
|
|
2401
|
+
if (entry.invalidate)
|
|
2402
|
+
parts.push(`invalidate: ${JSON.stringify(entry.invalidate)}`);
|
|
2403
|
+
return [`{ ${parts.join(", ")} }`];
|
|
2404
|
+
}
|
|
2405
|
+
const lines = ["{"];
|
|
2406
|
+
for (const [key, child] of [...node.children.entries()].sort(([a], [b]) => compareCodeUnits(a, b))) {
|
|
2407
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
2408
|
+
const childLines = emitNestedMapNode(child, `${indent}\t`);
|
|
2409
|
+
if (childLines.length === 1) {
|
|
2410
|
+
lines.push(`${indent}\t${safeKey}: ${childLines[0]},`);
|
|
2411
|
+
}
|
|
2412
|
+
else {
|
|
2413
|
+
lines.push(`${indent}\t${safeKey}: ${childLines[0]}`);
|
|
2414
|
+
for (let i = 1; i < childLines.length - 1; i++)
|
|
2415
|
+
lines.push(childLines[i] ?? "");
|
|
2416
|
+
lines.push(`${indent}\t${childLines[childLines.length - 1]},`);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
lines.push(`${indent}}`);
|
|
2420
|
+
return lines;
|
|
2421
|
+
}
|
|
2422
|
+
function buildSDKMap(nestedMap) {
|
|
2423
|
+
const l = [];
|
|
2424
|
+
l.push("export const serviceMap = {");
|
|
2425
|
+
for (const [key, node] of [...nestedMap.entries()].sort(([a], [b]) => compareCodeUnits(a, b))) {
|
|
2426
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
2427
|
+
const childLines = emitNestedMapNode(node, "\t");
|
|
2428
|
+
if (childLines.length === 1) {
|
|
2429
|
+
l.push(`\t${safeKey}: ${childLines[0]},`);
|
|
2430
|
+
}
|
|
2431
|
+
else {
|
|
2432
|
+
l.push(`\t${safeKey}: ${childLines[0]}`);
|
|
2433
|
+
for (let i = 1; i < childLines.length - 1; i++)
|
|
2434
|
+
l.push(childLines[i] ?? "");
|
|
2435
|
+
l.push(`\t${childLines[childLines.length - 1]},`);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
l.push("} as const");
|
|
2439
|
+
l.push("");
|
|
2440
|
+
return l.join("\n");
|
|
2441
|
+
}
|
|
2442
|
+
function buildSDKIndex(sdkName, stem) {
|
|
2443
|
+
const n = sdkName;
|
|
2444
|
+
const l = [];
|
|
2445
|
+
const errorNames = ERROR_EXPORT_NAMES.join(", ");
|
|
2446
|
+
l.push(`import { ${n} as _${n}Impl, ${errorNames} } from "./${stem}.client.gen"`);
|
|
2447
|
+
l.push(`import type { ${n} as _${n}Interface } from "./${stem}.types.gen"`);
|
|
2448
|
+
l.push("");
|
|
2449
|
+
l.push(`/* declaration merge: Proxy-based class acquires typed resource methods */`);
|
|
2450
|
+
l.push(`class ${n}<TThrow extends boolean = false> extends _${n}Impl<TThrow> {}`);
|
|
2451
|
+
l.push(`interface ${n}<TThrow extends boolean = false> extends _${n}Interface<TThrow> {}`);
|
|
2452
|
+
l.push("");
|
|
2453
|
+
l.push(`export { ${n}, ${errorNames} }`);
|
|
2454
|
+
l.push(`export { serviceMap } from "./${stem}.map.gen"`);
|
|
2455
|
+
l.push(`export type { ${n}Config, SDKResult, TypedWebSocket } from "./${stem}.types.gen"`);
|
|
2456
|
+
l.push("");
|
|
2457
|
+
return l.join("\n");
|
|
2458
|
+
}
|
|
2459
|
+
function buildSDKClient(sdkName, stem) {
|
|
2460
|
+
const n = sdkName;
|
|
2461
|
+
return [
|
|
2462
|
+
sdkClientHeader(n, stem),
|
|
2463
|
+
sdkClientTypes(),
|
|
2464
|
+
sdkClientClientError(),
|
|
2465
|
+
sdkClientClass(n),
|
|
2466
|
+
sdkClientConstructor(n),
|
|
2467
|
+
sdkClientProxy(n),
|
|
2468
|
+
sdkClientInterpolatePath(),
|
|
2469
|
+
sdkClientToColonParams(),
|
|
2470
|
+
sdkClientResolveInvalidationTargets(),
|
|
2471
|
+
sdkClientPathMatchesPattern(),
|
|
2472
|
+
sdkClientLookupStale(),
|
|
2473
|
+
sdkClientCreateTypedWebSocket(),
|
|
2474
|
+
sdkClientSerializeSearch(),
|
|
2475
|
+
sdkClientResolveBaseURL(),
|
|
2476
|
+
sdkClientBuildURL(),
|
|
2477
|
+
sdkClientNewRequestId(),
|
|
2478
|
+
sdkClientBuildHeaders(),
|
|
2479
|
+
sdkClientDoRequest(),
|
|
2480
|
+
sdkClientParseBody(),
|
|
2481
|
+
sdkClientParseErrorBody(),
|
|
2482
|
+
sdkClientParseAsClientError(),
|
|
2483
|
+
sdkClientBuildSignal(),
|
|
2484
|
+
sdkClientRequestThrow(),
|
|
2485
|
+
sdkClientRequestSafe(),
|
|
2486
|
+
sdkClientRequest(),
|
|
2487
|
+
sdkClientRequestSSE(),
|
|
2488
|
+
sdkClientConnectWS(),
|
|
2489
|
+
sdkClientBuildRequestMeta(),
|
|
2490
|
+
sdkClientMarkStale(),
|
|
2491
|
+
sdkClientClearStale(),
|
|
2492
|
+
sdkClientDispose(),
|
|
2493
|
+
sdkClientDoSSE(),
|
|
2494
|
+
sdkClientParseSSEBlock(),
|
|
2495
|
+
sdkClientFooter(),
|
|
2496
|
+
].join("");
|
|
2497
|
+
}
|
|
2498
|
+
function buildSDKRuntime() {
|
|
2499
|
+
return `/* ------------------------------------------------------------------ */
|
|
2500
|
+
/* ServerFrame (inlined from protocol — zero external imports) */
|
|
2501
|
+
/* ------------------------------------------------------------------ */
|
|
2502
|
+
|
|
2503
|
+
export type ServerFrame =
|
|
2504
|
+
\t| { data: unknown; id: number; t: "msg" }
|
|
2505
|
+
\t| { t: "pong" }
|
|
2506
|
+
\t| { reason: string; t: "bye" }
|
|
2507
|
+
\t| { reconnectToken: string; t: "ready" }
|
|
2508
|
+
|
|
2509
|
+
/* ------------------------------------------------------------------ */
|
|
2510
|
+
/* Error hierarchy */
|
|
2511
|
+
/* ------------------------------------------------------------------ */
|
|
2512
|
+
|
|
2513
|
+
export class RealtimeError extends Error {
|
|
2514
|
+
\treadonly reason: string
|
|
2515
|
+
|
|
2516
|
+
\tconstructor(message: string, reason: string) {
|
|
2517
|
+
\t\tsuper(message)
|
|
2518
|
+
\t\tthis.reason = reason
|
|
2519
|
+
\t\tthis.name = "RealtimeError"
|
|
2520
|
+
\t}
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
export class RealtimeConnectError extends RealtimeError {
|
|
2524
|
+
\tconstructor(message: string, reason: string) {
|
|
2525
|
+
\t\tsuper(message, reason)
|
|
2526
|
+
\t\tthis.name = "RealtimeConnectError"
|
|
2527
|
+
\t}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
export class RealtimeAuthError extends RealtimeError {
|
|
2531
|
+
\tconstructor(message: string, reason: string) {
|
|
2532
|
+
\t\tsuper(message, reason)
|
|
2533
|
+
\t\tthis.name = "RealtimeAuthError"
|
|
2534
|
+
\t}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
export class RealtimeKickedError extends RealtimeError {
|
|
2538
|
+
\tconstructor(message: string, reason: string) {
|
|
2539
|
+
\t\tsuper(message, reason)
|
|
2540
|
+
\t\tthis.name = "RealtimeKickedError"
|
|
2541
|
+
\t}
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
export class RealtimeAbortError extends RealtimeError {
|
|
2545
|
+
\tconstructor(message: string, reason: string) {
|
|
2546
|
+
\t\tsuper(message, reason)
|
|
2547
|
+
\t\tthis.name = "RealtimeAbortError"
|
|
2548
|
+
\t}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
/* ------------------------------------------------------------------ */
|
|
2552
|
+
/* Types */
|
|
2553
|
+
/* ------------------------------------------------------------------ */
|
|
2554
|
+
|
|
2555
|
+
export type Transport = "ws" | "sse" | "longpoll"
|
|
2556
|
+
export type ConnectionState = "idle" | "connecting" | "connected" | "draining" | "reconnecting" | "closed"
|
|
2557
|
+
|
|
2558
|
+
export type TransportAdapter = {
|
|
2559
|
+
\tconnect(url: string, opts: TransportOpts): TransportConnection
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
export type TransportConnection = {
|
|
2563
|
+
\tsend(data: string): void
|
|
2564
|
+
\tclose(): void
|
|
2565
|
+
\tonFrame: (frame: ServerFrame) => void
|
|
2566
|
+
\tonClose: (reason: string) => void
|
|
2567
|
+
\tonError: (err: unknown) => void
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
export type TransportOpts = {
|
|
2571
|
+
\tsignal?: AbortSignal
|
|
2572
|
+
\theaders?: Record<string, string>
|
|
2573
|
+
\tlastId?: number
|
|
2574
|
+
\treconnectToken?: string
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
/* ------------------------------------------------------------------ */
|
|
2578
|
+
/* KeepaliveLoop */
|
|
2579
|
+
/* ------------------------------------------------------------------ */
|
|
2580
|
+
|
|
2581
|
+
export function createKeepaliveLoop(opts: {
|
|
2582
|
+
\ttransport: Transport
|
|
2583
|
+
\tsendPing: () => void
|
|
2584
|
+
\tonDead: () => void
|
|
2585
|
+
\tinterval?: number
|
|
2586
|
+
\ttimeout?: number
|
|
2587
|
+
}): { start(): void; stop(): void; onPong(): void; onFrame(): void } {
|
|
2588
|
+
\tconst interval = opts.interval ?? 25_000
|
|
2589
|
+
\tconst timeout = opts.timeout ?? 60_000
|
|
2590
|
+
\tconst transport = opts.transport
|
|
2591
|
+
|
|
2592
|
+
\tlet pingTimer: ReturnType<typeof setInterval> | null = null
|
|
2593
|
+
\tlet deadTimer: ReturnType<typeof setTimeout> | null = null
|
|
2594
|
+
|
|
2595
|
+
\tfunction clearAll(): void {
|
|
2596
|
+
\t\tif (pingTimer !== null) {
|
|
2597
|
+
\t\t\tclearInterval(pingTimer)
|
|
2598
|
+
\t\t\tpingTimer = null
|
|
2599
|
+
\t\t}
|
|
2600
|
+
\t\tif (deadTimer !== null) {
|
|
2601
|
+
\t\t\tclearTimeout(deadTimer)
|
|
2602
|
+
\t\t\tdeadTimer = null
|
|
2603
|
+
\t\t}
|
|
2604
|
+
\t}
|
|
2605
|
+
|
|
2606
|
+
\tfunction resetDeadTimer(): void {
|
|
2607
|
+
\t\tif (deadTimer !== null) {
|
|
2608
|
+
\t\t\tclearTimeout(deadTimer)
|
|
2609
|
+
\t\t}
|
|
2610
|
+
\t\tdeadTimer = setTimeout(() => {
|
|
2611
|
+
\t\t\topts.onDead()
|
|
2612
|
+
\t\t}, timeout)
|
|
2613
|
+
\t}
|
|
2614
|
+
|
|
2615
|
+
\tfunction start(): void {
|
|
2616
|
+
\t\tclearAll()
|
|
2617
|
+
\t\tif (transport === "longpoll") return
|
|
2618
|
+
\t\tif (transport === "ws") {
|
|
2619
|
+
\t\t\tpingTimer = setInterval(() => {
|
|
2620
|
+
\t\t\t\topts.sendPing()
|
|
2621
|
+
\t\t\t}, interval)
|
|
2622
|
+
\t\t\tresetDeadTimer()
|
|
2623
|
+
\t\t\treturn
|
|
2624
|
+
\t\t}
|
|
2625
|
+
\t\tif (transport === "sse") {
|
|
2626
|
+
\t\t\tresetDeadTimer()
|
|
2627
|
+
\t\t}
|
|
2628
|
+
\t}
|
|
2629
|
+
|
|
2630
|
+
\tfunction stop(): void {
|
|
2631
|
+
\t\tclearAll()
|
|
2632
|
+
\t}
|
|
2633
|
+
|
|
2634
|
+
\tfunction onPong(): void {
|
|
2635
|
+
\t\tif (deadTimer !== null) {
|
|
2636
|
+
\t\t\tresetDeadTimer()
|
|
2637
|
+
\t\t}
|
|
2638
|
+
\t}
|
|
2639
|
+
|
|
2640
|
+
\tfunction onFrame(): void {
|
|
2641
|
+
\t\tif (transport === "sse" && deadTimer !== null) {
|
|
2642
|
+
\t\t\tresetDeadTimer()
|
|
2643
|
+
\t\t}
|
|
2644
|
+
\t}
|
|
2645
|
+
|
|
2646
|
+
\treturn { onFrame, onPong, start, stop }
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
/* ------------------------------------------------------------------ */
|
|
2650
|
+
/* FallbackChain */
|
|
2651
|
+
/* ------------------------------------------------------------------ */
|
|
2652
|
+
|
|
2653
|
+
const DEFAULT_FALLBACK_TIMEOUT = 3000
|
|
2654
|
+
|
|
2655
|
+
export function createFallbackChain(opts: {
|
|
2656
|
+
\ttransports: TransportAdapter[]
|
|
2657
|
+
\ttimeout?: number
|
|
2658
|
+
}): {
|
|
2659
|
+
\tconnect(url: string, transportOpts: TransportOpts): Promise<{ conn: TransportConnection; transport: Transport }>
|
|
2660
|
+
\treadonly provenTransport: Transport | null
|
|
2661
|
+
} {
|
|
2662
|
+
\tconst timeout = opts.timeout ?? DEFAULT_FALLBACK_TIMEOUT
|
|
2663
|
+
\tlet proven: Transport | null = null
|
|
2664
|
+
\tlet provenIndexStore: number | null = null
|
|
2665
|
+
|
|
2666
|
+
\tfunction tryTransport(
|
|
2667
|
+
\t\tadapter: TransportAdapter,
|
|
2668
|
+
\t\turl: string,
|
|
2669
|
+
\t\ttransportOpts: TransportOpts,
|
|
2670
|
+
\t): Promise<TransportConnection> {
|
|
2671
|
+
\t\treturn new Promise((resolve, reject) => {
|
|
2672
|
+
\t\t\tconst conn = adapter.connect(url, transportOpts)
|
|
2673
|
+
|
|
2674
|
+
\t\t\tconst timer = setTimeout(() => {
|
|
2675
|
+
\t\t\t\tconn.close()
|
|
2676
|
+
\t\t\t\treject(new Error("timeout"))
|
|
2677
|
+
\t\t\t}, timeout)
|
|
2678
|
+
|
|
2679
|
+
\t\t\tconst originalOnFrame = conn.onFrame
|
|
2680
|
+
\t\t\tconn.onFrame = (frame: ServerFrame) => {
|
|
2681
|
+
\t\t\t\tif (frame.t === "ready") {
|
|
2682
|
+
\t\t\t\t\tclearTimeout(timer)
|
|
2683
|
+
\t\t\t\t\tconn.onFrame = originalOnFrame
|
|
2684
|
+
\t\t\t\t\tresolve(conn)
|
|
2685
|
+
\t\t\t\t} else {
|
|
2686
|
+
\t\t\t\t\toriginalOnFrame(frame)
|
|
2687
|
+
\t\t\t\t}
|
|
2688
|
+
\t\t\t}
|
|
2689
|
+
|
|
2690
|
+
\t\t\tconn.onError = (err: unknown) => {
|
|
2691
|
+
\t\t\t\tclearTimeout(timer)
|
|
2692
|
+
\t\t\t\tconn.close()
|
|
2693
|
+
\t\t\t\treject(err)
|
|
2694
|
+
\t\t\t}
|
|
2695
|
+
|
|
2696
|
+
\t\t\tconn.onClose = (reason: string) => {
|
|
2697
|
+
\t\t\t\tclearTimeout(timer)
|
|
2698
|
+
\t\t\t\treject(new Error(reason))
|
|
2699
|
+
\t\t\t}
|
|
2700
|
+
\t\t})
|
|
2701
|
+
\t}
|
|
2702
|
+
|
|
2703
|
+
\tasync function connectInternal(
|
|
2704
|
+
\t\turl: string,
|
|
2705
|
+
\t\ttransportOpts: TransportOpts,
|
|
2706
|
+
\t\ttransports: TransportAdapter[],
|
|
2707
|
+
\t): Promise<{ conn: TransportConnection; transport: Transport }> {
|
|
2708
|
+
\t\tif (proven !== null) {
|
|
2709
|
+
\t\t\tif (provenIndexStore !== null && provenIndexStore < transports.length) {
|
|
2710
|
+
\t\t\t\ttry {
|
|
2711
|
+
\t\t\t\t\tconst conn = await tryTransport(transports[provenIndexStore], url, transportOpts)
|
|
2712
|
+
\t\t\t\t\treturn { conn, transport: proven }
|
|
2713
|
+
\t\t\t\t} catch {
|
|
2714
|
+
\t\t\t\t\t/* Proven transport failed — fall through to full chain */
|
|
2715
|
+
\t\t\t\t}
|
|
2716
|
+
\t\t\t}
|
|
2717
|
+
\t\t}
|
|
2718
|
+
|
|
2719
|
+
\t\tfor (let i = 0; i < transports.length; i++) {
|
|
2720
|
+
\t\t\ttry {
|
|
2721
|
+
\t\t\t\tconst conn = await tryTransport(transports[i], url, transportOpts)
|
|
2722
|
+
\t\t\t\tlet transportName: Transport = "longpoll"
|
|
2723
|
+
\t\t\t\tif (i === 0) transportName = "ws"
|
|
2724
|
+
\t\t\t\telse if (i === 1) transportName = "sse"
|
|
2725
|
+
\t\t\t\tproven = transportName
|
|
2726
|
+
\t\t\t\tprovenIndexStore = i
|
|
2727
|
+
\t\t\t\treturn { conn, transport: transportName }
|
|
2728
|
+
\t\t\t} catch {
|
|
2729
|
+
\t\t\t\t/* Try next */
|
|
2730
|
+
\t\t\t}
|
|
2731
|
+
\t\t}
|
|
2732
|
+
|
|
2733
|
+
\t\tthrow new RealtimeConnectError("All transports failed", "all_failed")
|
|
2734
|
+
\t}
|
|
2735
|
+
|
|
2736
|
+
\tfunction connect(
|
|
2737
|
+
\t\turl: string,
|
|
2738
|
+
\t\ttransportOpts: TransportOpts,
|
|
2739
|
+
\t): Promise<{ conn: TransportConnection; transport: Transport }> {
|
|
2740
|
+
\t\tconst transports = opts.transports
|
|
2741
|
+
\t\tif (transports.length === 0) {
|
|
2742
|
+
\t\t\tconst p = Promise.reject(new RealtimeConnectError("No transports configured", "no_transports"))
|
|
2743
|
+
\t\t\tp.catch(() => {})
|
|
2744
|
+
\t\t\treturn p
|
|
2745
|
+
\t\t}
|
|
2746
|
+
\t\tconst p = connectInternal(url, transportOpts, transports)
|
|
2747
|
+
\t\tp.catch(() => {})
|
|
2748
|
+
\t\treturn p
|
|
2749
|
+
\t}
|
|
2750
|
+
|
|
2751
|
+
\treturn {
|
|
2752
|
+
\t\tconnect,
|
|
2753
|
+
\t\tget provenTransport() {
|
|
2754
|
+
\t\t\treturn proven
|
|
2755
|
+
\t\t},
|
|
2756
|
+
\t}
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
/* ------------------------------------------------------------------ */
|
|
2760
|
+
/* ResumableConnection */
|
|
2761
|
+
/* ------------------------------------------------------------------ */
|
|
2762
|
+
|
|
2763
|
+
export type ResumableConnectionOpts = {
|
|
2764
|
+
\turl: string
|
|
2765
|
+
\ttransports: TransportAdapter[]
|
|
2766
|
+
\ttoken?: () => string | Promise<string>
|
|
2767
|
+
\t/** 401 on a request: return a fresh token to retry it once (null = give up). \`rejectedToken\` is the token that failed, so concurrent 401s can share one refresh. */\n\tonAuthExpired?: (ctx: { rejectedToken: string | null }) => Promise<string | null>
|
|
2768
|
+
\tonReconnecting?: (attempt: number, transport: Transport) => void
|
|
2769
|
+
\tonReconnected?: () => void
|
|
2770
|
+
\tsignal?: AbortSignal
|
|
2771
|
+
\tkeepaliveInterval?: number
|
|
2772
|
+
\treconnectDelayMs?: number
|
|
2773
|
+
\tmaxReconnectAttempts?: number
|
|
2774
|
+
\tfallbackTimeout?: number
|
|
2775
|
+
\theaders?: Record<string, string>
|
|
2776
|
+
\treconnectToken?: string
|
|
2777
|
+
\tlastId?: number
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
type QueueEntry =
|
|
2781
|
+
\t| { type: "value"; value: unknown }
|
|
2782
|
+
\t| { type: "done" }
|
|
2783
|
+
\t| { type: "error"; error: unknown }
|
|
2784
|
+
|
|
2785
|
+
export function createResumableConnection(opts: ResumableConnectionOpts): {
|
|
2786
|
+
\treadonly state: ConnectionState
|
|
2787
|
+
\treadonly provenTransport: Transport | null
|
|
2788
|
+
\tsend(data: unknown): void
|
|
2789
|
+
\tclose(reason?: string): void
|
|
2790
|
+
\t[Symbol.asyncIterator](): AsyncIterableIterator<unknown>
|
|
2791
|
+
} {
|
|
2792
|
+
\tconst reconnectDelayMs = opts.reconnectDelayMs ?? 1000
|
|
2793
|
+
\tconst maxReconnectAttempts = opts.maxReconnectAttempts ?? 5
|
|
2794
|
+
\tconst fallbackTimeout = opts.fallbackTimeout ?? 3000
|
|
2795
|
+
\tlet state: ConnectionState = "idle"
|
|
2796
|
+
\tlet currentConn: TransportConnection | null = null
|
|
2797
|
+
\tlet reconnectAttempts = 0
|
|
2798
|
+
\tlet provenIndex: number | null = null
|
|
2799
|
+
\tlet lastId: number | undefined = opts.lastId
|
|
2800
|
+
\tlet reconnectToken: string | undefined = opts.reconnectToken
|
|
2801
|
+
\tlet started = false
|
|
2802
|
+
\tlet closed = false
|
|
2803
|
+
|
|
2804
|
+
\tfunction indexToTransport(i: number): Transport {
|
|
2805
|
+
\t\tif (i === 0) return "ws"
|
|
2806
|
+
\t\tif (i === 1) return "sse"
|
|
2807
|
+
\t\treturn "longpoll"
|
|
2808
|
+
\t}
|
|
2809
|
+
|
|
2810
|
+
\tif (opts.signal?.aborted) {
|
|
2811
|
+
\t\tstate = "closed"
|
|
2812
|
+
\t\tclosed = true
|
|
2813
|
+
\t}
|
|
2814
|
+
|
|
2815
|
+
\tconst queue: QueueEntry[] = []
|
|
2816
|
+
\tlet pending: {
|
|
2817
|
+
\t\tresolve: (result: IteratorResult<unknown>) => void
|
|
2818
|
+
\t\treject: (err: unknown) => void
|
|
2819
|
+
\t} | null = null
|
|
2820
|
+
|
|
2821
|
+
\tfunction enqueue(entry: QueueEntry): void {
|
|
2822
|
+
\t\tif (pending) {
|
|
2823
|
+
\t\t\tconst { resolve, reject } = pending
|
|
2824
|
+
\t\t\tpending = null
|
|
2825
|
+
\t\t\tif (entry.type === "value") {
|
|
2826
|
+
\t\t\t\tresolve({ done: false, value: entry.value })
|
|
2827
|
+
\t\t\t} else if (entry.type === "done") {
|
|
2828
|
+
\t\t\t\tresolve({ done: true, value: undefined })
|
|
2829
|
+
\t\t\t} else {
|
|
2830
|
+
\t\t\t\treject(entry.error)
|
|
2831
|
+
\t\t\t}
|
|
2832
|
+
\t\t} else {
|
|
2833
|
+
\t\t\tqueue.push(entry)
|
|
2834
|
+
\t\t}
|
|
2835
|
+
\t}
|
|
2836
|
+
|
|
2837
|
+
\tfunction detachConn(): void {
|
|
2838
|
+
\t\tif (!currentConn) return
|
|
2839
|
+
\t\tconst conn = currentConn
|
|
2840
|
+
\t\tcurrentConn = null
|
|
2841
|
+
\t\tconn.onFrame = () => {}
|
|
2842
|
+
\t\tconn.onError = () => {}
|
|
2843
|
+
\t\tconn.onClose = () => {}
|
|
2844
|
+
\t\ttry {
|
|
2845
|
+
\t\t\tconn.close()
|
|
2846
|
+
\t\t} catch {
|
|
2847
|
+
\t\t\t/* swallow — adapter close() is best-effort */
|
|
2848
|
+
\t\t}
|
|
2849
|
+
\t}
|
|
2850
|
+
|
|
2851
|
+
\tfunction handleClose(): void {
|
|
2852
|
+
\t\tif (state === "closed") return
|
|
2853
|
+
\t\tstate = "closed"
|
|
2854
|
+
\t\tclosed = true
|
|
2855
|
+
\t\tdetachConn()
|
|
2856
|
+
\t\tenqueue({ type: "done" })
|
|
2857
|
+
\t}
|
|
2858
|
+
|
|
2859
|
+
\tfunction handleAbort(): void {
|
|
2860
|
+
\t\tif (state === "closed") return
|
|
2861
|
+
\t\tstate = "closed"
|
|
2862
|
+
\t\tclosed = true
|
|
2863
|
+
\t\tdetachConn()
|
|
2864
|
+
\t\tenqueue({ error: new RealtimeAbortError("Connection aborted", "aborted"), type: "error" })
|
|
2865
|
+
\t}
|
|
2866
|
+
|
|
2867
|
+
\tif (opts.signal && !opts.signal.aborted) {
|
|
2868
|
+
\t\topts.signal.addEventListener("abort", () => {
|
|
2869
|
+
\t\t\thandleAbort()
|
|
2870
|
+
\t\t}, { once: true })
|
|
2871
|
+
\t}
|
|
2872
|
+
|
|
2873
|
+
\t/* tryAdapterAt dials a single adapter and wires its onFrame/onError/onClose
|
|
2874
|
+
\t * straight into the resumable queue + reconnect loop the moment the adapter
|
|
2875
|
+
\t * returns. No post-ready re-wire hop — frames that arrive between ready and
|
|
2876
|
+
\t * the caller's await-continuation can't be lost. Resolves when ready frame
|
|
2877
|
+
\t * seen, rejects on error/close/timeout. */
|
|
2878
|
+
\tfunction tryAdapterAt(index: number): Promise<TransportConnection> {
|
|
2879
|
+
\t\treturn new Promise((resolve, reject) => {
|
|
2880
|
+
\t\t\tconst adapter = opts.transports[index]
|
|
2881
|
+
\t\t\tif (!adapter) {
|
|
2882
|
+
\t\t\t\treject(new RealtimeConnectError("adapter index out of range", "no_adapter"))
|
|
2883
|
+
\t\t\t\treturn
|
|
2884
|
+
\t\t\t}
|
|
2885
|
+
\t\t\tconst transportOpts: TransportOpts = {
|
|
2886
|
+
\t\t\t\theaders: opts.headers,
|
|
2887
|
+
\t\t\t\tlastId,
|
|
2888
|
+
\t\t\t\treconnectToken,
|
|
2889
|
+
\t\t\t\tsignal: opts.signal,
|
|
2890
|
+
\t\t\t}
|
|
2891
|
+
\t\t\tconst conn = adapter.connect(opts.url, transportOpts)
|
|
2892
|
+
\t\t\tlet ready = false
|
|
2893
|
+
\t\t\tconst timer = setTimeout(() => {
|
|
2894
|
+
\t\t\t\tif (ready) return
|
|
2895
|
+
\t\t\t\ttry { conn.close() } catch { /* ignore */ }
|
|
2896
|
+
\t\t\t\treject(new RealtimeConnectError("transport open timeout", "timeout"))
|
|
2897
|
+
\t\t\t}, fallbackTimeout)
|
|
2898
|
+
\t\t\tconn.onFrame = (frame: ServerFrame) => {
|
|
2899
|
+
\t\t\t\tif (!ready && frame.t === "ready") {
|
|
2900
|
+
\t\t\t\t\tready = true
|
|
2901
|
+
\t\t\t\t\tclearTimeout(timer)
|
|
2902
|
+
\t\t\t\t\treconnectToken = frame.reconnectToken
|
|
2903
|
+
\t\t\t\t\tresolve(conn)
|
|
2904
|
+
\t\t\t\t\treturn
|
|
2905
|
+
\t\t\t\t}
|
|
2906
|
+
\t\t\t\tif (frame.t === "msg") {
|
|
2907
|
+
\t\t\t\t\tlastId = frame.id
|
|
2908
|
+
\t\t\t\t\tenqueue({ type: "value", value: frame.data })
|
|
2909
|
+
\t\t\t\t\treturn
|
|
2910
|
+
\t\t\t\t}
|
|
2911
|
+
\t\t\t\tif (frame.t === "ready") {
|
|
2912
|
+
\t\t\t\t\treconnectToken = frame.reconnectToken
|
|
2913
|
+
\t\t\t\t\treturn
|
|
2914
|
+
\t\t\t\t}
|
|
2915
|
+
\t\t\t\tif (frame.t === "bye") {
|
|
2916
|
+
\t\t\t\t\tif (ready) {
|
|
2917
|
+
\t\t\t\t\t\tdetachConn()
|
|
2918
|
+
\t\t\t\t\t\tscheduleReconnect(new RealtimeError("server closed connection: " + frame.reason, "bye"))
|
|
2919
|
+
\t\t\t\t\t}
|
|
2920
|
+
\t\t\t\t\treturn
|
|
2921
|
+
\t\t\t\t}
|
|
2922
|
+
\t\t\t}
|
|
2923
|
+
\t\t\tconn.onError = (err: unknown) => {
|
|
2924
|
+
\t\t\t\tclearTimeout(timer)
|
|
2925
|
+
\t\t\t\tif (!ready) {
|
|
2926
|
+
\t\t\t\t\ttry { conn.close() } catch { /* ignore */ }
|
|
2927
|
+
\t\t\t\t\treject(err)
|
|
2928
|
+
\t\t\t\t\treturn
|
|
2929
|
+
\t\t\t\t}
|
|
2930
|
+
\t\t\t\tdetachConn()
|
|
2931
|
+
\t\t\t\tscheduleReconnect(err)
|
|
2932
|
+
\t\t\t}
|
|
2933
|
+
\t\t\tconn.onClose = (reason: string) => {
|
|
2934
|
+
\t\t\t\tclearTimeout(timer)
|
|
2935
|
+
\t\t\t\tif (!ready) {
|
|
2936
|
+
\t\t\t\t\treject(new RealtimeConnectError("transport closed before ready: " + reason, "closed_early"))
|
|
2937
|
+
\t\t\t\t\treturn
|
|
2938
|
+
\t\t\t\t}
|
|
2939
|
+
\t\t\t\tdetachConn()
|
|
2940
|
+
\t\t\t\tscheduleReconnect(new RealtimeError("transport closed: " + reason, "closed"))
|
|
2941
|
+
\t\t\t}
|
|
2942
|
+
\t\t})
|
|
2943
|
+
\t}
|
|
2944
|
+
|
|
2945
|
+
\tasync function openChain(): Promise<void> {
|
|
2946
|
+
\t\tif (opts.transports.length === 0) {
|
|
2947
|
+
\t\t\tthrow new RealtimeConnectError("No transports configured", "no_transports")
|
|
2948
|
+
\t\t}
|
|
2949
|
+
\t\t/* Try proven first if memoized */
|
|
2950
|
+
\t\tif (provenIndex !== null && provenIndex < opts.transports.length) {
|
|
2951
|
+
\t\t\ttry {
|
|
2952
|
+
\t\t\t\tconst conn = await tryAdapterAt(provenIndex)
|
|
2953
|
+
\t\t\t\tcurrentConn = conn
|
|
2954
|
+
\t\t\t\treturn
|
|
2955
|
+
\t\t\t} catch {
|
|
2956
|
+
\t\t\t\t/* fall through to full chain */
|
|
2957
|
+
\t\t\t}
|
|
2958
|
+
\t\t}
|
|
2959
|
+
\t\tlet lastErr: unknown = null
|
|
2960
|
+
\t\tfor (let i = 0; i < opts.transports.length; i++) {
|
|
2961
|
+
\t\t\ttry {
|
|
2962
|
+
\t\t\t\tconst conn = await tryAdapterAt(i)
|
|
2963
|
+
\t\t\t\tprovenIndex = i
|
|
2964
|
+
\t\t\t\tcurrentConn = conn
|
|
2965
|
+
\t\t\t\treturn
|
|
2966
|
+
\t\t\t} catch (err) {
|
|
2967
|
+
\t\t\t\tlastErr = err
|
|
2968
|
+
\t\t\t}
|
|
2969
|
+
\t\t}
|
|
2970
|
+
\t\tthrow lastErr ?? new RealtimeConnectError("All transports failed", "all_failed")
|
|
2971
|
+
\t}
|
|
2972
|
+
|
|
2973
|
+
\tfunction scheduleReconnect(_err: unknown): void {
|
|
2974
|
+
\t\tif (closed) return
|
|
2975
|
+
\t\treconnectAttempts += 1
|
|
2976
|
+
\t\tif (maxReconnectAttempts > 0 && reconnectAttempts > maxReconnectAttempts) {
|
|
2977
|
+
\t\t\tstate = "closed"
|
|
2978
|
+
\t\t\tclosed = true
|
|
2979
|
+
\t\t\tenqueue({ type: "done" })
|
|
2980
|
+
\t\t\treturn
|
|
2981
|
+
\t\t}
|
|
2982
|
+
\t\tstate = "reconnecting"
|
|
2983
|
+
\t\t/* invalidate proven — a drop means that transport isn't healthy */
|
|
2984
|
+
\t\tprovenIndex = null
|
|
2985
|
+
\t\tif (opts.onReconnecting) {
|
|
2986
|
+
\t\t\ttry {
|
|
2987
|
+
\t\t\t\topts.onReconnecting(reconnectAttempts, indexToTransport(provenIndex ?? 0))
|
|
2988
|
+
\t\t\t} catch {
|
|
2989
|
+
\t\t\t\t/* user callback must not break the loop */
|
|
2990
|
+
\t\t\t}
|
|
2991
|
+
\t\t}
|
|
2992
|
+
\t\tsetTimeout(() => {
|
|
2993
|
+
\t\t\tif (closed) return
|
|
2994
|
+
\t\t\tvoid connectOnce()
|
|
2995
|
+
\t\t}, reconnectDelayMs)
|
|
2996
|
+
\t}
|
|
2997
|
+
|
|
2998
|
+
\tasync function connectOnce(): Promise<void> {
|
|
2999
|
+
\t\tif (closed) return
|
|
3000
|
+
\t\tif (state === "idle" || state === "closed") state = "connecting"
|
|
3001
|
+
\t\ttry {
|
|
3002
|
+
\t\t\tawait openChain()
|
|
3003
|
+
\t\t\tif (closed) {
|
|
3004
|
+
\t\t\t\tdetachConn()
|
|
3005
|
+
\t\t\t\treturn
|
|
3006
|
+
\t\t\t}
|
|
3007
|
+
\t\t\tstate = "connected"
|
|
3008
|
+
\t\t\treconnectAttempts = 0
|
|
3009
|
+
\t\t\tif (opts.onReconnected) {
|
|
3010
|
+
\t\t\t\ttry { opts.onReconnected() } catch { /* user cb */ }
|
|
3011
|
+
\t\t\t}
|
|
3012
|
+
\t\t} catch (err) {
|
|
3013
|
+
\t\t\tscheduleReconnect(err)
|
|
3014
|
+
\t\t}
|
|
3015
|
+
\t}
|
|
3016
|
+
|
|
3017
|
+
\tfunction startConnection(): void {
|
|
3018
|
+
\t\tif (started || closed) return
|
|
3019
|
+
\t\tstarted = true
|
|
3020
|
+
\t\tstate = "connecting"
|
|
3021
|
+
\t\tvoid connectOnce()
|
|
3022
|
+
\t}
|
|
3023
|
+
|
|
3024
|
+
\tfunction send(data: unknown): void {
|
|
3025
|
+
\t\tif (closed || !currentConn) return
|
|
3026
|
+
\t\tcurrentConn.send(JSON.stringify(data))
|
|
3027
|
+
\t}
|
|
3028
|
+
|
|
3029
|
+
\tfunction close(_reason?: string): void {
|
|
3030
|
+
\t\thandleClose()
|
|
3031
|
+
\t}
|
|
3032
|
+
|
|
3033
|
+
\tfunction next(): Promise<IteratorResult<unknown>> {
|
|
3034
|
+
\t\tif (!started && !closed) {
|
|
3035
|
+
\t\t\tstartConnection()
|
|
3036
|
+
\t\t}
|
|
3037
|
+
\t\tif (queue.length > 0) {
|
|
3038
|
+
\t\t\tconst entry = queue.shift()
|
|
3039
|
+
\t\t\tif (!entry || entry.type === "done") {
|
|
3040
|
+
\t\t\t\treturn Promise.resolve({ done: true, value: undefined })
|
|
3041
|
+
\t\t\t}
|
|
3042
|
+
\t\t\tif (entry.type === "value") {
|
|
3043
|
+
\t\t\t\treturn Promise.resolve({ done: false, value: entry.value })
|
|
3044
|
+
\t\t\t}
|
|
3045
|
+
\t\t\treturn Promise.reject(entry.error)
|
|
3046
|
+
\t\t}
|
|
3047
|
+
\t\tif (closed) {
|
|
3048
|
+
\t\t\treturn Promise.resolve({ done: true, value: undefined })
|
|
3049
|
+
\t\t}
|
|
3050
|
+
\t\tconst promise = new Promise<IteratorResult<unknown>>((resolve, reject) => {
|
|
3051
|
+
\t\t\tpending = { reject, resolve }
|
|
3052
|
+
\t\t})
|
|
3053
|
+
\t\tpromise.catch(() => {})
|
|
3054
|
+
\t\treturn promise
|
|
3055
|
+
\t}
|
|
3056
|
+
|
|
3057
|
+
\tfunction asyncIterator(): AsyncIterableIterator<unknown> {
|
|
3058
|
+
\t\treturn {
|
|
3059
|
+
\t\t\tnext,
|
|
3060
|
+
\t\t\t[Symbol.asyncIterator]() {
|
|
3061
|
+
\t\t\t\treturn this
|
|
3062
|
+
\t\t\t},
|
|
3063
|
+
\t\t}
|
|
3064
|
+
\t}
|
|
3065
|
+
|
|
3066
|
+
\treturn {
|
|
3067
|
+
\t\t[Symbol.asyncIterator]: asyncIterator,
|
|
3068
|
+
\t\tclose,
|
|
3069
|
+
\t\tget provenTransport() {
|
|
3070
|
+
\t\t\treturn provenIndex === null ? null : indexToTransport(provenIndex)
|
|
3071
|
+
\t\t},
|
|
3072
|
+
\t\tsend,
|
|
3073
|
+
\t\tget state() {
|
|
3074
|
+
\t\t\treturn state
|
|
3075
|
+
\t\t},
|
|
3076
|
+
\t}
|
|
3077
|
+
}
|
|
3078
|
+
`;
|
|
3079
|
+
}
|
|
3080
|
+
function sdkClientHeader(n, stem) {
|
|
3081
|
+
return `import type { ${n}Config } from "./${stem}.types.gen"
|
|
3082
|
+
import { serviceMap } from "./${stem}.map.gen"
|
|
3083
|
+
`;
|
|
3084
|
+
}
|
|
3085
|
+
function sdkClientTypes() {
|
|
3086
|
+
return `
|
|
3087
|
+
type _SSEEvent = { data: string; event?: string; id?: string; retry?: number }
|
|
3088
|
+
|
|
3089
|
+
type _TypedWebSocket = {
|
|
3090
|
+
\tclose(code?: number, reason?: string): void
|
|
3091
|
+
\toff(event: "close" | "error" | "message" | "open", handler: (...args: never[]) => void): void
|
|
3092
|
+
\ton(event: "close", handler: (code: number, reason: string) => void): void
|
|
3093
|
+
\ton(event: "error", handler: (error: unknown) => void): void
|
|
3094
|
+
\ton(event: "message", handler: (data: string) => void): void
|
|
3095
|
+
\ton(event: "open", handler: () => void): void
|
|
3096
|
+
\treadonly readyState: number
|
|
3097
|
+
\tsend(data: ArrayBuffer | ArrayBufferView | object | string): void
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
type _ServiceEntry = {
|
|
3101
|
+
\tidempotent?: boolean
|
|
3102
|
+
\tinvalidate?: readonly string[]
|
|
3103
|
+
\tmethod: string
|
|
3104
|
+
\tparams?: readonly string[]
|
|
3105
|
+
\tpath: string
|
|
3106
|
+
\tsse?: boolean
|
|
3107
|
+
\twildcard?: string
|
|
3108
|
+
\tws?: boolean
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
type _ServiceMapNode = _ServiceEntry | { [key: string]: _ServiceMapNode }
|
|
3112
|
+
type _ServiceMap = { [key: string]: _ServiceMapNode }
|
|
3113
|
+
|
|
3114
|
+
type _RequestOptions = {
|
|
3115
|
+
\tbody?: ReadableStream<Uint8Array> | Blob | ArrayBuffer | Uint8Array
|
|
3116
|
+
\tcookies?: Record<string, string>
|
|
3117
|
+
\tform?: Record<string, unknown>
|
|
3118
|
+
\theaders?: Record<string, string>
|
|
3119
|
+
\tidempotencyKey?: string
|
|
3120
|
+
\tjson?: unknown
|
|
3121
|
+
\tlastEventId?: string
|
|
3122
|
+
\tparams?: Record<string, string>
|
|
3123
|
+
\tprotocols?: string | string[]
|
|
3124
|
+
\treconnectToken?: string
|
|
3125
|
+
\tsearch?: Record<string, unknown>
|
|
3126
|
+
\tsignal?: AbortSignal
|
|
3127
|
+
\ttimeout?: number
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
type _RequestMeta = {
|
|
3131
|
+
\tinvalidatedBy: string[]
|
|
3132
|
+
\tisStale: boolean
|
|
3133
|
+
\tselector: string
|
|
3134
|
+
\tseqSnapshot: number
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
type _StaleEntry = { by: string[]; refreshed?: Set<string>; seq: number; until: number }
|
|
3138
|
+
|
|
3139
|
+
/* a path parameter value that would change the request's path once a URL parser normalizes it */
|
|
3140
|
+
class _PathParamError extends Error {
|
|
3141
|
+
\tconstructor(message: string) {
|
|
3142
|
+
\t\tsuper(message)
|
|
3143
|
+
\t\tthis.name = "PathParamError"
|
|
3144
|
+
\t}
|
|
3145
|
+
}
|
|
3146
|
+
`;
|
|
3147
|
+
}
|
|
3148
|
+
function sdkClientClientError() {
|
|
3149
|
+
const subclassDecls = STATUS_ERROR_CLASSES.map(({ name }) => `class _${name} extends _ClientError { constructor(init: ConstructorParameters<typeof _ClientError>[0]) { super(init); this.name = "${name}" } }`).join("\n");
|
|
3150
|
+
const mapEntries = STATUS_ERROR_CLASSES.map(({ name, status }) => `\t${status}: _${name},`).join("\n");
|
|
3151
|
+
return `
|
|
3152
|
+
class _ClientError extends Error {
|
|
3153
|
+
\treadonly body: unknown
|
|
3154
|
+
\treadonly data: unknown
|
|
3155
|
+
\treadonly response: Response
|
|
3156
|
+
\treadonly status: number
|
|
3157
|
+
|
|
3158
|
+
\tconstructor(init: { body: unknown; data: unknown; message: string; response: Response; status: number }) {
|
|
3159
|
+
\t\tsuper(init.message)
|
|
3160
|
+
\t\t;(Error as unknown as { captureStackTrace?: (t: object, c: Function) => void }).captureStackTrace?.(this, _ClientError)
|
|
3161
|
+
\t\tthis.name = "ClientError"
|
|
3162
|
+
\t\tthis.body = init.body
|
|
3163
|
+
\t\tthis.data = init.data
|
|
3164
|
+
\t\tthis.response = init.response
|
|
3165
|
+
\t\tthis.status = init.status
|
|
3166
|
+
\t}
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
${subclassDecls}
|
|
3170
|
+
|
|
3171
|
+
const _STATUS_ERROR_MAP: Record<number, new (init: ConstructorParameters<typeof _ClientError>[0]) => _ClientError> = {
|
|
3172
|
+
${mapEntries}
|
|
3173
|
+
}
|
|
3174
|
+
`;
|
|
3175
|
+
}
|
|
3176
|
+
function sdkClientClass(n) {
|
|
3177
|
+
return `
|
|
3178
|
+
export class ${n}<TThrow extends boolean = false> {
|
|
3179
|
+
\tstate: Record<string, unknown>
|
|
3180
|
+
\t#config: ${n}Config<TThrow>
|
|
3181
|
+
\t#fetchFn: typeof fetch
|
|
3182
|
+
\t#resourceCache = new Map<string, Record<string, unknown>>()
|
|
3183
|
+
\t#searchSerializer: (query: Record<string, unknown>) => URLSearchParams
|
|
3184
|
+
\t#staleTime: number
|
|
3185
|
+
\t#staleUntil: Map<string, _StaleEntry> | null
|
|
3186
|
+
\t#stalePatterns = new Map<string, Map<string, _StaleEntry>>()
|
|
3187
|
+
\t#patternRegexCache = new Map<string, RegExp>()
|
|
3188
|
+
\t#invalidationSeq = 0
|
|
3189
|
+
\t#staleMaxEntries: number
|
|
3190
|
+
\t#maxSourcesPerTarget: number
|
|
3191
|
+
\t#maxErrorMessageChars: number
|
|
3192
|
+
\t#sseMaxBufferChars: number
|
|
3193
|
+
\t#disposeCtrl = new AbortController()
|
|
3194
|
+
\t#disposed = false
|
|
3195
|
+
\t#refreshing: Promise<string | null> | null = null
|
|
3196
|
+
\t#token: string | null = null
|
|
3197
|
+
`;
|
|
3198
|
+
}
|
|
3199
|
+
function sdkClientConstructor(n) {
|
|
3200
|
+
return `
|
|
3201
|
+
\tconstructor(config: ${n}Config<TThrow>) {
|
|
3202
|
+
\t\tconst ownState = config.state ?? {}
|
|
3203
|
+
\t\tthis.state = ownState
|
|
3204
|
+
\t\tthis.#config = { ...config, state: ownState }
|
|
3205
|
+
\t\tthis.#fetchFn = config.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : globalThis.fetch)
|
|
3206
|
+
\t\tthis.#searchSerializer = config.buildSearchParams ?? ((q: Record<string, unknown>) => this.#serializeSearch(q))
|
|
3207
|
+
\t\tthis.#staleTime = config.invalidation?.staleTime ?? 0
|
|
3208
|
+
\t\tthis.#staleUntil = this.#staleTime > 0 ? new Map<string, _StaleEntry>() : null
|
|
3209
|
+
\t\tthis.#staleMaxEntries = config.invalidation?.staleMaxEntries ?? 1000
|
|
3210
|
+
\t\tthis.#maxSourcesPerTarget = Math.max(config.invalidation?.maxSourcesPerTarget ?? 16, 1)
|
|
3211
|
+
\t\tthis.#maxErrorMessageChars = config.maxErrorMessageChars ?? 512
|
|
3212
|
+
\t\tthis.#sseMaxBufferChars = config.sseMaxBufferChars ?? 1024 * 1024
|
|
3213
|
+
`;
|
|
3214
|
+
}
|
|
3215
|
+
function sdkClientProxy(n) {
|
|
3216
|
+
return `
|
|
3217
|
+
\t\tconst self = this
|
|
3218
|
+
\t\tconst callable = (entry: _ServiceEntry, name: string): ((input?: Record<string, unknown>) => unknown) => {
|
|
3219
|
+
\t\t\tconst entryPath = self.#toColonParams(entry.path, entry.wildcard)
|
|
3220
|
+
\t\t\tlet fn: (input?: Record<string, unknown>) => unknown
|
|
3221
|
+
\t\t\tif (entry.ws) {
|
|
3222
|
+
\t\t\t\tfn = (input?: Record<string, unknown>) => self.#connectWS(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3223
|
+
\t\t\t} else if (entry.sse) {
|
|
3224
|
+
\t\t\t\tfn = (input?: Record<string, unknown>) => self.#requestSSE(entry, entryPath, (input ?? {}) as _RequestOptions)
|
|
3225
|
+
\t\t\t} else {
|
|
3226
|
+
\t\t\t\tfn = (input?: Record<string, unknown>) => self.#request(entry, input ?? {})
|
|
3227
|
+
\t\t\t}
|
|
3228
|
+
\t\t\tObject.defineProperty(fn, "name", { value: name })
|
|
3229
|
+
\t\t\treturn fn
|
|
3230
|
+
\t\t}
|
|
3231
|
+
\t\tconst isEntry = (node: unknown): node is _ServiceEntry =>
|
|
3232
|
+
\t\t\ttypeof node === "object" && node !== null && Object.hasOwn(node, "method") && typeof (node as Record<string, unknown>)["method"] === "string"
|
|
3233
|
+
\t\t/* only the map's own keys are operations — never toString, constructor or then */
|
|
3234
|
+
\t\tconst childOf = (node: _ServiceMapNode, key: string): _ServiceMapNode | undefined =>
|
|
3235
|
+
\t\t\tObject.hasOwn(node, key) ? (node as Record<string, _ServiceMapNode>)[key] : undefined
|
|
3236
|
+
|
|
3237
|
+
\t\tfunction makeNodeProxy(node: _ServiceMapNode, path: string[]): object {
|
|
3238
|
+
\t\t\tconst actionCache = new Map<string, (input?: Record<string, unknown>) => unknown>()
|
|
3239
|
+
\t\t\tconst childCache = new Map<string, object>()
|
|
3240
|
+
\t\t\treturn new Proxy({} as Record<string, unknown>, {
|
|
3241
|
+
\t\t\t\tget: (target, key: string | symbol) => {
|
|
3242
|
+
\t\t\t\t\tif (typeof key === "symbol") return Reflect.get(target, key)
|
|
3243
|
+
\t\t\t\t\tconst child = childOf(node, key)
|
|
3244
|
+
\t\t\t\t\tif (child === undefined) return Reflect.get(target, key)
|
|
3245
|
+
|
|
3246
|
+
\t\t\t\t\tif (isEntry(child)) {
|
|
3247
|
+
\t\t\t\t\t\tconst cached = actionCache.get(key)
|
|
3248
|
+
\t\t\t\t\t\tif (cached) return cached
|
|
3249
|
+
\t\t\t\t\t\tconst fn = callable(child, [...path, key].join("."))
|
|
3250
|
+
\t\t\t\t\t\tactionCache.set(key, fn)
|
|
3251
|
+
\t\t\t\t\t\treturn fn
|
|
3252
|
+
\t\t\t\t\t}
|
|
3253
|
+
|
|
3254
|
+
\t\t\t\t\tconst cachedChild = childCache.get(key)
|
|
3255
|
+
\t\t\t\t\tif (cachedChild) return cachedChild
|
|
3256
|
+
\t\t\t\t\tconst childProxy = makeNodeProxy(child, [...path, key])
|
|
3257
|
+
\t\t\t\t\tchildCache.set(key, childProxy)
|
|
3258
|
+
\t\t\t\t\treturn childProxy
|
|
3259
|
+
\t\t\t\t},
|
|
3260
|
+
\t\t\t})
|
|
3261
|
+
\t\t}
|
|
3262
|
+
|
|
3263
|
+
\t\treturn new Proxy(this, {
|
|
3264
|
+
\t\t\tget: (target, key: string | symbol) => {
|
|
3265
|
+
\t\t\t\t/* the client's own members (state, dispose, toString, ...) always win */
|
|
3266
|
+
\t\t\t\tif (typeof key === "symbol" || key in target) return Reflect.get(target, key)
|
|
3267
|
+
|
|
3268
|
+
\t\t\t\tconst cached = target.#resourceCache.get(key)
|
|
3269
|
+
\t\t\t\tif (cached) return cached
|
|
3270
|
+
|
|
3271
|
+
\t\t\t\tconst node = childOf(serviceMap as _ServiceMap, key)
|
|
3272
|
+
\t\t\t\tif (node === undefined) return undefined
|
|
3273
|
+
|
|
3274
|
+
\t\t\t\t/* root-level leaf (single-segment operationId) */
|
|
3275
|
+
\t\t\t\tif (isEntry(node)) {
|
|
3276
|
+
\t\t\t\t\tconst fn = callable(node, key)
|
|
3277
|
+
\t\t\t\t\ttarget.#resourceCache.set(key, fn as unknown as Record<string, unknown>)
|
|
3278
|
+
\t\t\t\t\treturn fn
|
|
3279
|
+
\t\t\t\t}
|
|
3280
|
+
|
|
3281
|
+
\t\t\t\t/* namespace node — check for single _call promotion */
|
|
3282
|
+
\t\t\t\tconst nodeKeys = Object.keys(node)
|
|
3283
|
+
\t\t\t\tconst only = childOf(node, "_call")
|
|
3284
|
+
\t\t\t\tif (nodeKeys.length === 1 && only !== undefined && isEntry(only)) {
|
|
3285
|
+
\t\t\t\t\tconst fn = callable(only, key)
|
|
3286
|
+
\t\t\t\t\ttarget.#resourceCache.set(key, fn as unknown as Record<string, unknown>)
|
|
3287
|
+
\t\t\t\t\treturn fn
|
|
3288
|
+
\t\t\t\t}
|
|
3289
|
+
|
|
3290
|
+
\t\t\t\tconst proxy = makeNodeProxy(node, [key])
|
|
3291
|
+
\t\t\t\ttarget.#resourceCache.set(key, proxy as Record<string, unknown>)
|
|
3292
|
+
\t\t\t\treturn proxy
|
|
3293
|
+
\t\t\t},
|
|
3294
|
+
\t\t}) as ${n}<TThrow>
|
|
3295
|
+
\t}
|
|
3296
|
+
`;
|
|
3297
|
+
}
|
|
3298
|
+
function sdkClientInterpolatePath() {
|
|
3299
|
+
return `
|
|
3300
|
+
\t/* Keep in sync with client/path.ts — :name (whole segment), *name (rest), {name} (inside a segment). */
|
|
3301
|
+
\t#encodeSegmentValue(key: string, value: string): string {
|
|
3302
|
+
\t\tif (value === "" || value === "." || value === "..") {
|
|
3303
|
+
\t\t\tthrow new _PathParamError(\`Invalid path param \${JSON.stringify(key)}: \${JSON.stringify(value)} is not a path segment\`)
|
|
3304
|
+
\t\t}
|
|
3305
|
+
\t\treturn encodeURIComponent(value)
|
|
3306
|
+
\t}
|
|
3307
|
+
|
|
3308
|
+
\t#encodeWildcardValue(key: string, value: string): string {
|
|
3309
|
+
\t\tif (value === "") return ""
|
|
3310
|
+
\t\treturn value.split("/").map((part) => this.#encodeSegmentValue(key, part)).join("/")
|
|
3311
|
+
\t}
|
|
3312
|
+
|
|
3313
|
+
\t#paramValue(params: Record<string, string> | undefined, key: string): string | undefined {
|
|
3314
|
+
\t\tif (!params || !Object.hasOwn(params, key)) return undefined
|
|
3315
|
+
\t\tconst raw = params[key] as unknown
|
|
3316
|
+
\t\tif (raw === undefined || raw === null) return undefined
|
|
3317
|
+
\t\treturn typeof raw === "string" ? raw : String(raw)
|
|
3318
|
+
\t}
|
|
3319
|
+
|
|
3320
|
+
\t#interpolatePath(path: string, params: Record<string, string> | undefined, partial = false): string {
|
|
3321
|
+
\t\tconst segments = path.split("/")
|
|
3322
|
+
\t\tconst out: string[] = []
|
|
3323
|
+
\t\tfor (let i = 0; i < segments.length; i++) {
|
|
3324
|
+
\t\t\tconst seg = segments[i] ?? ""
|
|
3325
|
+
\t\t\tif (seg.length > 1 && seg.charCodeAt(0) === 58) {
|
|
3326
|
+
\t\t\t\tconst optional = seg.endsWith("?")
|
|
3327
|
+
\t\t\t\tconst name = optional ? seg.slice(1, -1) : seg.slice(1)
|
|
3328
|
+
\t\t\t\tconst value = this.#paramValue(params, name)
|
|
3329
|
+
\t\t\t\tif (value === undefined) {
|
|
3330
|
+
\t\t\t\t\tif (partial) { out.push(seg); continue }
|
|
3331
|
+
\t\t\t\t\tif (optional) continue
|
|
3332
|
+
\t\t\t\t\tthrow new _PathParamError(\`Missing path param: \${name}\`)
|
|
3333
|
+
\t\t\t\t}
|
|
3334
|
+
\t\t\t\tout.push(this.#encodeSegmentValue(name, value))
|
|
3335
|
+
\t\t\t\tcontinue
|
|
3336
|
+
\t\t\t}
|
|
3337
|
+
\t\t\tif (seg.charCodeAt(0) === 42 && i === segments.length - 1) {
|
|
3338
|
+
\t\t\t\tconst name = seg.length > 1 ? seg.slice(1) : "*"
|
|
3339
|
+
\t\t\t\tconst value = this.#paramValue(params, name)
|
|
3340
|
+
\t\t\t\tif (value === undefined) {
|
|
3341
|
+
\t\t\t\t\tif (partial) { out.push(seg); continue }
|
|
3342
|
+
\t\t\t\t\tif (seg.length === 1) { out.push(""); continue }
|
|
3343
|
+
\t\t\t\t\tthrow new _PathParamError(\`Missing path param: \${name}\`)
|
|
3344
|
+
\t\t\t\t}
|
|
3345
|
+
\t\t\t\tout.push(this.#encodeWildcardValue(name, value))
|
|
3346
|
+
\t\t\t\tcontinue
|
|
3347
|
+
\t\t\t}
|
|
3348
|
+
\t\t\tout.push(
|
|
3349
|
+
\t\t\t\tseg.replace(/\\{([^{}/]+)\\}/g, (match: string, name: string) => {
|
|
3350
|
+
\t\t\t\t\tconst value = this.#paramValue(params, name)
|
|
3351
|
+
\t\t\t\t\tif (value === undefined) {
|
|
3352
|
+
\t\t\t\t\t\tif (partial) return match
|
|
3353
|
+
\t\t\t\t\t\tthrow new _PathParamError(\`Missing path param: \${name}\`)
|
|
3354
|
+
\t\t\t\t\t}
|
|
3355
|
+
\t\t\t\t\treturn this.#encodeSegmentValue(name, value)
|
|
3356
|
+
\t\t\t\t}),
|
|
3357
|
+
\t\t\t)
|
|
3358
|
+
\t\t}
|
|
3359
|
+
\t\treturn out.join("/")
|
|
3360
|
+
\t}
|
|
3361
|
+
|
|
3362
|
+
\t#hasPlaceholder(path: string): boolean {
|
|
3363
|
+
\t\tfor (const seg of path.split("/")) {
|
|
3364
|
+
\t\t\tif (seg.length > 1 && seg.charCodeAt(0) === 58) return true
|
|
3365
|
+
\t\t\tif (seg.charCodeAt(0) === 42) return true
|
|
3366
|
+
\t\t\tif (/\\{[^{}/]+\\}/.test(seg)) return true
|
|
3367
|
+
\t\t}
|
|
3368
|
+
\t\treturn false
|
|
3369
|
+
\t}
|
|
3370
|
+
`;
|
|
3371
|
+
}
|
|
3372
|
+
function sdkClientToColonParams() {
|
|
3373
|
+
return `
|
|
3374
|
+
\t/* OpenAPI template → route pattern: a whole-segment {name} is :name, the wildcard *name; {name} inside a segment stays */
|
|
3375
|
+
\t#toColonParams(path: string, wildcard?: string): string {
|
|
3376
|
+
\t\treturn path
|
|
3377
|
+
\t\t\t.split("/")
|
|
3378
|
+
\t\t\t.map((seg) => {
|
|
3379
|
+
\t\t\t\tconst m = /^\\{([^{}/]+)\\}$/.exec(seg)
|
|
3380
|
+
\t\t\t\tif (!m) return seg
|
|
3381
|
+
\t\t\t\treturn m[1] === wildcard ? \`*\${m[1]}\` : \`:\${m[1]}\`
|
|
3382
|
+
\t\t\t})
|
|
3383
|
+
\t\t\t.join("/")
|
|
3384
|
+
\t}
|
|
3385
|
+
`;
|
|
3386
|
+
}
|
|
3387
|
+
function sdkClientResolveInvalidationTargets() {
|
|
3388
|
+
return `
|
|
3389
|
+
\t/* params that are present are substituted; a target left partly unresolved stays a narrower pattern */
|
|
3390
|
+
\t#resolveInvalidationTargets(
|
|
3391
|
+
\t\ttargets: readonly string[],
|
|
3392
|
+
\t\tparams: Record<string, string> | undefined,
|
|
3393
|
+
\t): string[] {
|
|
3394
|
+
\t\tconst resolved: string[] = []
|
|
3395
|
+
\t\tfor (const target of targets) {
|
|
3396
|
+
\t\t\tconst spaceIdx = target.indexOf(" ")
|
|
3397
|
+
\t\t\tif (spaceIdx <= 0) continue
|
|
3398
|
+
\t\t\tconst targetMethod = target.slice(0, spaceIdx)
|
|
3399
|
+
\t\t\tconst targetPath = target.slice(spaceIdx + 1)
|
|
3400
|
+
\t\t\ttry {
|
|
3401
|
+
\t\t\t\tresolved.push(\`\${targetMethod} \${this.#interpolatePath(targetPath, params, true)}\`)
|
|
3402
|
+
\t\t\t} catch {
|
|
3403
|
+
\t\t\t\t/* a param value that is not a path segment marks nothing */
|
|
3404
|
+
\t\t\t}
|
|
3405
|
+
\t\t}
|
|
3406
|
+
\t\treturn resolved
|
|
3407
|
+
\t}
|
|
3408
|
+
`;
|
|
3409
|
+
}
|
|
3410
|
+
function sdkClientPathMatchesPattern() {
|
|
3411
|
+
return `
|
|
3412
|
+
\t#pathMatchesPattern(concretePath: string, pattern: string): boolean {
|
|
3413
|
+
\t\tlet re = this.#patternRegexCache.get(pattern)
|
|
3414
|
+
\t\tif (!re) {
|
|
3415
|
+
\t\t\tconst esc = (text: string) => text.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&")
|
|
3416
|
+
\t\t\tconst segments = pattern.split("/")
|
|
3417
|
+
\t\t\tlet source = ""
|
|
3418
|
+
\t\t\tfor (let i = 0; i < segments.length; i++) {
|
|
3419
|
+
\t\t\t\tconst seg = segments[i] ?? ""
|
|
3420
|
+
\t\t\t\tconst sep = i === 0 ? "" : "/"
|
|
3421
|
+
\t\t\t\tif (seg.length > 1 && seg.charCodeAt(0) === 58) {
|
|
3422
|
+
\t\t\t\t\tsource += seg.endsWith("?") ? \`(?:\${sep}[^/]+)?\` : \`\${sep}[^/]+\`
|
|
3423
|
+
\t\t\t\t\tcontinue
|
|
3424
|
+
\t\t\t\t}
|
|
3425
|
+
\t\t\t\tif (seg.charCodeAt(0) === 42 && i === segments.length - 1) {
|
|
3426
|
+
\t\t\t\t\tsource += \`(?:\${sep}.*)?\`
|
|
3427
|
+
\t\t\t\t\tcontinue
|
|
3428
|
+
\t\t\t\t}
|
|
3429
|
+
\t\t\t\tsource += sep + seg.split(/\\{[^{}/]+\\}/).map(esc).join("[^/]+")
|
|
3430
|
+
\t\t\t}
|
|
3431
|
+
\t\t\tre = new RegExp(\`^\${source}$\`)
|
|
3432
|
+
\t\t\tif (this.#patternRegexCache.size >= 1024) {
|
|
3433
|
+
\t\t\t\tconst oldest = this.#patternRegexCache.keys().next().value
|
|
3434
|
+
\t\t\t\tif (oldest !== undefined) this.#patternRegexCache.delete(oldest)
|
|
3435
|
+
\t\t\t}
|
|
3436
|
+
\t\t\tthis.#patternRegexCache.set(pattern, re)
|
|
3437
|
+
\t\t}
|
|
3438
|
+
\t\treturn re.test(concretePath)
|
|
3439
|
+
\t}
|
|
3440
|
+
`;
|
|
3441
|
+
}
|
|
3442
|
+
function sdkClientLookupStale() {
|
|
3443
|
+
return `
|
|
3444
|
+
\t/* Keep in sync with client/sdk.ts StaleIndex — exact keys and pattern keys are kept apart, so a
|
|
3445
|
+
\t concrete path containing ":" is never read as a pattern, and a lookup scans only its method. */
|
|
3446
|
+
\t#lookupStale(
|
|
3447
|
+
\t\tconcreteSelector: string,
|
|
3448
|
+
\t\tconcretePath: string,
|
|
3449
|
+
\t\tmethod: string,
|
|
3450
|
+
\t\tnow: number,
|
|
3451
|
+
\t): { by: string[]; isStale: boolean } {
|
|
3452
|
+
\t\tconst by = new Set<string>()
|
|
3453
|
+
\t\tconst exact = this.#staleUntil?.get(concreteSelector)
|
|
3454
|
+
\t\tif (exact) {
|
|
3455
|
+
\t\t\tif (exact.until > now) for (const m of exact.by) by.add(m)
|
|
3456
|
+
\t\t\telse this.#staleUntil?.delete(concreteSelector)
|
|
3457
|
+
\t\t}
|
|
3458
|
+
\t\tconst table = this.#stalePatterns.get(method)
|
|
3459
|
+
\t\tif (table) {
|
|
3460
|
+
\t\t\tfor (const [key, entry] of table) {
|
|
3461
|
+
\t\t\t\tif (entry.until <= now) { table.delete(key); continue }
|
|
3462
|
+
\t\t\t\tif (entry.refreshed?.has(concretePath)) continue
|
|
3463
|
+
\t\t\t\tif (this.#pathMatchesPattern(concretePath, key.slice(key.indexOf(" ") + 1))) {
|
|
3464
|
+
\t\t\t\t\tfor (const m of entry.by) by.add(m)
|
|
3465
|
+
\t\t\t\t}
|
|
3466
|
+
\t\t\t}
|
|
3467
|
+
\t\t}
|
|
3468
|
+
\t\treturn { by: [...by], isStale: by.size > 0 }
|
|
3469
|
+
\t}
|
|
3470
|
+
`;
|
|
3471
|
+
}
|
|
3472
|
+
function sdkClientCreateTypedWebSocket() {
|
|
3473
|
+
return `
|
|
3474
|
+
\t#createTypedWebSocket(url: string, protocols?: string | string[]): _TypedWebSocket {
|
|
3475
|
+
\t\tconst ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url)
|
|
3476
|
+
\t\t/* keyed by event and handler: one function may listen to several events */
|
|
3477
|
+
\t\tconst listenerMap = new Map<string, WeakMap<(...args: never[]) => void, EventListener>>()
|
|
3478
|
+
\t\tconst sendBuffer: Array<ArrayBuffer | ArrayBufferView | string> = []
|
|
3479
|
+
\t\tlet buffering = true
|
|
3480
|
+
|
|
3481
|
+
\t\tws.addEventListener("open", () => {
|
|
3482
|
+
\t\t\tbuffering = false
|
|
3483
|
+
\t\t\tfor (const msg of sendBuffer) ws.send(msg as Parameters<WebSocket["send"]>[0])
|
|
3484
|
+
\t\t\tsendBuffer.length = 0
|
|
3485
|
+
\t\t})
|
|
3486
|
+
\t\t/* a socket that never opens must not hold messages forever */
|
|
3487
|
+
\t\tconst drop = () => {
|
|
3488
|
+
\t\t\tbuffering = false
|
|
3489
|
+
\t\t\tsendBuffer.length = 0
|
|
3490
|
+
\t\t}
|
|
3491
|
+
\t\tws.addEventListener("close", drop)
|
|
3492
|
+
\t\tws.addEventListener("error", drop)
|
|
3493
|
+
|
|
3494
|
+
\t\tfunction close(code?: number, reason?: string) {
|
|
3495
|
+
\t\t\tdrop()
|
|
3496
|
+
\t\t\tws.close(code, reason)
|
|
3497
|
+
\t\t}
|
|
3498
|
+
|
|
3499
|
+
\t\tfunction on(event: string, handler: (...args: never[]) => void): void {
|
|
3500
|
+
\t\t\tlet wrapped: EventListener
|
|
3501
|
+
\t\t\tswitch (event) {
|
|
3502
|
+
\t\t\t\tcase "message":
|
|
3503
|
+
\t\t\t\t\twrapped = (e: Event) => (handler as (data: unknown) => void)((e as MessageEvent).data)
|
|
3504
|
+
\t\t\t\t\tbreak
|
|
3505
|
+
\t\t\t\tcase "open":
|
|
3506
|
+
\t\t\t\t\twrapped = () => (handler as () => void)()
|
|
3507
|
+
\t\t\t\t\tbreak
|
|
3508
|
+
\t\t\t\tcase "close":
|
|
3509
|
+
\t\t\t\t\twrapped = (e: Event) => (handler as (code: number, reason: string) => void)((e as CloseEvent).code, (e as CloseEvent).reason)
|
|
3510
|
+
\t\t\t\t\tbreak
|
|
3511
|
+
\t\t\t\tcase "error":
|
|
3512
|
+
\t\t\t\t\twrapped = (e: Event) => (handler as (error: unknown) => void)(e)
|
|
3513
|
+
\t\t\t\t\tbreak
|
|
3514
|
+
\t\t\t\tdefault:
|
|
3515
|
+
\t\t\t\t\treturn
|
|
3516
|
+
\t\t\t}
|
|
3517
|
+
\t\t\tlet byHandler = listenerMap.get(event)
|
|
3518
|
+
\t\t\tif (!byHandler) {
|
|
3519
|
+
\t\t\t\tbyHandler = new WeakMap()
|
|
3520
|
+
\t\t\t\tlistenerMap.set(event, byHandler)
|
|
3521
|
+
\t\t\t}
|
|
3522
|
+
\t\t\tbyHandler.set(handler, wrapped)
|
|
3523
|
+
\t\t\tws.addEventListener(event, wrapped)
|
|
3524
|
+
\t\t}
|
|
3525
|
+
|
|
3526
|
+
\t\tfunction off(event: string, handler: (...args: never[]) => void): void {
|
|
3527
|
+
\t\t\tconst byHandler = listenerMap.get(event)
|
|
3528
|
+
\t\t\tconst wrapped = byHandler?.get(handler)
|
|
3529
|
+
\t\t\tif (wrapped) {
|
|
3530
|
+
\t\t\t\tws.removeEventListener(event, wrapped)
|
|
3531
|
+
\t\t\t\tbyHandler?.delete(handler)
|
|
3532
|
+
\t\t\t}
|
|
3533
|
+
\t\t}
|
|
3534
|
+
|
|
3535
|
+
\t\tfunction send(data: ArrayBuffer | ArrayBufferView | object | string) {
|
|
3536
|
+
\t\t\tlet payload: ArrayBuffer | ArrayBufferView | string | Blob
|
|
3537
|
+
\t\t\tif (typeof data === "string") {
|
|
3538
|
+
\t\t\t\tpayload = data
|
|
3539
|
+
\t\t\t} else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
3540
|
+
\t\t\t\tpayload = data
|
|
3541
|
+
\t\t\t} else if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
3542
|
+
\t\t\t\tpayload = data
|
|
3543
|
+
\t\t\t} else {
|
|
3544
|
+
\t\t\t\tpayload = JSON.stringify(data)
|
|
3545
|
+
\t\t\t}
|
|
3546
|
+
\t\t\tif (buffering) { sendBuffer.push(payload as ArrayBuffer | ArrayBufferView | string) } else if (ws.readyState === 1) { ws.send(payload as Parameters<WebSocket["send"]>[0]) }
|
|
3547
|
+
\t\t}
|
|
3548
|
+
|
|
3549
|
+
\t\tconst typed: _TypedWebSocket = { close, off, on, get readyState() { return ws.readyState }, send }
|
|
3550
|
+
\t\tObject.defineProperty(typed, "_ws", { enumerable: false, value: ws })
|
|
3551
|
+
\t\treturn typed
|
|
3552
|
+
\t}
|
|
3553
|
+
`;
|
|
3554
|
+
}
|
|
3555
|
+
function sdkClientSerializeSearch() {
|
|
3556
|
+
return `
|
|
3557
|
+
\t#serializeSearch(query: Record<string, unknown>): URLSearchParams {
|
|
3558
|
+
\t\tconst params = new URLSearchParams()
|
|
3559
|
+
\t\tconst coerce = (v: unknown): string | null => {
|
|
3560
|
+
\t\t\tif (v === undefined || v === null) return null
|
|
3561
|
+
\t\t\tif (v instanceof Date) return v.toISOString()
|
|
3562
|
+
\t\t\tif (typeof v === "symbol") return null
|
|
3563
|
+
\t\t\treturn String(v)
|
|
3564
|
+
\t\t}
|
|
3565
|
+
\t\tfor (const [k, v] of Object.entries(query)) {
|
|
3566
|
+
\t\t\tif (Array.isArray(v)) {
|
|
3567
|
+
\t\t\t\tfor (const item of v) { const s = coerce(item); if (s !== null) params.append(k, s) }
|
|
3568
|
+
\t\t\t} else {
|
|
3569
|
+
\t\t\t\tconst s = coerce(v); if (s !== null) params.set(k, s)
|
|
3570
|
+
\t\t\t}
|
|
3571
|
+
\t\t}
|
|
3572
|
+
\t\treturn params
|
|
3573
|
+
\t}
|
|
3574
|
+
`;
|
|
3575
|
+
}
|
|
3576
|
+
/* Keep in sync with client/defaults.ts — generated SDKs have zero runtime imports. */
|
|
3577
|
+
function sdkClientResolveBaseURL() {
|
|
3578
|
+
return `
|
|
3579
|
+
#resolveBaseURL(baseURL: string): URL {
|
|
3580
|
+
if (/^(?:https?|wss?):\\/\\//i.test(baseURL)) return new URL(baseURL)
|
|
3581
|
+
let origin: unknown
|
|
3582
|
+
try {
|
|
3583
|
+
origin = globalThis.location?.origin
|
|
3584
|
+
} catch {
|
|
3585
|
+
origin = undefined
|
|
3586
|
+
}
|
|
3587
|
+
if (typeof origin === "string" && origin !== "" && origin !== "null" && /^(?:https?|wss?):\\/\\//i.test(origin)) {
|
|
3588
|
+
try {
|
|
3589
|
+
return new URL(baseURL, origin)
|
|
3590
|
+
} catch {
|
|
3591
|
+
throw new Error(\`Invalid baseURL \${JSON.stringify(baseURL)}: expected an absolute http(s): or ws(s): URL. Path-only values such as "/api" resolve against location.origin in browsers.\`)
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
throw new Error(\`Invalid baseURL \${JSON.stringify(baseURL)}: expected an absolute http(s): or ws(s): URL. Path-only values such as "/api" resolve against location.origin in browsers.\`)
|
|
3595
|
+
}
|
|
3596
|
+
`;
|
|
3597
|
+
}
|
|
3598
|
+
function sdkClientBuildURL() {
|
|
3599
|
+
return `
|
|
3600
|
+
\t#buildURL(path: string, opts: _RequestOptions): string {
|
|
3601
|
+
\t\tconst resolvedPath = this.#interpolatePath(path, opts.params)
|
|
3602
|
+
\t\tconst baseUrl = this.#resolveBaseURL(this.#config.baseURL)
|
|
3603
|
+
\t\tconst basePath = baseUrl.pathname.endsWith("/") ? baseUrl.pathname : \`\${baseUrl.pathname}/\`
|
|
3604
|
+
\t\tconst relative = resolvedPath.startsWith("/") ? resolvedPath.slice(1) : resolvedPath
|
|
3605
|
+
\t\tconst url = new URL(\`\${basePath}\${relative}\`, baseUrl)
|
|
3606
|
+
\t\t/* a request (and its credentials) never leaves the configured origin */
|
|
3607
|
+
\t\tif (url.origin !== baseUrl.origin) {
|
|
3608
|
+
\t\t\tthrow new _PathParamError(\`Invalid path param: \${JSON.stringify(path)} resolves outside \${baseUrl.origin}\`)
|
|
3609
|
+
\t\t}
|
|
3610
|
+
\t\tfor (const [k, v] of baseUrl.searchParams.entries()) url.searchParams.append(k, v)
|
|
3611
|
+
\t\tif (opts.search) {
|
|
3612
|
+
\t\t\tconst sp = this.#searchSerializer(opts.search)
|
|
3613
|
+
\t\t\tif (this.#config.sortSearchParams) sp.sort()
|
|
3614
|
+
\t\t\tfor (const [k, v] of sp.entries()) url.searchParams.append(k, v)
|
|
3615
|
+
\t\t}
|
|
3616
|
+
\t\treturn url.toString()
|
|
3617
|
+
\t}
|
|
3618
|
+
`;
|
|
3619
|
+
}
|
|
3620
|
+
function sdkClientNewRequestId() {
|
|
3621
|
+
return `
|
|
3622
|
+
\t#newRequestId(): string {
|
|
3623
|
+
\t\tconst c = globalThis.crypto as Crypto | undefined
|
|
3624
|
+
\t\tif (c && typeof c.randomUUID === "function") return c.randomUUID()
|
|
3625
|
+
\t\tif (c && typeof c.getRandomValues === "function") {
|
|
3626
|
+
\t\t\tconst bytes = new Uint8Array(16)
|
|
3627
|
+
\t\t\tc.getRandomValues(bytes)
|
|
3628
|
+
\t\t\tbytes[6] = (bytes[6]! & 0x0f) | 0x40
|
|
3629
|
+
\t\t\tbytes[8] = (bytes[8]! & 0x3f) | 0x80
|
|
3630
|
+
\t\t\tlet out = ""
|
|
3631
|
+
\t\t\tfor (let i = 0; i < 16; i++) {
|
|
3632
|
+
\t\t\t\tif (i === 4 || i === 6 || i === 8 || i === 10) out += "-"
|
|
3633
|
+
\t\t\t\tout += bytes[i]!.toString(16).padStart(2, "0")
|
|
3634
|
+
\t\t\t}
|
|
3635
|
+
\t\t\treturn out
|
|
3636
|
+
\t\t}
|
|
3637
|
+
\t\tthrow new Error("honey: no crypto.randomUUID or crypto.getRandomValues in this runtime")
|
|
3638
|
+
\t}
|
|
3639
|
+
`;
|
|
3640
|
+
}
|
|
3641
|
+
function sdkClientBuildHeaders() {
|
|
3642
|
+
return `
|
|
3643
|
+
\tasync #buildHeaders(
|
|
3644
|
+
\t\topts: _RequestOptions,
|
|
3645
|
+
\t\tctx: { method: string; path: string },
|
|
3646
|
+
\t): Promise<Headers> {
|
|
3647
|
+
\t\tconst headers = new Headers()
|
|
3648
|
+
|
|
3649
|
+
\t\tif (this.#config.headers) {
|
|
3650
|
+
\t\t\tconst resolved =
|
|
3651
|
+
\t\t\t\ttypeof this.#config.headers === "function"
|
|
3652
|
+
\t\t\t\t\t? await this.#config.headers(ctx)
|
|
3653
|
+
\t\t\t\t\t: this.#config.headers
|
|
3654
|
+
\t\t\tfor (const [k, v] of Object.entries(resolved)) {
|
|
3655
|
+
\t\t\t\tif (v !== undefined) headers.set(k, v)
|
|
3656
|
+
\t\t\t}
|
|
3657
|
+
\t\t}
|
|
3658
|
+
|
|
3659
|
+
\t\t/* a token from onAuthExpired replaces the configured one until the next refresh */
|
|
3660
|
+
\t\tif (this.#token !== null) {
|
|
3661
|
+
\t\t\theaders.set(this.#config.authHeaderName ?? "Authorization", \`\${this.#config.authHeaderPrefix ?? "Bearer "}\${this.#token}\`)
|
|
3662
|
+
\t\t}
|
|
3663
|
+
|
|
3664
|
+
\t\tif (opts.headers) {
|
|
3665
|
+
\t\t\tfor (const [k, v] of Object.entries(opts.headers)) {
|
|
3666
|
+
\t\t\t\tif (v !== undefined) headers.set(k, v)
|
|
3667
|
+
\t\t\t}
|
|
3668
|
+
\t\t}
|
|
3669
|
+
|
|
3670
|
+
\t\tif (opts.cookies) {
|
|
3671
|
+
\t\t\tconst existing = headers.get("cookie")
|
|
3672
|
+
\t\t\tconst pairs = Object.entries(opts.cookies)
|
|
3673
|
+
\t\t\t\t.map(([k, v]) => \`\${encodeURIComponent(k)}=\${encodeURIComponent(v)}\`)
|
|
3674
|
+
\t\t\t\t.join("; ")
|
|
3675
|
+
\t\t\tif (pairs) {
|
|
3676
|
+
\t\t\t\theaders.set("cookie", existing ? \`\${existing}; \${pairs}\` : pairs)
|
|
3677
|
+
\t\t\t}
|
|
3678
|
+
\t\t}
|
|
3679
|
+
|
|
3680
|
+
\t\tif (!headers.has("x-request-id")) {
|
|
3681
|
+
\t\t\theaders.set("x-request-id", this.#newRequestId())
|
|
3682
|
+
\t\t}
|
|
3683
|
+
|
|
3684
|
+
\t\treturn headers
|
|
3685
|
+
\t}
|
|
3686
|
+
`;
|
|
3687
|
+
}
|
|
3688
|
+
function sdkClientDoRequest() {
|
|
3689
|
+
return `
|
|
3690
|
+
\t/* the body of a request, and the content-type it needs — shared by plain requests and SSE */
|
|
3691
|
+
\t#buildBody(opts: _RequestOptions, headers: Headers): BodyInit | undefined {
|
|
3692
|
+
\t\tif (opts.body !== undefined) {
|
|
3693
|
+
\t\t\tif (!headers.has("content-type")) headers.set("content-type", "application/octet-stream")
|
|
3694
|
+
\t\t\treturn opts.body as BodyInit
|
|
3695
|
+
\t\t}
|
|
3696
|
+
\t\tif (opts.json !== undefined) {
|
|
3697
|
+
\t\t\theaders.set("content-type", "application/json")
|
|
3698
|
+
\t\t\ttry {
|
|
3699
|
+
\t\t\t\treturn JSON.stringify(opts.json)
|
|
3700
|
+
\t\t\t} catch (e) {
|
|
3701
|
+
\t\t\t\tthrow new _ClientError({
|
|
3702
|
+
\t\t\t\t\tbody: opts.json,
|
|
3703
|
+
\t\t\t\t\tdata: null,
|
|
3704
|
+
\t\t\t\t\tmessage: \`JSON serialization failed: \${e instanceof Error ? e.message : String(e)}\`,
|
|
3705
|
+
\t\t\t\t\tresponse: new Response(null, { status: 0 }),
|
|
3706
|
+
\t\t\t\t\tstatus: 0,
|
|
3707
|
+
\t\t\t\t})
|
|
3708
|
+
\t\t\t}
|
|
3709
|
+
\t\t}
|
|
3710
|
+
\t\tif (opts.form === undefined) return undefined
|
|
3711
|
+
\t\tconst isFile = (v: unknown) =>
|
|
3712
|
+
\t\t\t(typeof File !== "undefined" && v instanceof File) || (typeof Blob !== "undefined" && v instanceof Blob)
|
|
3713
|
+
\t\tconst hasFiles = Object.values(opts.form).some(
|
|
3714
|
+
\t\t\t(v) =>
|
|
3715
|
+
\t\t\t\tisFile(v) ||
|
|
3716
|
+
\t\t\t\t(typeof FileList !== "undefined" && v instanceof FileList) ||
|
|
3717
|
+
\t\t\t\t(Array.isArray(v) && v.some(isFile)),
|
|
3718
|
+
\t\t)
|
|
3719
|
+
\t\tconst text = (v: unknown): string | null => {
|
|
3720
|
+
\t\t\tif (v === undefined || v === null || typeof v === "symbol") return null
|
|
3721
|
+
\t\t\tif (v instanceof Date) return v.toISOString()
|
|
3722
|
+
\t\t\tif (typeof v === "object") throw new TypeError("form: nested objects cannot be form-encoded; send json instead")
|
|
3723
|
+
\t\t\treturn String(v)
|
|
3724
|
+
\t\t}
|
|
3725
|
+
\t\tif (hasFiles) {
|
|
3726
|
+
\t\t\tconst fd = new FormData()
|
|
3727
|
+
\t\t\tfor (const [k, v] of Object.entries(opts.form)) {
|
|
3728
|
+
\t\t\t\tif (typeof FileList !== "undefined" && v instanceof FileList) {
|
|
3729
|
+
\t\t\t\t\tfor (let i = 0; i < v.length; i++) { const f = v[i]; if (f) fd.append(k, f) }
|
|
3730
|
+
\t\t\t\t\tcontinue
|
|
3731
|
+
\t\t\t\t}
|
|
3732
|
+
\t\t\t\tfor (const item of Array.isArray(v) ? v : [v]) {
|
|
3733
|
+
\t\t\t\t\tif (isFile(item)) fd.append(k, item as Blob)
|
|
3734
|
+
\t\t\t\t\telse {
|
|
3735
|
+
\t\t\t\t\t\tconst s = text(item)
|
|
3736
|
+
\t\t\t\t\t\tif (s !== null) fd.append(k, s)
|
|
3737
|
+
\t\t\t\t\t}
|
|
3738
|
+
\t\t\t\t}
|
|
3739
|
+
\t\t\t}
|
|
3740
|
+
\t\t\treturn fd
|
|
3741
|
+
\t\t}
|
|
3742
|
+
\t\theaders.set("content-type", "application/x-www-form-urlencoded")
|
|
3743
|
+
\t\tconst sp = new URLSearchParams()
|
|
3744
|
+
\t\t/* arrays repeat the key, as they do in multipart and in the query */
|
|
3745
|
+
\t\tfor (const [k, v] of Object.entries(opts.form)) {
|
|
3746
|
+
\t\t\tfor (const item of Array.isArray(v) ? v : [v]) {
|
|
3747
|
+
\t\t\t\tconst s = text(item)
|
|
3748
|
+
\t\t\t\tif (s !== null) sp.append(k, s)
|
|
3749
|
+
\t\t\t}
|
|
3750
|
+
\t\t}
|
|
3751
|
+
\t\treturn sp.toString()
|
|
3752
|
+
\t}
|
|
3753
|
+
|
|
3754
|
+
\t/* a stream body is sent once and needs duplex: "half" (Node's fetch throws without it) */
|
|
3755
|
+
\t#requestInit(init: RequestInit): RequestInit {
|
|
3756
|
+
\t\tif (typeof ReadableStream !== "undefined" && init.body instanceof ReadableStream) {
|
|
3757
|
+
\t\t\t;(init as RequestInit & { duplex?: string }).duplex = "half"
|
|
3758
|
+
\t\t}
|
|
3759
|
+
\t\treturn init
|
|
3760
|
+
\t}
|
|
3761
|
+
|
|
3762
|
+
\t/* the Request a response hook sees; a stream body was consumed by the send, so it is left off */
|
|
3763
|
+
\t#hookRequest(url: string, init: RequestInit): Request {
|
|
3764
|
+
\t\tconst consumed = typeof ReadableStream !== "undefined" && init.body instanceof ReadableStream
|
|
3765
|
+
\t\treturn new Request(url, consumed ? { ...init, body: null } : init)
|
|
3766
|
+
\t}
|
|
3767
|
+
|
|
3768
|
+
\tasync #doRequest(
|
|
3769
|
+
\t\tmethod: string,
|
|
3770
|
+
\t\tpath: string,
|
|
3771
|
+
\t\topts: _RequestOptions,
|
|
3772
|
+
\t\tisRetry: boolean,
|
|
3773
|
+
\t\trequestMeta?: _RequestMeta,
|
|
3774
|
+
\t): Promise<{ done: () => void; response: Response }> {
|
|
3775
|
+
\t\tconst url = this.#buildURL(path, opts)
|
|
3776
|
+
\t\tconst headers = await this.#buildHeaders(opts, { method, path })
|
|
3777
|
+
\t\tlet body = this.#buildBody(opts, headers)
|
|
3778
|
+
|
|
3779
|
+
\t\tif (this.#config.onRequest) {
|
|
3780
|
+
\t\t\tconst reqCtx: { body?: BodyInit; headers: Headers; invalidatedBy?: string[]; isStale?: boolean; method: string; path: string; selector?: string; state: Record<string, unknown>; url: string } = { body, headers, method, path, state: this.#config.state ?? {}, url }
|
|
3781
|
+
\t\t\tif (requestMeta) {
|
|
3782
|
+
\t\t\t\treqCtx.invalidatedBy = requestMeta.invalidatedBy
|
|
3783
|
+
\t\t\t\treqCtx.isStale = requestMeta.isStale
|
|
3784
|
+
\t\t\t\treqCtx.selector = requestMeta.selector
|
|
3785
|
+
\t\t\t}
|
|
3786
|
+
\t\t\tfor (const hook of this.#config.onRequest) {
|
|
3787
|
+
\t\t\t\tawait hook(reqCtx)
|
|
3788
|
+
\t\t\t}
|
|
3789
|
+
\t\t\tif (reqCtx.body !== body) body = reqCtx.body
|
|
3790
|
+
\t\t}
|
|
3791
|
+
|
|
3792
|
+
\t\tconst { signal, cleanup } = this.#buildSignal(opts)
|
|
3793
|
+
\t\tconst init = this.#requestInit({ body, headers, method, signal })
|
|
3794
|
+
\t\tif (this.#config.credentials) init.credentials = this.#config.credentials
|
|
3795
|
+
\t\tif (this.#config.mode) init.mode = this.#config.mode
|
|
3796
|
+
|
|
3797
|
+
\t\tconst _logOp = \`\${method.toUpperCase()} \${path}\`
|
|
3798
|
+
\t\tconst _logStart = Date.now()
|
|
3799
|
+
\t\tthis.#config.onLog?.({ duration_ms: 0, event: "request_start", level: "debug", operation: _logOp })
|
|
3800
|
+
|
|
3801
|
+
\t\ttry {
|
|
3802
|
+
\t\t\tsignal?.throwIfAborted()
|
|
3803
|
+
\t\t\tlet response = await this.#send(url, init)
|
|
3804
|
+
|
|
3805
|
+
\t\t\t/* FormData, Blob, and strings can be sent again; a stream was consumed by the first attempt */
|
|
3806
|
+
\t\t\tif (response.status === 401 && this.#config.onAuthExpired && !isRetry && !(body instanceof ReadableStream)) {
|
|
3807
|
+
\t\t\t\tconst authName = this.#config.authHeaderName ?? "Authorization"
|
|
3808
|
+
\t\t\t\tconst authPrefix = this.#config.authHeaderPrefix ?? "Bearer "
|
|
3809
|
+
\t\t\t\tconst sent = new Headers(headers).get(authName)
|
|
3810
|
+
\t\t\t\tconst rejectedToken = sent?.startsWith(authPrefix) ? sent.slice(authPrefix.length) : (sent ?? null)
|
|
3811
|
+
\t\t\t\tconst newToken = await this.#refreshToken(rejectedToken)
|
|
3812
|
+
\t\t\t\tif (newToken !== null) {
|
|
3813
|
+
\t\t\t\t\tconst retryHeaders = new Headers(headers)
|
|
3814
|
+
\t\t\t\t\tretryHeaders.set(authName, \`\${authPrefix}\${newToken}\`)
|
|
3815
|
+
\t\t\t\t\tawait response.body?.cancel().catch(() => {})
|
|
3816
|
+
\t\t\t\t\tresponse = await this.#send(url, { ...init, headers: retryHeaders })
|
|
3817
|
+
\t\t\t\t}
|
|
3818
|
+
\t\t\t}
|
|
3819
|
+
|
|
3820
|
+
\t\t\tthis.#config.onLog?.({
|
|
3821
|
+
\t\t\t\tduration_ms: Date.now() - _logStart,
|
|
3822
|
+
\t\t\t\tevent: "response_received",
|
|
3823
|
+
\t\t\t\tlevel: response.status >= 400 ? "warn" : "info",
|
|
3824
|
+
\t\t\t\toperation: _logOp,
|
|
3825
|
+
\t\t\t\tstatus: response.status,
|
|
3826
|
+
\t\t\t})
|
|
3827
|
+
|
|
3828
|
+
\t\t\tif (this.#config.onResponse) {
|
|
3829
|
+
\t\t\t\tconst resCtx: { invalidatedBy?: string[]; isRetry: boolean; isStale?: boolean; method: string; path: string; request: Request; response: Response; retry: () => Promise<Response>; selector?: string; state: Record<string, unknown>; url: string } = {
|
|
3830
|
+
\t\t\t\t\tisRetry,
|
|
3831
|
+
\t\t\t\t\tmethod,
|
|
3832
|
+
\t\t\t\t\tpath,
|
|
3833
|
+
\t\t\t\t\trequest: this.#hookRequest(url, init),
|
|
3834
|
+
\t\t\t\t\tresponse,
|
|
3835
|
+
\t\t\t\t\tretry: () => {
|
|
3836
|
+
\t\t\t\t\t\tif (isRetry) throw new Error("Max 1 retry per request")
|
|
3837
|
+
\t\t\t\t\t\tif (body instanceof ReadableStream) throw new Error("A streamed request body cannot be sent again")
|
|
3838
|
+
\t\t\t\t\t\treturn this.#doRequest(method, path, opts, true, requestMeta)
|
|
3839
|
+
\t\t\t\t\t\t\t.then(async (r) => {
|
|
3840
|
+
\t\t\t\t\t\t\t\tr.done()
|
|
3841
|
+
\t\t\t\t\t\t\t\tif (!r.response.ok) throw await this.#parseAsClientError(r.response)
|
|
3842
|
+
\t\t\t\t\t\t\t\treturn r.response
|
|
3843
|
+
\t\t\t\t\t\t\t})
|
|
3844
|
+
\t\t\t\t\t},
|
|
3845
|
+
\t\t\t\t\tstate: this.#config.state ?? {},
|
|
3846
|
+
\t\t\t\t\turl,
|
|
3847
|
+
\t\t\t\t}
|
|
3848
|
+
\t\t\t\tif (requestMeta) {
|
|
3849
|
+
\t\t\t\t\tresCtx.invalidatedBy = requestMeta.invalidatedBy
|
|
3850
|
+
\t\t\t\t\tresCtx.isStale = requestMeta.isStale
|
|
3851
|
+
\t\t\t\t\tresCtx.selector = requestMeta.selector
|
|
3852
|
+
\t\t\t\t}
|
|
3853
|
+
\t\t\t\tfor (const hook of this.#config.onResponse) {
|
|
3854
|
+
\t\t\t\t\tconst result = await hook(resCtx)
|
|
3855
|
+
\t\t\t\t\tif (result instanceof Response) {
|
|
3856
|
+
\t\t\t\t\t\tresponse = result
|
|
3857
|
+
\t\t\t\t\t\tresCtx.response = result
|
|
3858
|
+
\t\t\t\t\t}
|
|
3859
|
+
\t\t\t\t}
|
|
3860
|
+
\t\t\t}
|
|
3861
|
+
|
|
3862
|
+
\t\t\t/* the caller releases the timer after reading the body: the timeout covers the whole exchange */
|
|
3863
|
+
\t\t\treturn { done: cleanup, response }
|
|
3864
|
+
\t\t} catch (err) {
|
|
3865
|
+
\t\t\tcleanup()
|
|
3866
|
+
\t\t\tconst _errStatus = typeof (err as { status?: unknown })?.status === "number" ? (err as { status: number }).status : undefined
|
|
3867
|
+
\t\t\tthis.#config.onLog?.({
|
|
3868
|
+
\t\t\t\tduration_ms: Date.now() - _logStart,
|
|
3869
|
+
\t\t\t\terror: err,
|
|
3870
|
+
\t\t\t\tevent: "error",
|
|
3871
|
+
\t\t\t\tlevel: "error",
|
|
3872
|
+
\t\t\t\toperation: _logOp,
|
|
3873
|
+
\t\t\t\t...(_errStatus !== undefined ? { status: _errStatus } : {}),
|
|
3874
|
+
\t\t\t})
|
|
3875
|
+
\t\t\tthrow err
|
|
3876
|
+
\t\t}
|
|
3877
|
+
\t}
|
|
3878
|
+
|
|
3879
|
+
\t/* one refresh for every 401 in flight; the new token is kept for later requests */
|
|
3880
|
+
\t#refreshToken(rejectedToken: string | null): Promise<string | null> {
|
|
3881
|
+
\t\tconst hook = this.#config.onAuthExpired
|
|
3882
|
+
\t\tif (!hook) return Promise.resolve(null)
|
|
3883
|
+
\t\tif (rejectedToken !== null && this.#token !== null && rejectedToken !== this.#token) {
|
|
3884
|
+
\t\t\t/* another request already refreshed past the rejected token */
|
|
3885
|
+
\t\t\treturn Promise.resolve(this.#token)
|
|
3886
|
+
\t\t}
|
|
3887
|
+
\t\tthis.#refreshing ??= Promise.resolve()
|
|
3888
|
+
\t\t\t.then(() => hook({ rejectedToken }))
|
|
3889
|
+
\t\t\t.then((token) => {
|
|
3890
|
+
\t\t\t\tthis.#token = typeof token === "string" && token.length > 0 ? token : null
|
|
3891
|
+
\t\t\t\treturn this.#token
|
|
3892
|
+
\t\t\t})
|
|
3893
|
+
\t\t\t.finally(() => {
|
|
3894
|
+
\t\t\t\tthis.#refreshing = null
|
|
3895
|
+
\t\t\t})
|
|
3896
|
+
\t\treturn this.#refreshing
|
|
3897
|
+
\t}
|
|
3898
|
+
|
|
3899
|
+
\t/* Same rules as client/http.ts; the cross-origin header allowlist comes from
|
|
3900
|
+
\t client/redirect-policy.ts. "same-origin" (default) follows a redirect only while it stays on
|
|
3901
|
+
\t the base URL's origin; "follow" also follows cross-origin ones, carrying only allowlisted
|
|
3902
|
+
\t headers and never a replayed body.
|
|
3903
|
+
\t Browsers hide redirect targets from script, so there the platform follows. */
|
|
3904
|
+
\tasync #send(url: string, init: RequestInit): Promise<Response> {
|
|
3905
|
+
\t\tconst policy = this.#config.redirect ?? "same-origin"
|
|
3906
|
+
\t\tconst g = globalThis as { document?: unknown; WorkerGlobalScope?: unknown }
|
|
3907
|
+
\t\tconst platformFollows = typeof g.document !== "undefined" || typeof g.WorkerGlobalScope !== "undefined"
|
|
3908
|
+
\t\tif (policy === "manual" || policy === "error" || platformFollows) {
|
|
3909
|
+
\t\t\treturn this.#fetchFn(url, policy === "same-origin" ? init : { ...init, redirect: policy })
|
|
3910
|
+
\t\t}
|
|
3911
|
+
\t\tconst origin = new URL(url).origin
|
|
3912
|
+
\t\tlet current = new URL(url)
|
|
3913
|
+
\t\tlet currentInit: RequestInit = { ...init, redirect: "manual" }
|
|
3914
|
+
\t\tconst replayable = (b: unknown) =>
|
|
3915
|
+
\t\t\tb == null || typeof b === "string" || (typeof Blob !== "undefined" && b instanceof Blob) || (typeof FormData !== "undefined" && b instanceof FormData) || b instanceof URLSearchParams || b instanceof ArrayBuffer || ArrayBuffer.isView(b)
|
|
3916
|
+
\t\tfor (let hop = 0; ; hop++) {
|
|
3917
|
+
\t\t\tconst response = await this.#fetchFn(current.toString(), currentInit)
|
|
3918
|
+
\t\t\tconst location = response.headers.get("location")
|
|
3919
|
+
\t\t\tif (![301, 302, 303, 307, 308].includes(response.status) || location === null || hop >= 20) return response
|
|
3920
|
+
\t\t\tlet target: URL
|
|
3921
|
+
\t\t\ttry {
|
|
3922
|
+
\t\t\t\ttarget = new URL(location, current)
|
|
3923
|
+
\t\t\t} catch {
|
|
3924
|
+
\t\t\t\treturn response
|
|
3925
|
+
\t\t\t}
|
|
3926
|
+
\t\t\tif (target.protocol !== "http:" && target.protocol !== "https:") return response
|
|
3927
|
+
\t\t\tconst crossOrigin = target.origin !== origin
|
|
3928
|
+
\t\t\tif (crossOrigin && policy !== "follow") return response
|
|
3929
|
+
\t\t\tconst nextInit: RequestInit = { ...currentInit, headers: new Headers(currentInit.headers) }
|
|
3930
|
+
\t\t\tif (response.status === 307 || response.status === 308) {
|
|
3931
|
+
\t\t\t\tif (currentInit.body != null && (crossOrigin || !replayable(currentInit.body))) return response
|
|
3932
|
+
\t\t\t} else {
|
|
3933
|
+
\t\t\t\tconst method = (currentInit.method ?? "GET").toUpperCase()
|
|
3934
|
+
\t\t\t\tif (response.status === 303 ? method !== "HEAD" : method === "POST") {
|
|
3935
|
+
\t\t\t\t\tnextInit.method = "GET"
|
|
3936
|
+
\t\t\t\t\tnextInit.body = undefined
|
|
3937
|
+
\t\t\t\t\t;(nextInit.headers as Headers).delete("content-type")
|
|
3938
|
+
\t\t\t\t}
|
|
3939
|
+
\t\t\t}
|
|
3940
|
+
\t\t\tif (crossOrigin) {
|
|
3941
|
+
\t\t\t\t/* only allowlisted headers cross: no credential, configured, per-call or hook header */
|
|
3942
|
+
\t\t\t\tconst safe = new Headers()
|
|
3943
|
+
\t\t\t\tfor (const name of [${CROSS_ORIGIN_SAFE_HEADERS.map((h) => JSON.stringify(h)).join(", ")}]) {
|
|
3944
|
+
\t\t\t\t\tconst value = (nextInit.headers as Headers).get(name)
|
|
3945
|
+
\t\t\t\t\tif (value !== null) safe.set(name, value)
|
|
3946
|
+
\t\t\t\t}
|
|
3947
|
+
\t\t\t\tnextInit.headers = safe
|
|
3948
|
+
\t\t\t}
|
|
3949
|
+
\t\t\tawait response.body?.cancel().catch(() => {})
|
|
3950
|
+
\t\t\tcurrent = target
|
|
3951
|
+
\t\t\tcurrentInit = nextInit
|
|
3952
|
+
\t\t}
|
|
3953
|
+
\t}
|
|
3954
|
+
`;
|
|
3955
|
+
}
|
|
3956
|
+
function sdkClientParseBody() {
|
|
3957
|
+
return `
|
|
3958
|
+
\t#parseBody(response: Response): Promise<unknown> {
|
|
3959
|
+
\t\tif (response.status === 204) return Promise.resolve(null)
|
|
3960
|
+
|
|
3961
|
+
\t\tconst rawCt = response.headers.get("content-type") ?? ""
|
|
3962
|
+
\t\tconst ct = rawCt.split(";")[0]?.trim().toLowerCase() ?? ""
|
|
3963
|
+
\t\tif (ct === "application/json" || ct.endsWith("+json")) {
|
|
3964
|
+
\t\t\treturn response.json()
|
|
3965
|
+
\t\t}
|
|
3966
|
+
\t\tif (ct === "application/octet-stream" || ct === "application/pdf") {
|
|
3967
|
+
\t\t\treturn response.arrayBuffer()
|
|
3968
|
+
\t\t}
|
|
3969
|
+
\t\t/* the classes the generated types use: text/*, XML, otherwise bytes */
|
|
3970
|
+
\t\tif (ct.startsWith("text/") || ct === "application/xml" || ct.endsWith("+xml")) return response.text()
|
|
3971
|
+
\t\t/* unknown content type \u2014 binary-safe fallback */
|
|
3972
|
+
\t\treturn response.arrayBuffer()
|
|
3973
|
+
\t}
|
|
3974
|
+
`;
|
|
3975
|
+
}
|
|
3976
|
+
function sdkClientParseErrorBody() {
|
|
3977
|
+
return `
|
|
3978
|
+
\tasync #parseErrorBody(response: Response): Promise<unknown> {
|
|
3979
|
+
\t\ttry {
|
|
3980
|
+
\t\t\treturn await response.json()
|
|
3981
|
+
\t\t} catch {
|
|
3982
|
+
\t\t\treturn undefined
|
|
3983
|
+
\t\t}
|
|
3984
|
+
\t}
|
|
3985
|
+
`;
|
|
3986
|
+
}
|
|
3987
|
+
function sdkClientParseAsClientError() {
|
|
3988
|
+
return `
|
|
3989
|
+
\tasync #parseAsClientError(response: Response): Promise<_ClientError> {
|
|
3990
|
+
\t\tconst preserved = response.clone()
|
|
3991
|
+
\t\tconst body = await this.#parseErrorBody(response)
|
|
3992
|
+
\t\tconst msgVal = typeof body === "object" && body !== null && "message" in body
|
|
3993
|
+
\t\t\t? (body as Record<string, unknown>)["message"]
|
|
3994
|
+
\t\t\t: undefined
|
|
3995
|
+
\t\tconst rawMsg = typeof msgVal === "string" ? msgVal : null
|
|
3996
|
+
\t\tconst safeMsg = rawMsg !== null
|
|
3997
|
+
\t\t\t? rawMsg.replace(/[\\x00-\\x1f]/g, "").slice(0, this.#maxErrorMessageChars)
|
|
3998
|
+
\t\t\t: \`HTTP \${response.status}\`
|
|
3999
|
+
\t\tconst Cls = _STATUS_ERROR_MAP[response.status] ?? _ClientError
|
|
4000
|
+
\t\treturn new Cls({ body, data: body, message: safeMsg, response: preserved, status: response.status })
|
|
4001
|
+
\t}
|
|
4002
|
+
`;
|
|
4003
|
+
}
|
|
4004
|
+
function sdkClientBuildSignal() {
|
|
4005
|
+
return `
|
|
4006
|
+
\t#buildSignal(opts: _RequestOptions, isStream?: boolean): { cleanup: () => void; signal: AbortSignal } {
|
|
4007
|
+
\t\tconst userSignal = opts.signal
|
|
4008
|
+
\t\tconst timeout = isStream ? undefined : (opts.timeout ?? this.#config.timeout)
|
|
4009
|
+
\t\tif (!timeout && !userSignal) return { cleanup: () => {}, signal: this.#disposeCtrl.signal }
|
|
4010
|
+
\t\tconst ctrl = new AbortController()
|
|
4011
|
+
\t\tlet timer: ReturnType<typeof setTimeout> | undefined
|
|
4012
|
+
\t\tif (timeout) {
|
|
4013
|
+
\t\t\ttimer = setTimeout(() => ctrl.abort(new Error("timeout")), timeout)
|
|
4014
|
+
\t\t\tif (typeof (timer as unknown as { unref?: () => void }).unref === "function") {
|
|
4015
|
+
\t\t\t\t(timer as unknown as { unref: () => void }).unref()
|
|
4016
|
+
\t\t\t}
|
|
4017
|
+
\t\t}
|
|
4018
|
+
\t\tconst abort = (reason?: unknown) => { if (!ctrl.signal.aborted) ctrl.abort(reason) }
|
|
4019
|
+
\t\tconst onDispose = () => abort(this.#disposeCtrl.signal.reason)
|
|
4020
|
+
\t\tconst onUserAbort = () => abort(userSignal?.reason)
|
|
4021
|
+
\t\tif (this.#disposeCtrl.signal.aborted) {
|
|
4022
|
+
\t\t\tabort(this.#disposeCtrl.signal.reason)
|
|
4023
|
+
\t\t} else {
|
|
4024
|
+
\t\t\tthis.#disposeCtrl.signal.addEventListener("abort", onDispose, { once: true })
|
|
4025
|
+
\t\t}
|
|
4026
|
+
\t\tif (userSignal?.aborted) {
|
|
4027
|
+
\t\t\tabort(userSignal.reason)
|
|
4028
|
+
\t\t} else if (userSignal) {
|
|
4029
|
+
\t\t\tuserSignal.addEventListener("abort", onUserAbort, { once: true })
|
|
4030
|
+
\t\t}
|
|
4031
|
+
\t\treturn {
|
|
4032
|
+
\t\t\tcleanup: () => {
|
|
4033
|
+
\t\t\t\tif (timer !== undefined) clearTimeout(timer)
|
|
4034
|
+
\t\t\t\tthis.#disposeCtrl.signal.removeEventListener("abort", onDispose)
|
|
4035
|
+
\t\t\t\tuserSignal?.removeEventListener("abort", onUserAbort)
|
|
4036
|
+
\t\t\t},
|
|
4037
|
+
\t\t\tsignal: ctrl.signal,
|
|
4038
|
+
\t\t}
|
|
4039
|
+
\t}
|
|
4040
|
+
`;
|
|
4041
|
+
}
|
|
4042
|
+
function sdkClientRequestThrow() {
|
|
4043
|
+
return `
|
|
4044
|
+
\tasync #requestThrow(
|
|
4045
|
+
\t\tmethod: string,
|
|
4046
|
+
\t\tpath: string,
|
|
4047
|
+
\t\topts: _RequestOptions,
|
|
4048
|
+
\t\trequestMeta?: _RequestMeta,
|
|
4049
|
+
\t): Promise<unknown> {
|
|
4050
|
+
\t\tconst { done, response } = await this.#doRequest(method, path, opts, false, requestMeta)
|
|
4051
|
+
\t\ttry {
|
|
4052
|
+
\t\t\tif (!response.ok) throw await this.#parseAsClientError(response)
|
|
4053
|
+
\t\t\treturn await this.#parseBody(response)
|
|
4054
|
+
\t\t} finally {
|
|
4055
|
+
\t\t\tdone()
|
|
4056
|
+
\t\t}
|
|
4057
|
+
\t}
|
|
4058
|
+
`;
|
|
4059
|
+
}
|
|
4060
|
+
function sdkClientRequestSafe() {
|
|
4061
|
+
return `
|
|
4062
|
+
\tasync #requestSafe(
|
|
4063
|
+
\t\tmethod: string,
|
|
4064
|
+
\t\tpath: string,
|
|
4065
|
+
\t\topts: _RequestOptions,
|
|
4066
|
+
\t\trequestMeta?: _RequestMeta,
|
|
4067
|
+
\t): Promise<{
|
|
4068
|
+
\t\tdata: unknown
|
|
4069
|
+
\t\terror: unknown
|
|
4070
|
+
\t\tresponse: Response
|
|
4071
|
+
\t\tstatus: number
|
|
4072
|
+
\t}> {
|
|
4073
|
+
\t\tlet doResponse: { done: () => void; response: Response }
|
|
4074
|
+
\t\ttry {
|
|
4075
|
+
\t\t\tdoResponse = await this.#doRequest(method, path, opts, false, requestMeta)
|
|
4076
|
+
\t\t} catch (e) {
|
|
4077
|
+
\t\t\tif (e instanceof _ClientError) return { data: null, error: e, response: e.response, status: e.status }
|
|
4078
|
+
\t\t\tthrow e
|
|
4079
|
+
\t\t}
|
|
4080
|
+
\t\tconst { done, response } = doResponse
|
|
4081
|
+
\t\ttry {
|
|
4082
|
+
\t\t\tif (!response.ok) {
|
|
4083
|
+
\t\t\t\tconst preserved = response.clone()
|
|
4084
|
+
\t\t\t\tconst parsed = await this.#parseErrorBody(response)
|
|
4085
|
+
\t\t\t\t/* a failed request always has a truthy error: the JSON body, or a ClientError for any other body */
|
|
4086
|
+
\t\t\t\tconst error = parsed !== undefined && parsed !== null && parsed !== false && parsed !== "" && parsed !== 0
|
|
4087
|
+
\t\t\t\t\t? parsed
|
|
4088
|
+
\t\t\t\t\t: await this.#parseAsClientError(preserved.clone())
|
|
4089
|
+
\t\t\t\treturn { data: null, error, response: preserved, status: response.status }
|
|
4090
|
+
\t\t\t}
|
|
4091
|
+
\t\t\tlet data: unknown
|
|
4092
|
+
\t\t\ttry {
|
|
4093
|
+
\t\t\t\tdata = await this.#parseBody(response)
|
|
4094
|
+
\t\t\t} catch (e) {
|
|
4095
|
+
\t\t\t\treturn { data: null, error: e, response, status: response.status }
|
|
4096
|
+
\t\t\t}
|
|
4097
|
+
\t\t\treturn { data, error: null, response, status: response.status }
|
|
4098
|
+
\t\t} finally {
|
|
4099
|
+
\t\t\tdone()
|
|
4100
|
+
\t\t}
|
|
4101
|
+
\t}
|
|
4102
|
+
`;
|
|
4103
|
+
}
|
|
4104
|
+
function sdkClientRequest() {
|
|
4105
|
+
return `
|
|
4106
|
+
\tasync #request(entry: _ServiceEntry, input: Record<string, unknown>): Promise<unknown> {
|
|
4107
|
+
\t\tconst path = this.#toColonParams(entry.path, entry.wildcard)
|
|
4108
|
+
\t\tconst method = entry.method
|
|
4109
|
+
\t\t/* a copy: the caller's input object is never written to, so reusing it never reuses a key */
|
|
4110
|
+
\t\tconst opts = { ...input } as _RequestOptions
|
|
4111
|
+
\t\tif (entry.idempotent) {
|
|
4112
|
+
\t\t\tconst existing = opts.headers?.["Idempotency-Key"] ?? opts.headers?.["idempotency-key"]
|
|
4113
|
+
\t\t\tif (existing === undefined) {
|
|
4114
|
+
\t\t\t\tconst key = opts.idempotencyKey ?? this.#newRequestId()
|
|
4115
|
+
\t\t\t\topts.headers = { ...(opts.headers ?? {}), "Idempotency-Key": key }
|
|
4116
|
+
\t\t\t}
|
|
4117
|
+
\t\t}
|
|
4118
|
+
\t\tconst params = opts.params
|
|
4119
|
+
\t\tlet cp: string
|
|
4120
|
+
\t\ttry {
|
|
4121
|
+
\t\t\tcp = this.#interpolatePath(path, params)
|
|
4122
|
+
\t\t} catch (e) {
|
|
4123
|
+
\t\t\tif (!(e instanceof _PathParamError)) throw e
|
|
4124
|
+
\t\t\tconst err = new _ClientError({
|
|
4125
|
+
\t\t\t\tbody: null,
|
|
4126
|
+
\t\t\t\tdata: null,
|
|
4127
|
+
\t\t\t\tmessage: \`\${e.message} for \${method} \${entry.path}\`,
|
|
4128
|
+
\t\t\t\tresponse: Response.error(),
|
|
4129
|
+
\t\t\t\tstatus: 0,
|
|
4130
|
+
\t\t\t})
|
|
4131
|
+
\t\t\tif (!this.#config.throwOnError) return { data: null, error: err, response: err.response, status: 0 }
|
|
4132
|
+
\t\t\tthrow err
|
|
4133
|
+
\t\t}
|
|
4134
|
+
\t\tconst cs = \`\${method} \${cp}\`
|
|
4135
|
+
\t\tconst requestMeta = this.#buildRequestMeta(cs, cp, method)
|
|
4136
|
+
\t\tif (!this.#config.throwOnError) {
|
|
4137
|
+
\t\t\tconst r = await this.#requestSafe(method, path, opts, requestMeta)
|
|
4138
|
+
\t\t\tif (r.status >= 200 && r.status < 300) {
|
|
4139
|
+
\t\t\t\tthis.#markStale(entry.invalidate??[], params, cs)
|
|
4140
|
+
\t\t\t\tif (requestMeta?.isStale) this.#clearStale(cs, cp, method, requestMeta.seqSnapshot)
|
|
4141
|
+
\t\t\t}
|
|
4142
|
+
\t\t\treturn r
|
|
4143
|
+
\t\t}
|
|
4144
|
+
\t\tconst data = await this.#requestThrow(method, path, opts, requestMeta)
|
|
4145
|
+
\t\tthis.#markStale(entry.invalidate??[], params, cs)
|
|
4146
|
+
\t\tif (requestMeta?.isStale) this.#clearStale(cs, cp, method, requestMeta?.seqSnapshot ?? 0)
|
|
4147
|
+
\t\treturn data
|
|
4148
|
+
\t}
|
|
4149
|
+
`;
|
|
4150
|
+
}
|
|
4151
|
+
function sdkClientRequestSSE() {
|
|
4152
|
+
return `
|
|
4153
|
+
\t#requestSSE(entry: _ServiceEntry, path: string, opts: _RequestOptions): AsyncIterable<_SSEEvent> {
|
|
4154
|
+
\t\treturn { [Symbol.asyncIterator]: () => this.#doSSE(entry, path, opts) }
|
|
4155
|
+
\t}
|
|
4156
|
+
`;
|
|
4157
|
+
}
|
|
4158
|
+
function sdkClientConnectWS() {
|
|
4159
|
+
return `
|
|
4160
|
+
\t#connectWS(entry: _ServiceEntry, path: string, opts: _RequestOptions): _TypedWebSocket {
|
|
4161
|
+
\t\tconst built = new URL(this.#buildURL(path, opts))
|
|
4162
|
+
\t\t/* only the scheme changes: an https URL elsewhere in the query is left alone */
|
|
4163
|
+
\t\tif (built.protocol === "https:") built.protocol = "wss:"
|
|
4164
|
+
\t\telse if (built.protocol === "http:") built.protocol = "ws:"
|
|
4165
|
+
\t\tif (opts.reconnectToken) built.searchParams.append("reconnect_token", opts.reconnectToken)
|
|
4166
|
+
\t\tconst ws = this.#createTypedWebSocket(built.toString(), opts.protocols)
|
|
4167
|
+
\t\tif (entry.invalidate && entry.invalidate.length > 0) {
|
|
4168
|
+
\t\t\tconst invalidate = entry.invalidate
|
|
4169
|
+
\t\t\tconst params = opts.params
|
|
4170
|
+
\t\t\tconst cs = \`WS \${this.#interpolatePath(path, params)}\`
|
|
4171
|
+
\t\t\tws.on("open", () => this.#markStale(invalidate, params, cs))
|
|
4172
|
+
\t\t}
|
|
4173
|
+
\t\treturn ws
|
|
4174
|
+
\t}
|
|
4175
|
+
`;
|
|
4176
|
+
}
|
|
4177
|
+
function sdkClientBuildRequestMeta() {
|
|
4178
|
+
return `
|
|
4179
|
+
\t#buildRequestMeta(concreteSelector: string, concretePath: string, method: string): _RequestMeta | undefined {
|
|
4180
|
+
\t\tif (!this.#staleUntil) return undefined
|
|
4181
|
+
\t\tconst now = Date.now()
|
|
4182
|
+
\t\tconst { by, isStale } = this.#lookupStale(concreteSelector, concretePath, method, now)
|
|
4183
|
+
\t\treturn { invalidatedBy: by, isStale, selector: concreteSelector, seqSnapshot: this.#invalidationSeq }
|
|
4184
|
+
\t}
|
|
4185
|
+
`;
|
|
4186
|
+
}
|
|
4187
|
+
function sdkClientMarkStale() {
|
|
4188
|
+
return `
|
|
4189
|
+
\t#markStale(invalidate: readonly string[], params: Record<string, string> | undefined, mutationSelector: string): void {
|
|
4190
|
+
\t\tconst exactTable = this.#staleUntil
|
|
4191
|
+
\t\tif (!exactTable || invalidate.length === 0) return
|
|
4192
|
+
\t\tconst seq = ++this.#invalidationSeq
|
|
4193
|
+
\t\tconst until = Date.now() + this.#staleTime
|
|
4194
|
+
\t\tfor (const target of this.#resolveInvalidationTargets(invalidate, params)) {
|
|
4195
|
+
\t\t\tconst spaceIdx = target.indexOf(" ")
|
|
4196
|
+
\t\t\tconst method = target.slice(0, spaceIdx)
|
|
4197
|
+
\t\t\tconst isPattern = this.#hasPlaceholder(target.slice(spaceIdx + 1))
|
|
4198
|
+
\t\t\tlet table: Map<string, _StaleEntry>
|
|
4199
|
+
\t\t\tif (isPattern) {
|
|
4200
|
+
\t\t\t\ttable = this.#stalePatterns.get(method) ?? new Map<string, _StaleEntry>()
|
|
4201
|
+
\t\t\t\tthis.#stalePatterns.set(method, table)
|
|
4202
|
+
\t\t\t} else {
|
|
4203
|
+
\t\t\t\ttable = exactTable
|
|
4204
|
+
\t\t\t}
|
|
4205
|
+
\t\t\tconst existing = table.get(target)
|
|
4206
|
+
\t\t\tif (existing) {
|
|
4207
|
+
\t\t\t\tif (!existing.by.includes(mutationSelector)) {
|
|
4208
|
+
\t\t\t\t\texisting.by.push(mutationSelector)
|
|
4209
|
+
\t\t\t\t\tif (existing.by.length > this.#maxSourcesPerTarget) existing.by.shift()
|
|
4210
|
+
\t\t\t\t}
|
|
4211
|
+
\t\t\t\texisting.until = until
|
|
4212
|
+
\t\t\t\texisting.seq = seq
|
|
4213
|
+
\t\t\t\t/* a new mutation re-marks every instance */
|
|
4214
|
+
\t\t\t\texisting.refreshed?.clear()
|
|
4215
|
+
\t\t\t} else {
|
|
4216
|
+
\t\t\t\ttable.set(target, { by: [mutationSelector], refreshed: isPattern ? new Set<string>() : undefined, seq, until })
|
|
4217
|
+
\t\t\t}
|
|
4218
|
+
\t\t}
|
|
4219
|
+
\t\tif (exactTable.size > this.#staleMaxEntries) {
|
|
4220
|
+
\t\t\tconst now = Date.now()
|
|
4221
|
+
\t\t\tfor (const [k, entry] of exactTable) if (entry.until <= now) exactTable.delete(k)
|
|
4222
|
+
\t\t\t/* still over: drop the oldest marks */
|
|
4223
|
+
\t\t\tfor (const k of exactTable.keys()) {
|
|
4224
|
+
\t\t\t\tif (exactTable.size <= this.#staleMaxEntries) break
|
|
4225
|
+
\t\t\t\texactTable.delete(k)
|
|
4226
|
+
\t\t\t}
|
|
4227
|
+
\t\t}
|
|
4228
|
+
\t}
|
|
4229
|
+
`;
|
|
4230
|
+
}
|
|
4231
|
+
function sdkClientClearStale() {
|
|
4232
|
+
return `
|
|
4233
|
+
\t/* a successful stale read clears that resource only: its exact mark, and this path's share of a pattern mark */
|
|
4234
|
+
\t#clearStale(concreteSelector: string, concretePath: string, method: string, seqSnapshot: number): void {
|
|
4235
|
+
\t\tif (!this.#staleUntil) return
|
|
4236
|
+
\t\tconst exact = this.#staleUntil.get(concreteSelector)
|
|
4237
|
+
\t\tif (exact && exact.seq <= seqSnapshot) this.#staleUntil.delete(concreteSelector)
|
|
4238
|
+
\t\tconst table = this.#stalePatterns.get(method)
|
|
4239
|
+
\t\tif (!table) return
|
|
4240
|
+
\t\tfor (const [key, entry] of table) {
|
|
4241
|
+
\t\t\tif (entry.seq > seqSnapshot || !entry.refreshed) continue
|
|
4242
|
+
\t\t\tif (!this.#pathMatchesPattern(concretePath, key.slice(key.indexOf(" ") + 1))) continue
|
|
4243
|
+
\t\t\tif (entry.refreshed.size >= 1024) {
|
|
4244
|
+
\t\t\t\tconst oldest = entry.refreshed.values().next().value
|
|
4245
|
+
\t\t\t\tif (oldest !== undefined) entry.refreshed.delete(oldest)
|
|
4246
|
+
\t\t\t}
|
|
4247
|
+
\t\t\tentry.refreshed.add(concretePath)
|
|
4248
|
+
\t\t}
|
|
4249
|
+
\t}
|
|
4250
|
+
`;
|
|
4251
|
+
}
|
|
4252
|
+
function sdkClientDispose() {
|
|
4253
|
+
return `
|
|
4254
|
+
\tdispose(): void {
|
|
4255
|
+
\t\tif (this.#disposed) return
|
|
4256
|
+
\t\tthis.#disposed = true
|
|
4257
|
+
\t\tthis.#disposeCtrl.abort()
|
|
4258
|
+
\t\tthis.#staleUntil?.clear()
|
|
4259
|
+
\t\tthis.#stalePatterns.clear()
|
|
4260
|
+
\t\tthis.#resourceCache.clear()
|
|
4261
|
+
\t\tthis.#patternRegexCache.clear()
|
|
4262
|
+
\t}
|
|
4263
|
+
`;
|
|
4264
|
+
}
|
|
4265
|
+
function sdkClientDoSSE() {
|
|
4266
|
+
return `
|
|
4267
|
+
\tasync *#doSSE(entry: _ServiceEntry, path: string, opts: _RequestOptions): AsyncGenerator<_SSEEvent> {
|
|
4268
|
+
\t\tconst method = entry.method
|
|
4269
|
+
\t\tconst url = this.#buildURL(path, opts)
|
|
4270
|
+
\t\tconst headers = await this.#buildHeaders(opts, { method, path })
|
|
4271
|
+
\t\theaders.set("accept", "text/event-stream")
|
|
4272
|
+
\t\tif (opts.lastEventId) {
|
|
4273
|
+
\t\t\theaders.set("last-event-id", opts.lastEventId)
|
|
4274
|
+
\t\t}
|
|
4275
|
+
\t\t/* a streamed operation carries its body like any other request */
|
|
4276
|
+
\t\tlet body = this.#buildBody(opts, headers)
|
|
4277
|
+
\t\tconst { signal, cleanup } = this.#buildSignal(opts, true)
|
|
4278
|
+
\t\ttry {
|
|
4279
|
+
\t\t\tif (this.#config.onRequest) {
|
|
4280
|
+
\t\t\t\tconst reqCtx: { body?: BodyInit; headers: Headers; invalidatedBy?: string[]; isStale?: boolean; method: string; path: string; selector?: string; state: Record<string, unknown>; url: string } = { body, headers, method, path, state: this.#config.state ?? {}, url }
|
|
4281
|
+
\t\t\t\tfor (const hook of this.#config.onRequest) {
|
|
4282
|
+
\t\t\t\t\tawait hook(reqCtx)
|
|
4283
|
+
\t\t\t\t}
|
|
4284
|
+
\t\t\t\tif (reqCtx.body !== body) body = reqCtx.body
|
|
4285
|
+
\t\t\t}
|
|
4286
|
+
\t\t\tconst sseInit = this.#requestInit({ body, headers, method, signal })
|
|
4287
|
+
\t\t\tif (this.#config.credentials) sseInit.credentials = this.#config.credentials
|
|
4288
|
+
\t\t\tif (this.#config.mode) sseInit.mode = this.#config.mode
|
|
4289
|
+
\t\t\tsignal?.throwIfAborted()
|
|
4290
|
+
\t\t\tlet response = await this.#fetchFn(url, sseInit)
|
|
4291
|
+
\t\t\tif (this.#config.onResponse) {
|
|
4292
|
+
\t\t\t\tconst resCtx: { invalidatedBy?: string[]; isRetry: boolean; isStale?: boolean; method: string; path: string; request: Request; response: Response; retry: () => Promise<Response>; selector?: string; state: Record<string, unknown>; url: string } = {
|
|
4293
|
+
\t\t\t\t\tisRetry: false, method, path, request: this.#hookRequest(url, sseInit), response,
|
|
4294
|
+
\t\t\t\t\tretry: () => { throw new Error("SSE streams do not support retry") },
|
|
4295
|
+
\t\t\t\t\tstate: this.#config.state ?? {}, url,
|
|
4296
|
+
\t\t\t\t}
|
|
4297
|
+
\t\t\t\tfor (const hook of this.#config.onResponse) {
|
|
4298
|
+
\t\t\t\t\tconst result = await hook(resCtx)
|
|
4299
|
+
\t\t\t\t\tif (result instanceof Response) { response = result; resCtx.response = result }
|
|
4300
|
+
\t\t\t\t}
|
|
4301
|
+
\t\t\t}
|
|
4302
|
+
\t\t\tif (!response.ok) {
|
|
4303
|
+
\t\t\t\tthrow await this.#parseAsClientError(response)
|
|
4304
|
+
\t\t\t}
|
|
4305
|
+
\t\t\tif (entry.invalidate && entry.invalidate.length > 0) {
|
|
4306
|
+
\t\t\t\tconst cs = \`\${method} \${this.#interpolatePath(path, opts.params)}\`
|
|
4307
|
+
\t\t\t\tthis.#markStale(entry.invalidate, opts.params, cs)
|
|
4308
|
+
\t\t\t}
|
|
4309
|
+
\t\t\tif (!response.body) return
|
|
4310
|
+
\t\t\tyield* this.#parseSSE(response.body)
|
|
4311
|
+
\t\t} finally {
|
|
4312
|
+
\t\t\tcleanup()
|
|
4313
|
+
\t\t}
|
|
4314
|
+
\t}
|
|
4315
|
+
`;
|
|
4316
|
+
}
|
|
4317
|
+
function sdkClientParseSSEBlock() {
|
|
4318
|
+
return `
|
|
4319
|
+
\t/* Keep in sync with client/sse.ts — the WHATWG event-stream rules: lines end at CRLF, LF or CR;
|
|
4320
|
+
\t a leading BOM is skipped; id is sticky and ignored if it holds NUL; retry is digits only; an
|
|
4321
|
+
\t event still open when the stream ends is discarded, never dispatched. */
|
|
4322
|
+
\tasync *#parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<_SSEEvent> {
|
|
4323
|
+
\t\tconst decoder = new TextDecoder()
|
|
4324
|
+
\t\tconst reader = stream.getReader()
|
|
4325
|
+
\t\tconst maxBuffer = this.#sseMaxBufferChars
|
|
4326
|
+
\t\tlet pending = ""
|
|
4327
|
+
\t\tlet first = true
|
|
4328
|
+
\t\tlet data: string[] = []
|
|
4329
|
+
\t\tlet dataSize = 0
|
|
4330
|
+
\t\tlet hasData = false
|
|
4331
|
+
\t\tlet event: string | undefined
|
|
4332
|
+
\t\tlet lastId = ""
|
|
4333
|
+
\t\tlet retry: number | undefined
|
|
4334
|
+
\t\tconst dispatch = (): _SSEEvent | undefined => {
|
|
4335
|
+
\t\t\tconst out: _SSEEvent | undefined = hasData ? { data: data.join("\\n") } : undefined
|
|
4336
|
+
\t\t\tif (out) {
|
|
4337
|
+
\t\t\t\tif (event !== undefined && event !== "") out.event = event
|
|
4338
|
+
\t\t\t\tif (lastId !== "") out.id = lastId
|
|
4339
|
+
\t\t\t\tif (retry !== undefined) out.retry = retry
|
|
4340
|
+
\t\t\t\tretry = undefined
|
|
4341
|
+
\t\t\t}
|
|
4342
|
+
\t\t\tdata = []
|
|
4343
|
+
\t\t\tdataSize = 0
|
|
4344
|
+
\t\t\thasData = false
|
|
4345
|
+
\t\t\tevent = undefined
|
|
4346
|
+
\t\t\treturn out
|
|
4347
|
+
\t\t}
|
|
4348
|
+
\t\tconst processLine = (line: string): void => {
|
|
4349
|
+
\t\t\tif (line.charCodeAt(0) === 58) return
|
|
4350
|
+
\t\t\tconst colon = line.indexOf(":")
|
|
4351
|
+
\t\t\tconst field = colon === -1 ? line : line.slice(0, colon)
|
|
4352
|
+
\t\t\tlet value = colon === -1 ? "" : line.slice(colon + 1)
|
|
4353
|
+
\t\t\tif (value.charCodeAt(0) === 32) value = value.slice(1)
|
|
4354
|
+
\t\t\tswitch (field) {
|
|
4355
|
+
\t\t\t\tcase "data":
|
|
4356
|
+
\t\t\t\t\tdataSize += value.length + 1
|
|
4357
|
+
\t\t\t\t\tif (dataSize > maxBuffer) throw new Error(\`SSE event exceeded \${maxBuffer} characters\`)
|
|
4358
|
+
\t\t\t\t\tdata.push(value)
|
|
4359
|
+
\t\t\t\t\thasData = true
|
|
4360
|
+
\t\t\t\t\tbreak
|
|
4361
|
+
\t\t\t\tcase "event":
|
|
4362
|
+
\t\t\t\t\tevent = value
|
|
4363
|
+
\t\t\t\t\tbreak
|
|
4364
|
+
\t\t\t\tcase "id":
|
|
4365
|
+
\t\t\t\t\tif (!value.includes("\\0")) lastId = value
|
|
4366
|
+
\t\t\t\t\tbreak
|
|
4367
|
+
\t\t\t\tcase "retry":
|
|
4368
|
+
\t\t\t\t\tif (/^\\d+$/.test(value)) retry = Number(value)
|
|
4369
|
+
\t\t\t\t\tbreak
|
|
4370
|
+
\t\t\t}
|
|
4371
|
+
\t\t}
|
|
4372
|
+
\t\ttry {
|
|
4373
|
+
\t\t\twhile (true) {
|
|
4374
|
+
\t\t\t\tconst { done, value } = await reader.read()
|
|
4375
|
+
\t\t\t\tif (done) break
|
|
4376
|
+
\t\t\t\tpending += decoder.decode(value, { stream: true })
|
|
4377
|
+
\t\t\t\tif (first && pending.length > 0) {
|
|
4378
|
+
\t\t\t\t\tif (pending.charCodeAt(0) === 0xfeff) pending = pending.slice(1)
|
|
4379
|
+
\t\t\t\t\tfirst = false
|
|
4380
|
+
\t\t\t\t}
|
|
4381
|
+
\t\t\t\tlet start = 0
|
|
4382
|
+
\t\t\t\twhile (start < pending.length) {
|
|
4383
|
+
\t\t\t\t\tlet end = start
|
|
4384
|
+
\t\t\t\t\twhile (end < pending.length) {
|
|
4385
|
+
\t\t\t\t\t\tconst c = pending.charCodeAt(end)
|
|
4386
|
+
\t\t\t\t\t\tif (c === 10 || c === 13) break
|
|
4387
|
+
\t\t\t\t\t\tend++
|
|
4388
|
+
\t\t\t\t\t}
|
|
4389
|
+
\t\t\t\t\tif (end === pending.length) break
|
|
4390
|
+
\t\t\t\t\t/* a CR as the last character may be the first half of CRLF */
|
|
4391
|
+
\t\t\t\t\tif (pending.charCodeAt(end) === 13 && end === pending.length - 1) break
|
|
4392
|
+
\t\t\t\t\tconst line = pending.slice(start, end)
|
|
4393
|
+
\t\t\t\t\tstart = end + (pending.charCodeAt(end) === 13 && pending.charCodeAt(end + 1) === 10 ? 2 : 1)
|
|
4394
|
+
\t\t\t\t\tif (line === "") {
|
|
4395
|
+
\t\t\t\t\t\tconst out = dispatch()
|
|
4396
|
+
\t\t\t\t\t\tif (out) yield out
|
|
4397
|
+
\t\t\t\t\t} else {
|
|
4398
|
+
\t\t\t\t\t\tprocessLine(line)
|
|
4399
|
+
\t\t\t\t\t}
|
|
4400
|
+
\t\t\t\t}
|
|
4401
|
+
\t\t\t\tpending = pending.slice(start)
|
|
4402
|
+
\t\t\t\tif (pending.length > maxBuffer) throw new Error(\`SSE buffer exceeded \${maxBuffer} characters\`)
|
|
4403
|
+
\t\t\t}
|
|
4404
|
+
\t\t\t/* a lone CR held back at the end of the stream still terminates its line */
|
|
4405
|
+
\t\t\tif (pending === "\\r") {
|
|
4406
|
+
\t\t\t\tconst out = dispatch()
|
|
4407
|
+
\t\t\t\tif (out) yield out
|
|
4408
|
+
\t\t\t}
|
|
4409
|
+
\t\t\t/* anything else still open is an incomplete event: discard it */
|
|
4410
|
+
\t\t} finally {
|
|
4411
|
+
\t\t\tawait reader.cancel().catch(() => {})
|
|
4412
|
+
\t\t\treader.releaseLock()
|
|
4413
|
+
\t\t}
|
|
4414
|
+
\t}
|
|
4415
|
+
`;
|
|
4416
|
+
}
|
|
4417
|
+
function sdkClientFooter() {
|
|
4418
|
+
const reexports = ["\t_ClientError as ClientError,"]
|
|
4419
|
+
.concat(STATUS_ERROR_CLASSES.map(({ name }) => `\t_${name} as ${name},`))
|
|
4420
|
+
.join("\n");
|
|
4421
|
+
return `}
|
|
4422
|
+
|
|
4423
|
+
export {
|
|
4424
|
+
${reexports}
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
export function isClientError(e: unknown): e is _ClientError {
|
|
4428
|
+
\treturn e instanceof _ClientError
|
|
4429
|
+
}
|
|
4430
|
+
`;
|
|
4431
|
+
}
|
|
4432
|
+
/** The IR every TypeScript SDK emitter reads: refs inlined, derived ids, duplicate ids refused. */
|
|
4433
|
+
export function sdkIR(spec) {
|
|
4434
|
+
return toIR(resolveRefs(spec), { deriveOperationIds: true, duplicateOperationIds: "throw" });
|
|
4435
|
+
}
|
|
4436
|
+
/** Flat `resource → action → entry` map of every operation, for SDK result objects. */
|
|
4437
|
+
export function serviceMapOf(ir) {
|
|
4438
|
+
/* null-prototype: an operationId of "__proto__.x" must not reach Object.prototype */
|
|
4439
|
+
const serviceMap = Object.create(null);
|
|
4440
|
+
for (const op of ir.operations) {
|
|
4441
|
+
const segments = op.id.split(".");
|
|
4442
|
+
const resource = segments.length === 1 ? op.id : (segments[0] ?? op.id);
|
|
4443
|
+
const action = segments.length === 1 ? "_call" : segments.slice(1).join(".");
|
|
4444
|
+
if (!Object.hasOwn(serviceMap, resource))
|
|
4445
|
+
serviceMap[resource] = Object.create(null);
|
|
4446
|
+
serviceMap[resource][action] = buildServiceEntryForOp(op);
|
|
4447
|
+
}
|
|
4448
|
+
return serviceMap;
|
|
4449
|
+
}
|
|
4450
|
+
export function collectSDKMethods(spec,
|
|
4451
|
+
/* toIR validates namespace collisions; an operation without an operationId gets a derived
|
|
4452
|
+
one instead of silently vanishing from the SDK */
|
|
4453
|
+
ir = sdkIR(spec)) {
|
|
4454
|
+
const resolve = irResolver(ir.schemas);
|
|
4455
|
+
const methods = [];
|
|
4456
|
+
for (const op of ir.operations) {
|
|
4457
|
+
const operationId = op.id;
|
|
4458
|
+
const segments = operationId.split(".");
|
|
4459
|
+
const isTopLevel = segments.length === 1;
|
|
4460
|
+
const resource = isTopLevel ? operationId : (segments[0] ?? operationId);
|
|
4461
|
+
const action = isTopLevel ? "_call" : segments.slice(1).join(".");
|
|
4462
|
+
const inputResult = emitSDKInputType(op);
|
|
4463
|
+
methods.push({
|
|
4464
|
+
action,
|
|
4465
|
+
errorsByStatusType: emitSDKErrorsByStatusType(op, resolve),
|
|
4466
|
+
id: operationId,
|
|
4467
|
+
inputHasMandatory: inputResult.hasMandatory,
|
|
4468
|
+
inputType: inputResult.type,
|
|
4469
|
+
realtime: op.extensions.realtime === true,
|
|
4470
|
+
resource,
|
|
4471
|
+
responseType: emitSDKResponseType(op),
|
|
4472
|
+
sse: op.extensions.sse === true,
|
|
4473
|
+
ws: op.extensions.websocket === true,
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
const nestedMap = buildNestedServiceMap(safeMemberTree(ir.tree, true));
|
|
4477
|
+
return { methods, nestedMap, serviceMap: serviceMapOf(ir) };
|
|
4478
|
+
}
|
|
4479
|
+
function buildServiceEntryForOp(op) {
|
|
4480
|
+
const params = openApiPathParams(op.path);
|
|
4481
|
+
const entry = { method: op.method, path: op.path };
|
|
4482
|
+
if (params.length > 0)
|
|
4483
|
+
entry.params = params;
|
|
4484
|
+
const wildcard = op.params.path.find((p) => p.wildcard === true);
|
|
4485
|
+
if (wildcard)
|
|
4486
|
+
entry.wildcard = wildcard.name;
|
|
4487
|
+
if (op.extensions.sse)
|
|
4488
|
+
entry.sse = true;
|
|
4489
|
+
if (op.extensions.websocket)
|
|
4490
|
+
entry.ws = true;
|
|
4491
|
+
if (op.extensions.realtime)
|
|
4492
|
+
entry.realtime = true;
|
|
4493
|
+
if (op.extensions.idempotencyKey)
|
|
4494
|
+
entry.idempotent = true;
|
|
4495
|
+
const inv = op.extensions.invalidates;
|
|
4496
|
+
if (inv && inv.length > 0)
|
|
4497
|
+
entry.invalidate = inv;
|
|
4498
|
+
return entry;
|
|
4499
|
+
}
|
|
4500
|
+
/** Members a generated client already has; a resource of the same name would hide them. */
|
|
4501
|
+
const SDK_ROOT_MEMBERS = new Set(["dispose", "state"]);
|
|
4502
|
+
/**
|
|
4503
|
+
* Names JavaScript machinery reads off any object: a `then` member makes the client a
|
|
4504
|
+
* thenable, `toString` breaks `String(sdk)`, `__proto__` sets the prototype.
|
|
4505
|
+
*/
|
|
4506
|
+
const SDK_OBJECT_MEMBERS = new Set([
|
|
4507
|
+
"__defineGetter__",
|
|
4508
|
+
"__defineSetter__",
|
|
4509
|
+
"__lookupGetter__",
|
|
4510
|
+
"__lookupSetter__",
|
|
4511
|
+
"__proto__",
|
|
4512
|
+
"constructor",
|
|
4513
|
+
"hasOwnProperty",
|
|
4514
|
+
"isPrototypeOf",
|
|
4515
|
+
"propertyIsEnumerable",
|
|
4516
|
+
"then",
|
|
4517
|
+
"toJSON",
|
|
4518
|
+
"toLocaleString",
|
|
4519
|
+
"toString",
|
|
4520
|
+
"valueOf",
|
|
4521
|
+
]);
|
|
4522
|
+
/**
|
|
4523
|
+
* The resource tree with every member name a client can expose. A name that would shadow a
|
|
4524
|
+
* client member or an `Object.prototype` name gets a trailing `_` (`state` → `state_`), the
|
|
4525
|
+
* same rename in the interface and the service map, so types and runtime agree.
|
|
4526
|
+
*/
|
|
4527
|
+
function safeMemberTree(ns, root) {
|
|
4528
|
+
const entries = new Map();
|
|
4529
|
+
const sorted = [...ns.entries.entries()].sort(([a], [b]) => compareCodeUnits(a, b));
|
|
4530
|
+
for (const [key, entry] of sorted) {
|
|
4531
|
+
let name = key;
|
|
4532
|
+
if ((root && SDK_ROOT_MEMBERS.has(key)) || SDK_OBJECT_MEMBERS.has(key)) {
|
|
4533
|
+
name = `${key}_`;
|
|
4534
|
+
while (ns.entries.has(name) || entries.has(name))
|
|
4535
|
+
name += "_";
|
|
4536
|
+
}
|
|
4537
|
+
entries.set(name, entry.kind === "namespace" ? { kind: "namespace", ns: safeMemberTree(entry.ns, false) } : entry);
|
|
4538
|
+
}
|
|
4539
|
+
return { entries };
|
|
4540
|
+
}
|
|
4541
|
+
/**
|
|
4542
|
+
* The member path of every operation on a generated TypeScript client, after the renames
|
|
4543
|
+
* `safeMemberTree` applies: `["state_", "get"]` for an operation `state.get`. A namespace whose
|
|
4544
|
+
* only method is `_call` is callable itself, so its path ends at the namespace.
|
|
4545
|
+
*/
|
|
4546
|
+
export function sdkMemberPaths(ir) {
|
|
4547
|
+
const out = new Map();
|
|
4548
|
+
const walk = (ns, prefix) => {
|
|
4549
|
+
const methods = methodsOf(ns);
|
|
4550
|
+
for (const [key, entry] of ns.entries) {
|
|
4551
|
+
if (entry.kind === "namespace") {
|
|
4552
|
+
walk(entry.ns, [...prefix, key]);
|
|
4553
|
+
}
|
|
4554
|
+
else if (key !== "_call") {
|
|
4555
|
+
out.set(entry.op.id, [...prefix, key]);
|
|
4556
|
+
}
|
|
4557
|
+
else if (methods.length === 1) {
|
|
4558
|
+
/* the interface promotes a lone `_call` to the namespace itself and drops any other */
|
|
4559
|
+
out.set(entry.op.id, prefix);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
};
|
|
4563
|
+
walk(safeMemberTree(ir.tree, true), []);
|
|
4564
|
+
return out;
|
|
4565
|
+
}
|
|
4566
|
+
function buildNestedServiceMap(ns) {
|
|
4567
|
+
function walkNs(namespace) {
|
|
4568
|
+
const map = new Map();
|
|
4569
|
+
for (const [key, entry] of [...namespace.entries.entries()].sort(([a], [b]) => compareCodeUnits(a, b))) {
|
|
4570
|
+
if (entry.kind === "method")
|
|
4571
|
+
map.set(key, { entry: buildServiceEntryForOp(entry.op), kind: "leaf" });
|
|
4572
|
+
else
|
|
4573
|
+
map.set(key, { children: walkNs(entry.ns), kind: "ns" });
|
|
4574
|
+
}
|
|
4575
|
+
return map;
|
|
4576
|
+
}
|
|
4577
|
+
return walkNs(ns);
|
|
4578
|
+
}
|
|
4579
|
+
const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
4580
|
+
/** The SDK class name lands in source, the stem in import specifiers and file names. */
|
|
4581
|
+
function validateSdkNames(name, stem) {
|
|
4582
|
+
if (!TS_IDENTIFIER.test(name) || SDK_TS_RESERVED.has(name)) {
|
|
4583
|
+
throw new Error(`generateSDK: name ${JSON.stringify(name)} is not a TypeScript identifier`);
|
|
4584
|
+
}
|
|
4585
|
+
if (!/^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(stem)) {
|
|
4586
|
+
throw new Error(`generateSDK: stem ${JSON.stringify(stem)} must be a plain file name (letters, digits, ".", "_", "-")`);
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
export function generateSDK(spec, options) {
|
|
4590
|
+
const sdkName = options?.name ?? "SDK";
|
|
4591
|
+
const stem = options?.stem ?? "sdk";
|
|
4592
|
+
validateSdkNames(sdkName, stem);
|
|
4593
|
+
const ir = sdkIR(spec);
|
|
4594
|
+
const { methods: sdkMethods, nestedMap, serviceMap } = collectSDKMethods(spec, ir);
|
|
4595
|
+
const hasRealtime = sdkMethods.some((m) => m.realtime);
|
|
4596
|
+
const fullMethodLookup = new Map();
|
|
4597
|
+
for (const m of sdkMethods)
|
|
4598
|
+
fullMethodLookup.set(m.id, m);
|
|
4599
|
+
return {
|
|
4600
|
+
files: {
|
|
4601
|
+
client: buildSDKClient(sdkName, stem),
|
|
4602
|
+
index: buildSDKIndex(sdkName, stem),
|
|
4603
|
+
map: buildSDKMap(nestedMap),
|
|
4604
|
+
runtime: hasRealtime ? buildSDKRuntime() : null,
|
|
4605
|
+
types: buildSDKTypes(sdkName, sdkMethods, safeMemberTree(ir.tree, true), fullMethodLookup),
|
|
4606
|
+
},
|
|
4607
|
+
serviceMap,
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
//# sourceMappingURL=codegen.js.map
|