ts-procedures 7.3.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -3
- package/agent_config/claude-code/agents/ts-procedures-architect.md +6 -8
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +30 -33
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +104 -53
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +205 -232
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +80 -153
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +4 -5
- package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +4 -7
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono.md +223 -0
- package/agent_config/copilot/copilot-instructions.md +34 -48
- package/agent_config/cursor/cursorrules +34 -48
- package/build/client/call.js +4 -1
- package/build/client/call.js.map +1 -1
- package/build/client/call.test.js +23 -0
- package/build/client/call.test.js.map +1 -1
- package/build/client/fetch-adapter.js +3 -1
- package/build/client/fetch-adapter.js.map +1 -1
- package/build/client/fetch-adapter.test.js +11 -1
- package/build/client/fetch-adapter.test.js.map +1 -1
- package/build/client/index.test.js +7 -7
- package/build/client/index.test.js.map +1 -1
- package/build/client/request-builder.d.ts +1 -1
- package/build/client/request-builder.js +2 -2
- package/build/client/request-builder.js.map +1 -1
- package/build/client/stream.js +13 -2
- package/build/client/stream.js.map +1 -1
- package/build/client/stream.test.js +32 -7
- package/build/client/stream.test.js.map +1 -1
- package/build/client/typed-error-dispatch.test.js +8 -92
- package/build/client/typed-error-dispatch.test.js.map +1 -1
- package/build/client/types.d.ts +21 -3
- package/build/codegen/bin/cli.js +0 -0
- package/build/codegen/e2e.test.js +87 -23
- package/build/codegen/e2e.test.js.map +1 -1
- package/build/codegen/emit-errors.integration.test.js +1 -1
- package/build/codegen/emit-errors.integration.test.js.map +1 -1
- package/build/codegen/emit-scope.js +308 -47
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/emit-scope.test.js +363 -110
- package/build/codegen/emit-scope.test.js.map +1 -1
- package/build/codegen/pipeline.test.js +7 -7
- package/build/codegen/pipeline.test.js.map +1 -1
- package/build/codegen/resolve-envelope.js +1 -1
- package/build/codegen/resolve-envelope.js.map +1 -1
- package/build/codegen/resolve-envelope.test.js +5 -5
- package/build/codegen/resolve-envelope.test.js.map +1 -1
- package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
- package/build/codegen/targets/_shared/route-slots.js +49 -8
- package/build/codegen/targets/_shared/route-slots.js.map +1 -1
- package/build/codegen/targets/_shared/route-slots.test.js +99 -26
- package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/integration.test.js +6 -0
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
- package/build/codegen/targets/swift/access-level.test.js +8 -11
- package/build/codegen/targets/swift/access-level.test.js.map +1 -1
- package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
- package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
- package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
- package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
- package/build/codegen/targets/swift/integration.test.js +6 -0
- package/build/codegen/targets/swift/integration.test.js.map +1 -1
- package/build/create-http-stream.d.ts +58 -0
- package/build/create-http-stream.js +122 -0
- package/build/create-http-stream.js.map +1 -0
- package/build/create-http-stream.test.js +88 -0
- package/build/create-http-stream.test.js.map +1 -0
- package/build/create-http.d.ts +49 -0
- package/build/create-http.js +108 -0
- package/build/create-http.js.map +1 -0
- package/build/create-http.test.js +137 -0
- package/build/create-http.test.js.map +1 -0
- package/build/create-stream.d.ts +35 -0
- package/build/create-stream.js +123 -0
- package/build/create-stream.js.map +1 -0
- package/build/create-stream.test.js +428 -0
- package/build/create-stream.test.js.map +1 -0
- package/build/create.d.ts +28 -0
- package/build/create.js +82 -0
- package/build/create.js.map +1 -0
- package/build/create.test.js +483 -0
- package/build/create.test.js.map +1 -0
- package/build/exports.d.ts +2 -0
- package/build/implementations/http/astro/index.test.js +20 -12
- package/build/implementations/http/astro/index.test.js.map +1 -1
- package/build/implementations/http/doc-registry.js +1 -1
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +36 -5
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-dispatch.d.ts +76 -0
- package/build/implementations/http/error-dispatch.js +77 -0
- package/build/implementations/http/error-dispatch.js.map +1 -0
- package/build/implementations/http/error-dispatch.test.js +254 -0
- package/build/implementations/http/error-dispatch.test.js.map +1 -0
- package/build/implementations/http/error-taxonomy.d.ts +5 -5
- package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/http-doc.js +42 -0
- package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
- package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
- package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/stream-doc.js +42 -0
- package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
- package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
- package/build/implementations/http/hono/handlers/http-stream.js +123 -0
- package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
- package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
- package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/http.d.ts +10 -0
- package/build/implementations/http/hono/handlers/http.js +115 -0
- package/build/implementations/http/hono/handlers/http.js.map +1 -0
- package/build/implementations/http/hono/handlers/http.test.js +118 -0
- package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
- package/build/implementations/http/hono/handlers/rpc.js +32 -0
- package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
- package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
- package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
- package/build/implementations/http/hono/handlers/stream.js +147 -0
- package/build/implementations/http/hono/handlers/stream.js.map +1 -0
- package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
- package/build/implementations/http/hono/handlers/stream.test.js +177 -0
- package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
- package/build/implementations/http/hono/index.d.ts +57 -0
- package/build/implementations/http/hono/index.js +149 -0
- package/build/implementations/http/hono/index.js.map +1 -0
- package/build/implementations/http/hono/index.test.d.ts +1 -0
- package/build/implementations/http/hono/index.test.js +274 -0
- package/build/implementations/http/hono/index.test.js.map +1 -0
- package/build/implementations/http/hono/path.d.ts +17 -0
- package/build/implementations/http/hono/path.js +39 -0
- package/build/implementations/http/hono/path.js.map +1 -0
- package/build/implementations/http/hono/path.test.d.ts +1 -0
- package/build/implementations/http/hono/path.test.js +83 -0
- package/build/implementations/http/hono/path.test.js.map +1 -0
- package/build/implementations/http/hono/types.d.ts +51 -0
- package/build/implementations/http/hono/types.js.map +1 -0
- package/build/implementations/http/on-request-error.test.js +6 -96
- package/build/implementations/http/on-request-error.test.js.map +1 -1
- package/build/implementations/http/route-errors.test.js +11 -59
- package/build/implementations/http/route-errors.test.js.map +1 -1
- package/build/implementations/types.d.ts +43 -9
- package/build/index.d.ts +124 -124
- package/build/index.js +10 -221
- package/build/index.js.map +1 -1
- package/build/index.test.js +20 -919
- package/build/index.test.js.map +1 -1
- package/build/migration.test.d.ts +1 -0
- package/build/migration.test.js +34 -0
- package/build/migration.test.js.map +1 -0
- package/build/schema/compute-schema.d.ts +11 -3
- package/build/schema/compute-schema.js +13 -7
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +11 -3
- package/build/schema/parser.js +49 -9
- package/build/schema/parser.js.map +1 -1
- package/build/stack-utils.js +8 -0
- package/build/stack-utils.js.map +1 -1
- package/build/types.d.ts +142 -0
- package/build/types.js.map +1 -0
- package/docs/astro-adapter.md +5 -5
- package/docs/core.md +15 -17
- package/docs/http-integrations.md +83 -170
- package/docs/streaming.md +3 -60
- package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
- package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
- package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
- package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
- package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
- package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
- package/package.json +4 -22
- package/src/client/call.test.ts +26 -0
- package/src/client/call.ts +4 -1
- package/src/client/fetch-adapter.test.ts +14 -1
- package/src/client/fetch-adapter.ts +3 -1
- package/src/client/index.test.ts +7 -7
- package/src/client/request-builder.ts +2 -2
- package/src/client/stream.test.ts +39 -7
- package/src/client/stream.ts +16 -2
- package/src/client/typed-error-dispatch.test.ts +7 -97
- package/src/client/types.ts +21 -3
- package/src/codegen/__fixtures__/users-envelope.json +119 -38
- package/src/codegen/e2e.test.ts +98 -24
- package/src/codegen/emit-errors.integration.test.ts +1 -1
- package/src/codegen/emit-scope.test.ts +395 -110
- package/src/codegen/emit-scope.ts +350 -55
- package/src/codegen/pipeline.test.ts +7 -7
- package/src/codegen/resolve-envelope.test.ts +5 -5
- package/src/codegen/resolve-envelope.ts +1 -1
- package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
- package/src/codegen/targets/_shared/route-slots.ts +48 -11
- package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
- package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
- package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
- package/src/codegen/targets/kotlin/integration.test.ts +19 -0
- package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
- package/src/codegen/targets/swift/access-level.test.ts +8 -11
- package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
- package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
- package/src/codegen/targets/swift/integration.test.ts +17 -0
- package/src/create-http-stream.test.ts +97 -0
- package/src/create-http-stream.ts +191 -0
- package/src/create-http.test.ts +163 -0
- package/src/create-http.ts +211 -0
- package/src/create-stream.test.ts +565 -0
- package/src/create-stream.ts +228 -0
- package/src/create.test.ts +658 -0
- package/src/create.ts +172 -0
- package/src/exports.ts +2 -0
- package/src/implementations/http/README.md +135 -95
- package/src/implementations/http/astro/README.md +4 -5
- package/src/implementations/http/astro/index.test.ts +25 -18
- package/src/implementations/http/doc-registry.test.ts +42 -5
- package/src/implementations/http/doc-registry.ts +1 -1
- package/src/implementations/http/error-dispatch.test.ts +283 -0
- package/src/implementations/http/error-dispatch.ts +176 -0
- package/src/implementations/http/error-taxonomy.ts +5 -5
- package/src/implementations/http/hono/docs/http-doc.ts +43 -0
- package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
- package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
- package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
- package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
- package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
- package/src/implementations/http/hono/handlers/http.test.ts +130 -0
- package/src/implementations/http/hono/handlers/http.ts +147 -0
- package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
- package/src/implementations/http/hono/handlers/rpc.ts +54 -0
- package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
- package/src/implementations/http/hono/handlers/stream.ts +208 -0
- package/src/implementations/http/hono/index.test.ts +329 -0
- package/src/implementations/http/hono/index.ts +204 -0
- package/src/implementations/http/hono/path.test.ts +96 -0
- package/src/implementations/http/hono/path.ts +59 -0
- package/src/implementations/http/hono/types.ts +93 -0
- package/src/implementations/http/on-request-error.test.ts +10 -116
- package/src/implementations/http/route-errors.test.ts +11 -77
- package/src/implementations/types.ts +44 -9
- package/src/index.test.ts +22 -1249
- package/src/index.ts +49 -485
- package/src/migration.test.ts +48 -0
- package/src/schema/compute-schema.ts +26 -12
- package/src/schema/parser.ts +62 -12
- package/src/stack-utils.ts +8 -0
- package/src/types.ts +133 -0
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/express-rpc/index.d.ts +0 -125
- package/build/implementations/http/express-rpc/index.js +0 -216
- package/build/implementations/http/express-rpc/index.js.map +0 -1
- package/build/implementations/http/express-rpc/index.test.js +0 -684
- package/build/implementations/http/express-rpc/index.test.js.map +0 -1
- package/build/implementations/http/express-rpc/types.d.ts +0 -11
- package/build/implementations/http/express-rpc/types.js.map +0 -1
- package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-api/index.d.ts +0 -151
- package/build/implementations/http/hono-api/index.js +0 -344
- package/build/implementations/http/hono-api/index.js.map +0 -1
- package/build/implementations/http/hono-api/index.test.js +0 -992
- package/build/implementations/http/hono-api/index.test.js.map +0 -1
- package/build/implementations/http/hono-api/types.d.ts +0 -13
- package/build/implementations/http/hono-api/types.js.map +0 -1
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-rpc/index.d.ts +0 -130
- package/build/implementations/http/hono-rpc/index.js +0 -209
- package/build/implementations/http/hono-rpc/index.js.map +0 -1
- package/build/implementations/http/hono-rpc/index.test.js +0 -828
- package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
- package/build/implementations/http/hono-rpc/types.d.ts +0 -11
- package/build/implementations/http/hono-rpc/types.js +0 -2
- package/build/implementations/http/hono-rpc/types.js.map +0 -1
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-stream/index.d.ts +0 -171
- package/build/implementations/http/hono-stream/index.js +0 -415
- package/build/implementations/http/hono-stream/index.js.map +0 -1
- package/build/implementations/http/hono-stream/index.test.js +0 -1383
- package/build/implementations/http/hono-stream/index.test.js.map +0 -1
- package/build/implementations/http/hono-stream/types.d.ts +0 -15
- package/build/implementations/http/hono-stream/types.js +0 -2
- package/build/implementations/http/hono-stream/types.js.map +0 -1
- package/src/implementations/http/express-rpc/README.md +0 -280
- package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
- package/src/implementations/http/express-rpc/index.test.ts +0 -957
- package/src/implementations/http/express-rpc/index.ts +0 -327
- package/src/implementations/http/express-rpc/types.ts +0 -16
- package/src/implementations/http/hono-api/README.md +0 -284
- package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
- package/src/implementations/http/hono-api/index.test.ts +0 -1341
- package/src/implementations/http/hono-api/index.ts +0 -519
- package/src/implementations/http/hono-api/types.ts +0 -16
- package/src/implementations/http/hono-rpc/README.md +0 -357
- package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
- package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
- package/src/implementations/http/hono-rpc/index.ts +0 -320
- package/src/implementations/http/hono-rpc/types.ts +0 -16
- package/src/implementations/http/hono-stream/README.md +0 -559
- package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
- package/src/implementations/http/hono-stream/index.test.ts +0 -1804
- package/src/implementations/http/hono-stream/index.ts +0 -622
- package/src/implementations/http/hono-stream/types.ts +0 -20
- /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
- /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
- /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
- /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
- /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
- /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
|
@@ -28,23 +28,20 @@ const envelope: DocEnvelope = {
|
|
|
28
28
|
method: 'get',
|
|
29
29
|
path: '/things/:id',
|
|
30
30
|
fullPath: '/things/:id',
|
|
31
|
-
jsonSchema: {
|
|
32
|
-
|
|
33
|
-
// `route.schema.returnType` directly; mirror what the codegen
|
|
34
|
-
// pipeline downstream expects rather than the doc-builder
|
|
35
|
-
// `jsonSchema` shape.
|
|
36
|
-
schema: {
|
|
37
|
-
input: {
|
|
31
|
+
jsonSchema: {
|
|
32
|
+
req: {
|
|
38
33
|
pathParams: {
|
|
39
34
|
type: 'object',
|
|
40
35
|
properties: { id: { type: 'string' } },
|
|
41
36
|
required: ['id'],
|
|
42
37
|
},
|
|
43
38
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
res: {
|
|
40
|
+
body: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: { id: { type: 'string' } },
|
|
43
|
+
required: ['id'],
|
|
44
|
+
},
|
|
48
45
|
},
|
|
49
46
|
},
|
|
50
47
|
errors: [],
|
|
@@ -34,11 +34,9 @@ describe('emitSwiftRoute', () => {
|
|
|
34
34
|
name: 'GetUser',
|
|
35
35
|
method: 'GET',
|
|
36
36
|
fullPath: '/users/:id',
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
returnType: { type: 'object' },
|
|
37
|
+
jsonSchema: {
|
|
38
|
+
req: { pathParams: { type: 'object' } },
|
|
39
|
+
res: { body: { type: 'object' } },
|
|
42
40
|
},
|
|
43
41
|
errors: [],
|
|
44
42
|
} as unknown as AnyHttpRouteDoc
|
|
@@ -64,7 +62,10 @@ describe('emitSwiftRoute', () => {
|
|
|
64
62
|
name: 'CreateUser',
|
|
65
63
|
method: 'POST',
|
|
66
64
|
fullPath: '/users',
|
|
67
|
-
|
|
65
|
+
jsonSchema: {
|
|
66
|
+
req: { body: { type: 'object' } },
|
|
67
|
+
res: { body: { type: 'object' } },
|
|
68
|
+
},
|
|
68
69
|
errors: [],
|
|
69
70
|
} as unknown as AnyHttpRouteDoc
|
|
70
71
|
|
|
@@ -84,7 +85,10 @@ describe('emitSwiftRoute', () => {
|
|
|
84
85
|
name: 'GetUser',
|
|
85
86
|
method: 'GET',
|
|
86
87
|
fullPath: '/users/:id',
|
|
87
|
-
|
|
88
|
+
jsonSchema: {
|
|
89
|
+
req: { pathParams: { type: 'object' } },
|
|
90
|
+
res: { body: { type: 'object' } },
|
|
91
|
+
},
|
|
88
92
|
errors: ['NotFound'],
|
|
89
93
|
} as unknown as AnyHttpRouteDoc
|
|
90
94
|
|
|
@@ -103,14 +107,14 @@ describe('emitSwiftRoute', () => {
|
|
|
103
107
|
it('silently skips error keys with no schema in the envelope map', () => {
|
|
104
108
|
const route = {
|
|
105
109
|
kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users',
|
|
106
|
-
|
|
110
|
+
jsonSchema: {}, errors: ['UnknownTaxonomyKey'],
|
|
107
111
|
} as unknown as AnyHttpRouteDoc
|
|
108
112
|
const result = emitSwiftRoute(route, createStubSwiftEmitter({}), new Map())
|
|
109
113
|
expect(result.code).not.toContain('enum Errors {')
|
|
110
114
|
})
|
|
111
115
|
|
|
112
116
|
it('returns skipped:true for stream routes', () => {
|
|
113
|
-
const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream',
|
|
117
|
+
const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
|
|
114
118
|
const result = emitSwiftRoute(route, createStubSwiftEmitter({}), noErrors)
|
|
115
119
|
expect(result.code).toBe('')
|
|
116
120
|
expect(result.skipped).toBe(true)
|
|
@@ -122,9 +126,9 @@ describe('emitSwiftRoute', () => {
|
|
|
122
126
|
name: 'GetUser',
|
|
123
127
|
method: 'GET',
|
|
124
128
|
fullPath: '/users/:id',
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
jsonSchema: {
|
|
130
|
+
req: { pathParams: { type: 'object' } },
|
|
131
|
+
res: { body: { type: 'object' } },
|
|
128
132
|
},
|
|
129
133
|
errors: ['NotFound'],
|
|
130
134
|
} as unknown as AnyHttpRouteDoc
|
|
@@ -157,13 +161,13 @@ describe('emitSwiftRoute', () => {
|
|
|
157
161
|
name: 'X',
|
|
158
162
|
method: 'POST',
|
|
159
163
|
fullPath: '/x/:id',
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
jsonSchema: {
|
|
165
|
+
req: {
|
|
162
166
|
pathParams: { type: 'object' },
|
|
163
167
|
query: { type: 'object' },
|
|
164
168
|
body: { type: 'object' },
|
|
165
169
|
},
|
|
166
|
-
|
|
170
|
+
res: { body: { type: 'object' } },
|
|
167
171
|
},
|
|
168
172
|
errors: ['Z'],
|
|
169
173
|
} as unknown as AnyHttpRouteDoc
|
|
@@ -189,7 +193,7 @@ describe('emitSwiftRoute', () => {
|
|
|
189
193
|
name: 'X',
|
|
190
194
|
method: 'GET',
|
|
191
195
|
fullPath: '/x',
|
|
192
|
-
|
|
196
|
+
jsonSchema: { res: { body: { type: 'object' } } },
|
|
193
197
|
errors: [],
|
|
194
198
|
} as unknown as AnyHttpRouteDoc
|
|
195
199
|
|
|
@@ -209,13 +213,92 @@ describe('emitSwiftRoute', () => {
|
|
|
209
213
|
expect(calls[0]!.opts.uncountableWords).toEqual(['data', 'metadata'])
|
|
210
214
|
})
|
|
211
215
|
|
|
216
|
+
it('emits ResponseHeaders slot when res.headers is declared on an api route', () => {
|
|
217
|
+
const route = {
|
|
218
|
+
kind: 'api',
|
|
219
|
+
name: 'DownloadUser',
|
|
220
|
+
method: 'GET',
|
|
221
|
+
fullPath: '/users/:id/download',
|
|
222
|
+
jsonSchema: {
|
|
223
|
+
req: { pathParams: { type: 'object' } },
|
|
224
|
+
res: {
|
|
225
|
+
body: { type: 'object' },
|
|
226
|
+
headers: { type: 'object' },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
errors: [],
|
|
230
|
+
} as unknown as AnyHttpRouteDoc
|
|
231
|
+
|
|
232
|
+
const { emitter, calls } = makeSpyEmitter({
|
|
233
|
+
PathParams: ok('public struct PathParams: Codable { public let id: String }', 'PathParams'),
|
|
234
|
+
Response: ok('public struct Response: Codable { public let url: String }', 'Response'),
|
|
235
|
+
ResponseHeaders: ok(
|
|
236
|
+
'public struct ResponseHeaders: Codable { public let xDownloadToken: String }',
|
|
237
|
+
'ResponseHeaders',
|
|
238
|
+
),
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const result = emitSwiftRoute(route, emitter, noErrors)
|
|
242
|
+
expect(calls.map((c) => c.opts.rootTypeName)).toEqual(['PathParams', 'Response', 'ResponseHeaders'])
|
|
243
|
+
expect(result.code).toContain('public struct ResponseHeaders: Codable { public let xDownloadToken: String }')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('does not emit ResponseHeaders when res.headers is absent', () => {
|
|
247
|
+
const route = {
|
|
248
|
+
kind: 'api',
|
|
249
|
+
name: 'GetUser',
|
|
250
|
+
method: 'GET',
|
|
251
|
+
fullPath: '/users/:id',
|
|
252
|
+
jsonSchema: {
|
|
253
|
+
req: { pathParams: { type: 'object' } },
|
|
254
|
+
res: { body: { type: 'object' } },
|
|
255
|
+
},
|
|
256
|
+
errors: [],
|
|
257
|
+
} as unknown as AnyHttpRouteDoc
|
|
258
|
+
|
|
259
|
+
const { emitter, calls } = makeSpyEmitter({
|
|
260
|
+
PathParams: ok('public struct PathParams: Codable { public let id: String }', 'PathParams'),
|
|
261
|
+
Response: ok('public struct Response: Codable { public let id: String }', 'Response'),
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
emitSwiftRoute(route, emitter, noErrors)
|
|
265
|
+
expect(calls.map((c) => c.opts.rootTypeName)).not.toContain('ResponseHeaders')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('processes http-stream routes (not skipped) and emits Yield/ReturnType slots', () => {
|
|
269
|
+
const route = {
|
|
270
|
+
kind: 'http-stream',
|
|
271
|
+
name: 'TailLogs',
|
|
272
|
+
method: 'GET',
|
|
273
|
+
fullPath: '/logs/tail',
|
|
274
|
+
jsonSchema: {
|
|
275
|
+
req: { query: { type: 'object' } },
|
|
276
|
+
yield: { type: 'object' },
|
|
277
|
+
returnType: { type: 'object' },
|
|
278
|
+
},
|
|
279
|
+
errors: [],
|
|
280
|
+
} as unknown as AnyHttpRouteDoc
|
|
281
|
+
|
|
282
|
+
const { emitter, calls } = makeSpyEmitter({
|
|
283
|
+
Query: ok('public struct Query: Codable { public let filter: String? }', 'Query'),
|
|
284
|
+
Yield: ok('public struct Yield: Codable { public let line: String }', 'Yield'),
|
|
285
|
+
ReturnType: ok('public struct ReturnType: Codable { public let count: Int }', 'ReturnType'),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const result = emitSwiftRoute(route, emitter, noErrors)
|
|
289
|
+
expect(result.skipped).toBeFalsy()
|
|
290
|
+
expect(calls.map((c) => c.opts.rootTypeName)).toEqual(['Query', 'Yield', 'ReturnType'])
|
|
291
|
+
expect(result.code).toContain('public static let method = "GET"')
|
|
292
|
+
expect(result.code).toContain('public static let path = "/logs/tail"')
|
|
293
|
+
})
|
|
294
|
+
|
|
212
295
|
it('does not include passthrough keys when caller omits them', () => {
|
|
213
296
|
const route = {
|
|
214
297
|
kind: 'api',
|
|
215
298
|
name: 'X',
|
|
216
299
|
method: 'GET',
|
|
217
300
|
fullPath: '/x',
|
|
218
|
-
|
|
301
|
+
jsonSchema: { res: { body: { type: 'object' } } },
|
|
219
302
|
errors: [],
|
|
220
303
|
} as unknown as AnyHttpRouteDoc
|
|
221
304
|
|
|
@@ -246,7 +329,7 @@ describe('emitSwiftRoute', () => {
|
|
|
246
329
|
name: 'GetUser',
|
|
247
330
|
method: 'GET',
|
|
248
331
|
fullPath: '/users/:id',
|
|
249
|
-
|
|
332
|
+
jsonSchema: { req: { pathParams: { type: 'object' } } },
|
|
250
333
|
errors: [],
|
|
251
334
|
} as unknown as AnyHttpRouteDoc
|
|
252
335
|
|
|
@@ -265,7 +348,7 @@ describe('emitSwiftRoute', () => {
|
|
|
265
348
|
name: 'GetUser',
|
|
266
349
|
method: 'GET',
|
|
267
350
|
fullPath: '/users/:id',
|
|
268
|
-
|
|
351
|
+
jsonSchema: { req: { pathParams: { type: 'object' } } },
|
|
269
352
|
errors: ['NotFound'],
|
|
270
353
|
} as unknown as AnyHttpRouteDoc
|
|
271
354
|
|
|
@@ -291,7 +374,7 @@ describe('emitSwiftRoute', () => {
|
|
|
291
374
|
name: 'CreateUser',
|
|
292
375
|
method: 'POST',
|
|
293
376
|
fullPath: '/users',
|
|
294
|
-
|
|
377
|
+
jsonSchema: {},
|
|
295
378
|
errors: [],
|
|
296
379
|
} as unknown as AnyHttpRouteDoc
|
|
297
380
|
const result = emitSwiftRoute(route, createStubSwiftEmitter({}), noErrors, { accessLevel: 'internal' })
|
|
@@ -18,7 +18,10 @@ describe('emitSwiftScope', () => {
|
|
|
18
18
|
name: 'GetUser',
|
|
19
19
|
method: 'GET',
|
|
20
20
|
fullPath: '/users/:id',
|
|
21
|
-
|
|
21
|
+
jsonSchema: {
|
|
22
|
+
req: { pathParams: { type: 'object' } },
|
|
23
|
+
res: { body: { type: 'object' } },
|
|
24
|
+
},
|
|
22
25
|
errors: [],
|
|
23
26
|
} as unknown as AnyHttpRouteDoc
|
|
24
27
|
|
|
@@ -41,8 +44,8 @@ describe('emitSwiftScope', () => {
|
|
|
41
44
|
})
|
|
42
45
|
|
|
43
46
|
it('joins multiple routes inside one scope enum, separated by a blank line', () => {
|
|
44
|
-
const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id',
|
|
45
|
-
const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users',
|
|
47
|
+
const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
|
|
48
|
+
const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
|
|
46
49
|
const group: ScopeGroup = { scopeKey: 'users', camelCase: 'users', routes: [route1, route2] }
|
|
47
50
|
const emitter = createStubSwiftEmitter({})
|
|
48
51
|
|
|
@@ -74,8 +77,8 @@ describe('emitSwiftScope', () => {
|
|
|
74
77
|
})
|
|
75
78
|
|
|
76
79
|
it('dedupes and sorts imports across all routes in the scope', () => {
|
|
77
|
-
const route1 = { kind: 'api', name: 'A', method: 'GET', fullPath: '/a',
|
|
78
|
-
const route2 = { kind: 'api', name: 'B', method: 'GET', fullPath: '/b',
|
|
80
|
+
const route1 = { kind: 'api', name: 'A', method: 'GET', fullPath: '/a', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
|
|
81
|
+
const route2 = { kind: 'api', name: 'B', method: 'GET', fullPath: '/b', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
|
|
79
82
|
const group: ScopeGroup = { scopeKey: 'multi', camelCase: 'multi', routes: [route1, route2] }
|
|
80
83
|
const emitter = {
|
|
81
84
|
emit(_s: unknown, opts: SwiftEmitOptions): SwiftEmitResult {
|
|
@@ -93,7 +96,7 @@ describe('emitSwiftScope', () => {
|
|
|
93
96
|
})
|
|
94
97
|
|
|
95
98
|
it('threads all 6 passthrough opts to every emitter call', () => {
|
|
96
|
-
const route = { kind: 'api', name: 'X', method: 'GET', fullPath: '/x',
|
|
99
|
+
const route = { kind: 'api', name: 'X', method: 'GET', fullPath: '/x', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
|
|
97
100
|
const group: ScopeGroup = { scopeKey: 'x', camelCase: 'x', routes: [route] }
|
|
98
101
|
|
|
99
102
|
const calls: SwiftEmitOptions[] = []
|
|
@@ -130,7 +133,7 @@ describe('emitSwiftScope', () => {
|
|
|
130
133
|
})
|
|
131
134
|
|
|
132
135
|
it('applies accessLevel to the outer scope enum and the route enum wrappers', () => {
|
|
133
|
-
const route = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users',
|
|
136
|
+
const route = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
|
|
134
137
|
const group: ScopeGroup = { scopeKey: 'users', camelCase: 'users', routes: [route] }
|
|
135
138
|
const emitter = createStubSwiftEmitter({})
|
|
136
139
|
|
|
@@ -142,8 +145,8 @@ describe('emitSwiftScope', () => {
|
|
|
142
145
|
})
|
|
143
146
|
|
|
144
147
|
it('collects skipped stream-route names', () => {
|
|
145
|
-
const stream = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/u/stream',
|
|
146
|
-
const api = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/u',
|
|
148
|
+
const stream = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/u/stream', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
|
|
149
|
+
const api = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/u', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
|
|
147
150
|
const group: ScopeGroup = { scopeKey: 'u', camelCase: 'u', routes: [stream, api] }
|
|
148
151
|
|
|
149
152
|
const emitter = createStubSwiftEmitter({
|
|
@@ -62,6 +62,23 @@ describe('swift codegen — integration', () => {
|
|
|
62
62
|
'public struct Query: Codable {\n public let status: Status?\n public let limit: Int64?\n\n public enum Status: String, Codable {\n case active\n case inactive\n }\n}',
|
|
63
63
|
'Query',
|
|
64
64
|
),
|
|
65
|
+
|
|
66
|
+
// DownloadUser
|
|
67
|
+
ResponseHeaders: ok(
|
|
68
|
+
'public struct ResponseHeaders: Codable {\n public let xDownloadToken: String\n public let contentDisposition: String?\n\n enum CodingKeys: String, CodingKey {\n case xDownloadToken = "x-download-token"\n case contentDisposition = "content-disposition"\n }\n}',
|
|
69
|
+
'ResponseHeaders',
|
|
70
|
+
),
|
|
71
|
+
|
|
72
|
+
// WatchUsers (http-stream) — reuses Query and ResponseHeaders stubs from above;
|
|
73
|
+
// adds Yield and ReturnType which are unique to the stream route.
|
|
74
|
+
Yield: ok(
|
|
75
|
+
'public struct Yield: Codable {\n public let id: String\n public let event: Event\n\n public enum Event: String, Codable {\n case created\n case updated\n case deleted\n }\n}',
|
|
76
|
+
'Yield',
|
|
77
|
+
),
|
|
78
|
+
ReturnType: ok(
|
|
79
|
+
'public struct ReturnType: Codable {\n public let count: Int64\n}',
|
|
80
|
+
'ReturnType',
|
|
81
|
+
),
|
|
65
82
|
})
|
|
66
83
|
|
|
67
84
|
const files = await runPipeline({
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { Procedures } from './index.js'
|
|
3
|
+
import { Type } from 'typebox'
|
|
4
|
+
import { ProcedureValidationError } from './errors.js'
|
|
5
|
+
|
|
6
|
+
describe('CreateHttpStream basic generator', () => {
|
|
7
|
+
it('registers with kind: http-stream', () => {
|
|
8
|
+
const procs = Procedures()
|
|
9
|
+
procs.CreateHttpStream('Tail', {
|
|
10
|
+
path: '/streams/logs', method: 'get',
|
|
11
|
+
schema: {
|
|
12
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
13
|
+
yield: Type.Object({ line: Type.String() }),
|
|
14
|
+
},
|
|
15
|
+
}, async function* () { yield { line: 'a' } })
|
|
16
|
+
|
|
17
|
+
expect(procs.getProcedure('Tail')?.kind).toBe('http-stream')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('yields and returns', async () => {
|
|
21
|
+
const procs = Procedures()
|
|
22
|
+
const { Tail } = procs.CreateHttpStream('Tail', {
|
|
23
|
+
path: '/streams/logs', method: 'get',
|
|
24
|
+
schema: {
|
|
25
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
26
|
+
yield: Type.Object({ line: Type.String() }),
|
|
27
|
+
returnType: Type.Object({ totalLines: Type.Number() }),
|
|
28
|
+
},
|
|
29
|
+
}, async function* (_, { query }) {
|
|
30
|
+
yield { line: `from-${query.source}-1` }
|
|
31
|
+
yield { line: `from-${query.source}-2` }
|
|
32
|
+
return { totalLines: 2 }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const lines: string[] = []
|
|
36
|
+
let returnVal: unknown
|
|
37
|
+
const { stream } = await Tail({} as any, { query: { source: 'app' } })
|
|
38
|
+
let result = await stream.next()
|
|
39
|
+
while (!result.done) {
|
|
40
|
+
lines.push(result.value.line)
|
|
41
|
+
result = await stream.next()
|
|
42
|
+
}
|
|
43
|
+
returnVal = result.value
|
|
44
|
+
expect(lines).toEqual(['from-app-1', 'from-app-2'])
|
|
45
|
+
expect(returnVal).toEqual({ totalLines: 2 })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('rejects req.query validation error before opening stream', async () => {
|
|
49
|
+
const procs = Procedures()
|
|
50
|
+
const { Tail } = procs.CreateHttpStream('Tail', {
|
|
51
|
+
path: '/streams/logs', method: 'get',
|
|
52
|
+
schema: {
|
|
53
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
54
|
+
yield: Type.Object({ line: Type.String() }),
|
|
55
|
+
},
|
|
56
|
+
}, async function* () { yield { line: 'should not run' } })
|
|
57
|
+
|
|
58
|
+
// Two independent async calls — each rejects before a stream is produced.
|
|
59
|
+
await expect(Tail({} as any, { query: {} as any })).rejects.toThrow(ProcedureValidationError)
|
|
60
|
+
await expect(Tail({} as any, { query: {} as any })).rejects.toThrow(/req\.query/)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('CreateHttpStream async-preamble shape (res.headers)', () => {
|
|
65
|
+
it('handler returning Promise<{ headers, stream }> surfaces headers in initialHeaders', async () => {
|
|
66
|
+
const procs = Procedures()
|
|
67
|
+
const { Tail } = procs.CreateHttpStream('Tail', {
|
|
68
|
+
path: '/streams/logs', method: 'get',
|
|
69
|
+
schema: {
|
|
70
|
+
req: { query: Type.Object({ source: Type.String() }) },
|
|
71
|
+
yield: Type.Object({ line: Type.String() }),
|
|
72
|
+
res: { headers: Type.Object({ 'x-stream-id': Type.String() }) },
|
|
73
|
+
},
|
|
74
|
+
}, async (_, { query }) => ({
|
|
75
|
+
headers: { 'x-stream-id': `id-${query.source}` },
|
|
76
|
+
stream: (async function* () { yield { line: 'a' } })(),
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
const { stream, initialHeaders } = await Tail({} as any, { query: { source: 'app' } })
|
|
80
|
+
expect(initialHeaders).toEqual({ 'x-stream-id': 'id-app' })
|
|
81
|
+
const first = await stream.next()
|
|
82
|
+
expect(first.value).toEqual({ line: 'a' })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('throws when handler returns malformed shape', async () => {
|
|
86
|
+
const procs = Procedures()
|
|
87
|
+
const { Bad } = procs.CreateHttpStream('Bad', {
|
|
88
|
+
path: '/streams', method: 'get',
|
|
89
|
+
schema: {
|
|
90
|
+
yield: Type.Number(),
|
|
91
|
+
res: { headers: Type.Object({}) },
|
|
92
|
+
},
|
|
93
|
+
}, (async () => 'not-a-shape') as any)
|
|
94
|
+
|
|
95
|
+
await expect(Bad({} as any, undefined)).rejects.toThrow(/did not resolve to/)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { ProcedureError, ProcedureRegistrationError, ProcedureValidationError, ProcedureYieldValidationError } from './errors.js'
|
|
2
|
+
import { computeSchema } from './schema/compute-schema.js'
|
|
3
|
+
import { Prettify, TSchemaLib } from './schema/types.js'
|
|
4
|
+
import { captureDefinitionInfo } from './stack-utils.js'
|
|
5
|
+
import {
|
|
6
|
+
HttpMethod, TBuilderConfig, TStreamContext,
|
|
7
|
+
THttpStreamProcedureRegistration, TProcedureRegistration, TStreamProcedureRegistration, THttpProcedureRegistration,
|
|
8
|
+
} from './types.js'
|
|
9
|
+
import { checkPathParamConsistency } from './create-http.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wraps a user's AsyncIterator in a generator that:
|
|
13
|
+
* - optionally validates each yielded value
|
|
14
|
+
* - augments error stack traces with the procedure definition site
|
|
15
|
+
* - aborts the stream controller on completion
|
|
16
|
+
*/
|
|
17
|
+
async function* yieldingWrapper(
|
|
18
|
+
userIterator: AsyncIterator<any, any, unknown>,
|
|
19
|
+
validateYields: boolean,
|
|
20
|
+
validations: { yield?: (value: any) => { errors?: any[] } },
|
|
21
|
+
abortController: AbortController,
|
|
22
|
+
definitionInfo: ReturnType<typeof captureDefinitionInfo>,
|
|
23
|
+
name: string,
|
|
24
|
+
): AsyncGenerator<any, any, unknown> {
|
|
25
|
+
try {
|
|
26
|
+
let result = await userIterator.next()
|
|
27
|
+
while (!result.done) {
|
|
28
|
+
const value = result.value
|
|
29
|
+
if (validateYields && validations.yield) {
|
|
30
|
+
const { errors } = validations.yield(value)
|
|
31
|
+
if (errors) throw new ProcedureYieldValidationError(name, `Yield validation error for ${name}`, errors, definitionInfo)
|
|
32
|
+
}
|
|
33
|
+
yield value
|
|
34
|
+
result = await userIterator.next()
|
|
35
|
+
}
|
|
36
|
+
return result.value
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
if (definitionInfo.definedAt && error && typeof error.stack === 'string') {
|
|
39
|
+
const { file, line, column } = definitionInfo.definedAt
|
|
40
|
+
error.stack = `${error.stack}\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
|
|
41
|
+
}
|
|
42
|
+
throw error
|
|
43
|
+
} finally {
|
|
44
|
+
try { await userIterator.return?.(undefined) } catch { /* swallow */ }
|
|
45
|
+
abortController.abort('stream-completed')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function makeCreateHttpStream<TContext>(
|
|
50
|
+
procedures: Map<string, TProcedureRegistration<TContext, any> | TStreamProcedureRegistration<TContext, any> | THttpProcedureRegistration<TContext> | THttpStreamProcedureRegistration<TContext>>,
|
|
51
|
+
builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
|
|
52
|
+
) {
|
|
53
|
+
return function CreateHttpStream<
|
|
54
|
+
TName extends string,
|
|
55
|
+
TReq extends Record<string, unknown> | undefined,
|
|
56
|
+
TYieldType,
|
|
57
|
+
TReturnType = void,
|
|
58
|
+
TResHeaders = undefined,
|
|
59
|
+
TErrorKey extends string = string,
|
|
60
|
+
>(
|
|
61
|
+
name: TName,
|
|
62
|
+
config: {
|
|
63
|
+
path: string
|
|
64
|
+
method: HttpMethod
|
|
65
|
+
scope?: string
|
|
66
|
+
errors?: TErrorKey[]
|
|
67
|
+
description?: string
|
|
68
|
+
schema: {
|
|
69
|
+
req?: TReq
|
|
70
|
+
yield?: TYieldType
|
|
71
|
+
returnType?: TReturnType
|
|
72
|
+
res?: TResHeaders extends undefined ? undefined : { headers: TResHeaders }
|
|
73
|
+
}
|
|
74
|
+
validateYields?: boolean
|
|
75
|
+
},
|
|
76
|
+
handler: TResHeaders extends undefined
|
|
77
|
+
? (
|
|
78
|
+
ctx: Prettify<TContext & TStreamContext>,
|
|
79
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
80
|
+
) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
|
|
81
|
+
: (
|
|
82
|
+
ctx: Prettify<TContext & TStreamContext>,
|
|
83
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
84
|
+
) => Promise<{ headers: TSchemaLib<TResHeaders>; stream: AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown> }>,
|
|
85
|
+
) {
|
|
86
|
+
const definitionInfo = captureDefinitionInfo()
|
|
87
|
+
|
|
88
|
+
if (procedures.has(name)) throw new Error(`Procedure with name ${name} is already registered`)
|
|
89
|
+
if ((config.schema as any)?.params) {
|
|
90
|
+
throw new ProcedureRegistrationError(name, `Use schema.req.body (or schema.req.query) instead of schema.params on CreateHttpStream. Procedure: "${name}".`, definitionInfo)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { jsonSchema, validations } = computeSchema(name, {
|
|
94
|
+
req: config.schema.req as Record<string, unknown> | undefined,
|
|
95
|
+
yieldType: config.schema.yield,
|
|
96
|
+
returnType: config.schema.returnType,
|
|
97
|
+
}, definitionInfo)
|
|
98
|
+
|
|
99
|
+
const pathParamsSchema = (jsonSchema.req as Record<string, unknown> | undefined)?.pathParams
|
|
100
|
+
checkPathParamConsistency(name, config.path, pathParamsSchema as Record<string, unknown> | undefined, definitionInfo)
|
|
101
|
+
|
|
102
|
+
const errorFactory = (message: string, meta?: object) => new ProcedureError(name, message, meta, definitionInfo)
|
|
103
|
+
const validateYields = config.validateYields ?? false
|
|
104
|
+
|
|
105
|
+
const wrappedHandler = async (ctx: TContext, req: any): Promise<{ stream: AsyncGenerator<any, any, unknown>; initialHeaders?: Record<string, string> }> => {
|
|
106
|
+
const abortController = new AbortController()
|
|
107
|
+
const skipValidation = (ctx as { isPrevalidated?: boolean }).isPrevalidated || builder?.config?.noRuntimeValidation
|
|
108
|
+
|
|
109
|
+
if (validations?.req && !skipValidation) {
|
|
110
|
+
for (const [channel, validator] of Object.entries(validations.req)) {
|
|
111
|
+
const channelValue = (req as Record<string, unknown>)?.[channel]
|
|
112
|
+
const { errors } = validator(channelValue)
|
|
113
|
+
if (errors) {
|
|
114
|
+
throw new ProcedureValidationError(name, `Validation error for ${name} in req.${channel}`, errors, definitionInfo)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const incomingSignal = (ctx as { signal?: AbortSignal }).signal
|
|
120
|
+
const signal = incomingSignal ? AbortSignal.any([incomingSignal, abortController.signal]) : abortController.signal
|
|
121
|
+
const streamCtx: TStreamContext = { error: errorFactory, signal }
|
|
122
|
+
|
|
123
|
+
const handlerResult: any = (handler as any)({ ...ctx, ...streamCtx }, req)
|
|
124
|
+
|
|
125
|
+
if (typeof handlerResult?.[Symbol.asyncIterator] === 'function') {
|
|
126
|
+
// Plain async generator from user
|
|
127
|
+
const userIterator = (handlerResult as AsyncGenerator<any, any, unknown>)[Symbol.asyncIterator]()
|
|
128
|
+
return { stream: yieldingWrapper(userIterator, validateYields, validations, abortController, definitionInfo, name) }
|
|
129
|
+
} else if (typeof handlerResult?.then === 'function') {
|
|
130
|
+
// Async preamble: Promise<{ headers, stream }>
|
|
131
|
+
const resolved = await handlerResult
|
|
132
|
+
if (!resolved || typeof resolved !== 'object' || !('stream' in resolved)) {
|
|
133
|
+
throw new ProcedureError(name, `CreateHttpStream handler returned a Promise that did not resolve to { headers, stream }. Got: ${JSON.stringify(resolved)}`, undefined, definitionInfo)
|
|
134
|
+
}
|
|
135
|
+
const userIterator = (resolved.stream as AsyncGenerator<any, any, unknown>)[Symbol.asyncIterator]()
|
|
136
|
+
return {
|
|
137
|
+
stream: yieldingWrapper(userIterator, validateYields, validations, abortController, definitionInfo, name),
|
|
138
|
+
initialHeaders: resolved.headers,
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
throw new ProcedureError(name, `CreateHttpStream handler must return an AsyncGenerator or Promise<{ headers, stream }>.`, undefined, definitionInfo)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const registeredProcedure: THttpStreamProcedureRegistration<TContext> = {
|
|
146
|
+
name,
|
|
147
|
+
kind: 'http-stream',
|
|
148
|
+
config: {
|
|
149
|
+
path: config.path,
|
|
150
|
+
method: config.method,
|
|
151
|
+
scope: config.scope,
|
|
152
|
+
errors: config.errors as string[] | undefined,
|
|
153
|
+
description: config.description,
|
|
154
|
+
schema: jsonSchema as any,
|
|
155
|
+
validation: { req: validations.req, yield: validations.yield },
|
|
156
|
+
validateYields,
|
|
157
|
+
},
|
|
158
|
+
handler: wrappedHandler as any,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
procedures.set(name, registeredProcedure as any)
|
|
162
|
+
builder?.onCreate?.(registeredProcedure)
|
|
163
|
+
|
|
164
|
+
const info = { name, kind: 'http-stream' as const, ...registeredProcedure.config }
|
|
165
|
+
|
|
166
|
+
// Explicit return type — preserves TReq/TYieldType/TReturnType through the public callable.
|
|
167
|
+
// Do NOT use `typeof registeredProcedure.handler` — that erases generics to `any`.
|
|
168
|
+
return {
|
|
169
|
+
[name]: registeredProcedure.handler,
|
|
170
|
+
procedure: registeredProcedure.handler,
|
|
171
|
+
info,
|
|
172
|
+
} as {
|
|
173
|
+
[K in TName]: (
|
|
174
|
+
ctx: Prettify<TContext>,
|
|
175
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
176
|
+
) => Promise<{
|
|
177
|
+
stream: AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
|
|
178
|
+
initialHeaders?: Record<string, string>
|
|
179
|
+
}>
|
|
180
|
+
} & {
|
|
181
|
+
procedure: (
|
|
182
|
+
ctx: Prettify<TContext>,
|
|
183
|
+
req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
|
|
184
|
+
) => Promise<{
|
|
185
|
+
stream: AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
|
|
186
|
+
initialHeaders?: Record<string, string>
|
|
187
|
+
}>
|
|
188
|
+
info: typeof info
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|