@techspokes/typescript-wsdl-client 0.27.0 → 0.29.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 +4 -4
- package/dist/app/generateApp.js +2 -2
- package/dist/gateway/generators.d.ts +3 -2
- package/dist/gateway/generators.d.ts.map +1 -1
- package/dist/gateway/generators.js +51 -9
- package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
- package/dist/openapi/generateOpenAPI.js +4 -1
- package/dist/runtime/jsonArray.d.ts +24 -0
- package/dist/runtime/jsonArray.d.ts.map +1 -0
- package/dist/runtime/jsonArray.js +52 -0
- package/dist/test/generators.d.ts.map +1 -1
- package/dist/test/generators.js +25 -3
- package/docs/README.md +2 -2
- package/docs/api-reference.md +3 -1
- package/docs/architecture.md +1 -1
- package/docs/cli-reference.md +1 -1
- package/docs/concepts.md +2 -2
- package/docs/configuration.md +1 -0
- package/docs/decisions/002-streamable-responses.md +32 -10
- package/docs/gateway-guide.md +5 -10
- package/docs/generated-code.md +1 -1
- package/docs/migration-playbook.md +1 -1
- package/docs/migration.md +1 -1
- package/docs/output-anatomy.md +26 -4
- package/docs/production.md +2 -2
- package/docs/releases/v0.28.0.md +32 -0
- package/docs/releases/v0.29.0.md +32 -0
- package/docs/roadmap/README.md +31 -18
- package/docs/roadmap/v1.0-capability-conformance-framework.md +219 -0
- package/docs/roadmap/v1.0-contract-audit.md +14 -13
- package/docs/roadmap/v1.0-json-array-streaming.md +21 -19
- package/docs/roadmap/v1.0-openapi-fastify-compatibility.md +15 -13
- package/docs/roadmap/v1.0-release-candidate-gates.md +2 -0
- package/docs/roadmap/v1.0-wsdl-coverage-matrix.md +15 -13
- package/docs/start-here.md +4 -2
- package/docs/supported-patterns.md +1 -1
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/package.json +5 -5
- package/src/runtime/jsonArray.ts +55 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Most tools in this space stop at one layer: a SOAP runtime, type generation, or
|
|
|
28
28
|
- Handles complex inheritance, `xs:attribute`, namespace collisions, nested XSD imports, and configurable `xs:choice` modeling
|
|
29
29
|
- Generated `operations.ts` interface enables testing without importing `soap` or calling a live service
|
|
30
30
|
- OpenAPI is a first-class output, not an afterthought; types, schemas, and descriptions stay aligned
|
|
31
|
-
- Opt-in
|
|
31
|
+
- Opt-in streaming for large SOAP responses: client emits `AsyncIterable<RecordType>`, gateway flushes NDJSON or JSON array records incrementally, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`
|
|
32
32
|
- Optional agent skill ZIP for consumer-project AI agents that need package-specific generation guidance
|
|
33
33
|
- MIT licensed; generated code is yours with no attribution required
|
|
34
34
|
|
|
@@ -140,7 +140,7 @@ Platform API gateways solve governance, policy, and multi-language SDK generatio
|
|
|
140
140
|
| REST gateway generation | no | no | no | yes |
|
|
141
141
|
| Runnable app scaffolding | no | no | no | yes |
|
|
142
142
|
| Mockable operations interface | no | no | no | yes |
|
|
143
|
-
| Streaming large responses (NDJSON) | no | no | no | yes |
|
|
143
|
+
| Streaming large responses (NDJSON or JSON array) | no | no | no | yes |
|
|
144
144
|
|
|
145
145
|
Data as of April 2026.
|
|
146
146
|
|
|
@@ -233,8 +233,8 @@ See [CLI Reference](docs/cli-reference.md) for all flags and examples.
|
|
|
233
233
|
| [Core Concepts](docs/concepts.md) | Flattening, $value, primitives, determinism |
|
|
234
234
|
| [Architecture](docs/architecture.md) | Internal pipeline for contributors |
|
|
235
235
|
| [Agent Skill Artifact](docs/agent-skill.md) | Release ZIP for consumer-project AI agents |
|
|
236
|
-
| [Version 1.0 Roadmap Plan](docs/roadmap/README.md) |
|
|
237
|
-
| [Streamable Responses (ADR-002)](docs/decisions/002-streamable-responses.md) | Opt-in streaming: client `AsyncIterable`, gateway NDJSON, `x-wsdl-tsc-stream` |
|
|
236
|
+
| [Version 1.0 Roadmap Plan](docs/roadmap/README.md) | Conformance framework, implementation slices, and release gates for 1.0 |
|
|
237
|
+
| [Streamable Responses (ADR-002)](docs/decisions/002-streamable-responses.md) | Opt-in streaming: client `AsyncIterable`, gateway NDJSON or JSON array, `x-wsdl-tsc-stream` |
|
|
238
238
|
| [Version Migration](docs/migration.md) | Upgrading between package versions |
|
|
239
239
|
|
|
240
240
|
## Why This Exists
|
package/dist/app/generateApp.js
CHANGED
|
@@ -510,12 +510,12 @@ function generatePackageJson(appDir, force) {
|
|
|
510
510
|
},
|
|
511
511
|
dependencies: {
|
|
512
512
|
fastify: "^5.8.5",
|
|
513
|
-
"fastify-plugin": "^
|
|
513
|
+
"fastify-plugin": "^6.0.0",
|
|
514
514
|
saxes: "^6.0.0",
|
|
515
515
|
soap: "^1.9.3",
|
|
516
516
|
},
|
|
517
517
|
devDependencies: {
|
|
518
|
-
"@types/node": "^25.9.
|
|
518
|
+
"@types/node": "^25.9.3",
|
|
519
519
|
tsx: "^4.22.4",
|
|
520
520
|
typescript: "^6.0.3",
|
|
521
521
|
},
|
|
@@ -42,8 +42,9 @@ export interface OperationMetadata {
|
|
|
42
42
|
skipResponseSchema?: boolean;
|
|
43
43
|
/**
|
|
44
44
|
* Stream metadata populated from the OpenAPI `x-wsdl-tsc-stream` extension.
|
|
45
|
-
* When present, the route handler pipes `result.records` through the
|
|
46
|
-
*
|
|
45
|
+
* When present, the route handler pipes `result.records` through the helper
|
|
46
|
+
* matching the configured stream format instead of envelope-wrapping a single
|
|
47
|
+
* response object.
|
|
47
48
|
*/
|
|
48
49
|
stream?: {
|
|
49
50
|
mediaType: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/gateway/generators.ts"],"names":[],"mappings":"AAcA,OAAO,EAAC,KAAK,UAAU,EAAE,KAAK,eAAe,EAAyE,MAAM,cAAc,CAAC;AAG3I;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6IAA6I;IAC7I,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B
|
|
1
|
+
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/gateway/generators.ts"],"names":[],"mappings":"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"}
|
|
@@ -230,8 +230,8 @@ export function emitOperationSchemas(doc, opsDir, versionSlug, serviceSlug, sche
|
|
|
230
230
|
path: p,
|
|
231
231
|
summary: normalizeDocCommentText(typeof operation.summary === "string" ? operation.summary : undefined),
|
|
232
232
|
description: normalizeDocCommentText(typeof operation.description === "string" ? operation.description : undefined),
|
|
233
|
-
// Stream ops always skip response schema because
|
|
234
|
-
//
|
|
233
|
+
// Stream ops always skip response schema because the handler sends a
|
|
234
|
+
// Readable directly instead of a value for fast-json-stringify.
|
|
235
235
|
skipResponseSchema: skipResponseSchema || !!streamEntry,
|
|
236
236
|
...(streamEntry ? { stream: streamEntry } : {}),
|
|
237
237
|
});
|
|
@@ -646,10 +646,10 @@ export function createGatewayErrorHandler_${vSlug}_${sSlug}() {
|
|
|
646
646
|
/**
|
|
647
647
|
* Returns the streaming helpers block for runtime.ts.
|
|
648
648
|
*
|
|
649
|
-
* The emitted
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
649
|
+
* The emitted helpers mirror the reference implementations in src/runtime but
|
|
650
|
+
* are inlined here to avoid a cross-package import from the generated gateway
|
|
651
|
+
* (which would require wsdl-tsc to be a runtime dependency of the consumer's
|
|
652
|
+
* project).
|
|
653
653
|
*/
|
|
654
654
|
function buildStreamRuntimeSection() {
|
|
655
655
|
return `
|
|
@@ -676,6 +676,45 @@ async function* encodeNdjson<T>(records: AsyncIterable<T>): AsyncIterable<string
|
|
|
676
676
|
yield JSON.stringify(record) + "\\n";
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Wrap an async iterable of records in a Node Readable stream that emits a JSON
|
|
682
|
+
* array without buffering the full record set. The first record is prefetched
|
|
683
|
+
* before the opening bracket is pushed, so before-first-record source errors
|
|
684
|
+
* can still be translated into the standard JSON error envelope.
|
|
685
|
+
*/
|
|
686
|
+
export function toJsonArray<T>(records: AsyncIterable<T>): Readable {
|
|
687
|
+
return Readable.from(encodeJsonArray(records), { objectMode: false, encoding: "utf-8" });
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function* encodeJsonArray<T>(records: AsyncIterable<T>): AsyncIterable<string> {
|
|
691
|
+
const iterator = records[Symbol.asyncIterator]();
|
|
692
|
+
let complete = false;
|
|
693
|
+
try {
|
|
694
|
+
const first = await iterator.next();
|
|
695
|
+
if (first.done) {
|
|
696
|
+
complete = true;
|
|
697
|
+
yield "[]";
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
yield "[" + JSON.stringify(first.value);
|
|
702
|
+
|
|
703
|
+
while (true) {
|
|
704
|
+
const next = await iterator.next();
|
|
705
|
+
if (next.done) {
|
|
706
|
+
complete = true;
|
|
707
|
+
yield "]";
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
yield "," + JSON.stringify(next.value);
|
|
711
|
+
}
|
|
712
|
+
} finally {
|
|
713
|
+
if (!complete && typeof iterator.return === "function") {
|
|
714
|
+
await iterator.return();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
679
718
|
`;
|
|
680
719
|
}
|
|
681
720
|
/**
|
|
@@ -872,8 +911,11 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
|
|
|
872
911
|
? `request.body as ${reqTypeName}`
|
|
873
912
|
: "request.body";
|
|
874
913
|
// Build the runtime import and return expression based on unwrap availability
|
|
914
|
+
const streamHelperName = op.stream?.format === "json-array"
|
|
915
|
+
? "toJsonArray"
|
|
916
|
+
: "toNdjson";
|
|
875
917
|
const runtimeImport = op.stream
|
|
876
|
-
? `import {
|
|
918
|
+
? `import { ${streamHelperName} } from "../runtime${suffix}";`
|
|
877
919
|
: hasUnwrap
|
|
878
920
|
? `import { buildSuccessEnvelope, unwrapArrayWrappers } from "../runtime${suffix}";`
|
|
879
921
|
: `import { buildSuccessEnvelope } from "../runtime${suffix}";`;
|
|
@@ -883,7 +925,7 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
|
|
|
883
925
|
// Note: op.path comes from OpenAPI and already includes any base path
|
|
884
926
|
const schemaBinding = op.skipResponseSchema
|
|
885
927
|
? `\n// Response schema omitted: ${op.stream
|
|
886
|
-
? "stream operations
|
|
928
|
+
? "stream operations send a Readable directly"
|
|
887
929
|
: "$ref graph exceeds fast-json-stringify depth limit"}\nconst { response: _response, ...routeSchema } = schema as Record<string, unknown>;\n`
|
|
888
930
|
: "";
|
|
889
931
|
const schemaLine = op.skipResponseSchema
|
|
@@ -895,7 +937,7 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
|
|
|
895
937
|
const client = fastify.${clientMeta.decoratorName};
|
|
896
938
|
const result = await client.${clientMethod}(${bodyArg});
|
|
897
939
|
reply.type(${JSON.stringify(op.stream.mediaType)});
|
|
898
|
-
return reply.send(
|
|
940
|
+
return reply.send(${streamHelperName}(result.records));
|
|
899
941
|
},`
|
|
900
942
|
: ` handler: async (request) => {
|
|
901
943
|
const client = fastify.${clientMeta.decoratorName};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateOpenAPI.d.ts","sourceRoot":"","sources":["../../src/openapi/generateOpenAPI.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,EAAiB,KAAK,eAAe,EAAC,MAAM,+BAA+B,CAAC;AAKnF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3C,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC;IAC3E,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,
|
|
1
|
+
{"version":3,"file":"generateOpenAPI.d.ts","sourceRoot":"","sources":["../../src/openapi/generateOpenAPI.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,EAAiB,KAAK,eAAe,EAAC,MAAM,+BAA+B,CAAC;AAKnF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3C,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC;IAC3E,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,CA4TD"}
|
|
@@ -191,12 +191,15 @@ export async function generateOpenAPI(opts) {
|
|
|
191
191
|
// Errors raised before the first byte still use the standard envelope.
|
|
192
192
|
const recordType = op.stream.recordTypeName;
|
|
193
193
|
const itemRef = { $ref: `#/components/schemas/${recordType}` };
|
|
194
|
+
const streamSchema = op.stream.format === "json-array"
|
|
195
|
+
? { type: "array", items: itemRef }
|
|
196
|
+
: { type: "string" };
|
|
194
197
|
if (methodObj.responses?.["200"]) {
|
|
195
198
|
methodObj.responses["200"] = {
|
|
196
199
|
description: "Successful streamed SOAP operation response",
|
|
197
200
|
content: {
|
|
198
201
|
[op.stream.mediaType]: {
|
|
199
|
-
schema:
|
|
202
|
+
schema: streamSchema,
|
|
200
203
|
"x-wsdl-tsc-stream": {
|
|
201
204
|
format: op.stream.format,
|
|
202
205
|
itemSchema: itemRef,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON array adapter for record iterables.
|
|
3
|
+
*
|
|
4
|
+
* Given an `AsyncIterable<T>` (typically the output of `parseRecords`), produce
|
|
5
|
+
* a Node `Readable` that emits a single JSON array document without buffering
|
|
6
|
+
* the full record set.
|
|
7
|
+
*
|
|
8
|
+
* Terminal-error policy: the first record is prefetched before any bytes are
|
|
9
|
+
* pushed. A source error before the first record reaches Fastify before the
|
|
10
|
+
* response starts, so the gateway can return the standard JSON error envelope.
|
|
11
|
+
* Errors after the first record abort the stream and leave a truncated JSON
|
|
12
|
+
* document for clients to treat as a failed stream.
|
|
13
|
+
*/
|
|
14
|
+
import { Readable } from "node:stream";
|
|
15
|
+
/**
|
|
16
|
+
* Wrap an async iterable of records in a Node `Readable` stream that emits a
|
|
17
|
+
* JSON array. Downstream backpressure is honored via `Readable.from`'s default
|
|
18
|
+
* behavior: the iterator's `next()` is not called until the internal buffer has
|
|
19
|
+
* room.
|
|
20
|
+
*
|
|
21
|
+
* Source errors are forwarded to the returned stream's `error` event.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toJsonArray<T>(records: AsyncIterable<T>): Readable;
|
|
24
|
+
//# sourceMappingURL=jsonArray.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonArray.d.ts","sourceRoot":"","sources":["../../src/runtime/jsonArray.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAC;AAErC;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,QAAQ,CAElE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON array adapter for record iterables.
|
|
3
|
+
*
|
|
4
|
+
* Given an `AsyncIterable<T>` (typically the output of `parseRecords`), produce
|
|
5
|
+
* a Node `Readable` that emits a single JSON array document without buffering
|
|
6
|
+
* the full record set.
|
|
7
|
+
*
|
|
8
|
+
* Terminal-error policy: the first record is prefetched before any bytes are
|
|
9
|
+
* pushed. A source error before the first record reaches Fastify before the
|
|
10
|
+
* response starts, so the gateway can return the standard JSON error envelope.
|
|
11
|
+
* Errors after the first record abort the stream and leave a truncated JSON
|
|
12
|
+
* document for clients to treat as a failed stream.
|
|
13
|
+
*/
|
|
14
|
+
import { Readable } from "node:stream";
|
|
15
|
+
/**
|
|
16
|
+
* Wrap an async iterable of records in a Node `Readable` stream that emits a
|
|
17
|
+
* JSON array. Downstream backpressure is honored via `Readable.from`'s default
|
|
18
|
+
* behavior: the iterator's `next()` is not called until the internal buffer has
|
|
19
|
+
* room.
|
|
20
|
+
*
|
|
21
|
+
* Source errors are forwarded to the returned stream's `error` event.
|
|
22
|
+
*/
|
|
23
|
+
export function toJsonArray(records) {
|
|
24
|
+
return Readable.from(encode(records), { objectMode: false, encoding: "utf-8" });
|
|
25
|
+
}
|
|
26
|
+
async function* encode(records) {
|
|
27
|
+
const iterator = records[Symbol.asyncIterator]();
|
|
28
|
+
let complete = false;
|
|
29
|
+
try {
|
|
30
|
+
const first = await iterator.next();
|
|
31
|
+
if (first.done) {
|
|
32
|
+
complete = true;
|
|
33
|
+
yield "[]";
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
yield "[" + JSON.stringify(first.value);
|
|
37
|
+
while (true) {
|
|
38
|
+
const next = await iterator.next();
|
|
39
|
+
if (next.done) {
|
|
40
|
+
complete = true;
|
|
41
|
+
yield "]";
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
yield "," + JSON.stringify(next.value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
if (!complete && typeof iterator.return === "function") {
|
|
49
|
+
await iterator.return();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/test/generators.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAA0C,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAG5F,+EAA+E;AAC/E,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AA6FrF;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAkBzC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAgFR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,GACrB,MAAM,CAsCR;AAED;;;;;;;GAOG;AAEH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,
|
|
1
|
+
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/test/generators.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAA0C,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAG5F,+EAA+E;AAC/E,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AA6FrF;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAkBzC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAgFR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,GACrB,MAAM,CAsCR;AAED;;;;;;;GAOG;AAEH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CA8FR;AAED;;;;;;;GAOG;AAEH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAiIR;AAED;;GAEG;AAEH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAuER;AAED;;GAEG;AAEH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,CAAC,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE,eAAe,GACxB,MAAM,CAwDR;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,MAAM,CAgFR;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,MAAM,CAgDR;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,OAAO,EAAE,eAAe,GACvB,MAAM,GAAG,IAAI,CAkEf"}
|
package/dist/test/generators.js
CHANGED
|
@@ -256,10 +256,32 @@ export function emitRoutesTest(testDir, importsMode, operations, mocks) {
|
|
|
256
256
|
const requestPayload = JSON.stringify(mockData?.request ?? {}, null, 4).replace(/\n/g, "\n ");
|
|
257
257
|
const hint = formatOperationHint(op.summary, op.description);
|
|
258
258
|
const hintComment = hint ? ` // ${hint}\n` : "";
|
|
259
|
+
if (op.stream?.format === "json-array") {
|
|
260
|
+
// JSON array streams return one JSON document containing streamed records.
|
|
261
|
+
return `${hintComment} it("${op.method.toUpperCase()} ${op.path} streams json-array records", async () => {
|
|
262
|
+
const app = await createTestApp();
|
|
263
|
+
try {
|
|
264
|
+
const res = await app.inject({
|
|
265
|
+
method: "${op.method.toUpperCase()}",
|
|
266
|
+
url: "${op.path}",
|
|
267
|
+
payload: ${requestPayload},
|
|
268
|
+
});
|
|
269
|
+
expect(res.statusCode).toBe(200);
|
|
270
|
+
expect(String(res.headers["content-type"] ?? "")).toContain(${JSON.stringify(op.stream.mediaType)});
|
|
271
|
+
const records = JSON.parse(res.body);
|
|
272
|
+
expect(Array.isArray(records)).toBe(true);
|
|
273
|
+
expect(records.length).toBeGreaterThan(0);
|
|
274
|
+
for (const record of records) {
|
|
275
|
+
expect(record).toBeDefined();
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
await app.close();
|
|
279
|
+
}
|
|
280
|
+
});`;
|
|
281
|
+
}
|
|
259
282
|
if (op.stream) {
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
return `${hintComment} it("${op.method.toUpperCase()} ${op.path} streams ${op.stream.format} records", async () => {
|
|
283
|
+
// NDJSON streams return one parseable JSON document per line.
|
|
284
|
+
return `${hintComment} it("${op.method.toUpperCase()} ${op.path} streams ndjson records", async () => {
|
|
263
285
|
const app = await createTestApp();
|
|
264
286
|
try {
|
|
265
287
|
const res = await app.inject({
|
package/docs/README.md
CHANGED
|
@@ -29,8 +29,8 @@ Human-maintained reference documents for `@techspokes/typescript-wsdl-client`. T
|
|
|
29
29
|
- [concepts.md](concepts.md): flattening, `$value`, primitives, determinism
|
|
30
30
|
- [architecture.md](architecture.md): internal pipeline for contributors
|
|
31
31
|
- [agent-skill.md](agent-skill.md): release ZIP for consumer-project AI agents
|
|
32
|
-
- [roadmap/README.md](roadmap/README.md): implementation slices and release gates for 1.0
|
|
33
|
-
- [decisions/002-streamable-responses.md](decisions/002-streamable-responses.md): opt-in streaming design (client `AsyncIterable`, gateway NDJSON, `x-wsdl-tsc-stream` OpenAPI extension); shipped in 0.17.0
|
|
32
|
+
- [roadmap/README.md](roadmap/README.md): conformance framework, implementation slices, and release gates for 1.0
|
|
33
|
+
- [decisions/002-streamable-responses.md](decisions/002-streamable-responses.md): opt-in streaming design (client `AsyncIterable`, gateway NDJSON or JSON array, `x-wsdl-tsc-stream` OpenAPI extension); shipped in 0.17.0 and extended in 0.28.0
|
|
34
34
|
- [migration.md](migration.md): upgrading between package versions
|
|
35
35
|
|
|
36
36
|
## Conventions
|
package/docs/api-reference.md
CHANGED
|
@@ -357,6 +357,8 @@ interface OperationStreamMetadata {
|
|
|
357
357
|
|
|
358
358
|
Exactly one of `wsdlSource` or `catalogFile` must be set on each `ShapeCatalogRef`. `OperationStreamMetadata` is produced by the parser; `sourceOutputTypeName` is populated by the compiler when it binds the operation to the main WSDL.
|
|
359
359
|
|
|
360
|
+
`format` selects the gateway wire format. `ndjson` emits one JSON document per line with the default `application/x-ndjson` media type, and `json-array` emits one JSON array document with the default `application/json` media type.
|
|
361
|
+
|
|
360
362
|
## Security Configuration Helpers
|
|
361
363
|
|
|
362
364
|
Parse and build the shared security configuration used by OpenAPI generation and
|
|
@@ -408,7 +410,7 @@ const { compiled, openapiDoc } = await runGenerationPipeline({
|
|
|
408
410
|
});
|
|
409
411
|
|
|
410
412
|
const streamOp = compiled.operations.find((op) => op.stream);
|
|
411
|
-
console.log(streamOp?.stream?.mediaType); // "application/x-ndjson"
|
|
413
|
+
console.log(streamOp?.stream?.mediaType); // "application/x-ndjson" by default
|
|
412
414
|
```
|
|
413
415
|
|
|
414
416
|
The generated client exports a `StreamOperationResponse<RecordType>` type; each stream-configured operation returns `Promise<StreamOperationResponse<RecordType>>` with `records: AsyncIterable<RecordType>`.
|
package/docs/architecture.md
CHANGED
|
@@ -73,7 +73,7 @@ tools.ts provides string helpers (pascal, kebab, QName resolution). cli.ts provi
|
|
|
73
73
|
|
|
74
74
|
### runtime/
|
|
75
75
|
|
|
76
|
-
Template sources embedded into generated clients and gateways. streamXml.ts implements a SAX-driven `parseRecords(stream, spec)` that tracks the configured `recordPath` positionally (duplicate local names allowed) and yields records as their closing tags arrive. ndjson.ts wraps an async iterable of records in a `Readable` that emits NDJSON lines with honored backpressure. clientStreamMethods.tpl.txt and operationsStreamHelper.tpl.txt are text templates emitted into the generated `client.ts` and `operations.ts` when stream operations are present; they encode the `callStream()` transport and the `StreamOperationResponse<T>` type.
|
|
76
|
+
Template sources embedded into generated clients and gateways. streamXml.ts implements a SAX-driven `parseRecords(stream, spec)` that tracks the configured `recordPath` positionally (duplicate local names allowed) and yields records as their closing tags arrive. ndjson.ts wraps an async iterable of records in a `Readable` that emits NDJSON lines with honored backpressure. jsonArray.ts wraps the same record iterable as one streamed JSON array document. clientStreamMethods.tpl.txt and operationsStreamHelper.tpl.txt are text templates emitted into the generated `client.ts` and `operations.ts` when stream operations are present; they encode the `callStream()` transport and the `StreamOperationResponse<T>` type.
|
|
77
77
|
|
|
78
78
|
### xsd/
|
|
79
79
|
|
package/docs/cli-reference.md
CHANGED
|
@@ -135,7 +135,7 @@ When `--init-app` is used and `--openapi-servers` is not provided, servers defau
|
|
|
135
135
|
|
|
136
136
|
| Flag | Default | Description |
|
|
137
137
|
|------|---------|-------------|
|
|
138
|
-
| `--stream-config` | (none) | Path to a JSON stream-configuration file (ADR-002). Marks selected operations as streaming: client emits `AsyncIterable<RecordType>`, gateway serves NDJSON, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`. Buffered output is unchanged when the flag is omitted. |
|
|
138
|
+
| `--stream-config` | (none) | Path to a JSON stream-configuration file (ADR-002). Marks selected operations as streaming: client emits `AsyncIterable<RecordType>`, gateway serves NDJSON or JSON array output, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`. Buffered output is unchanged when the flag is omitted. |
|
|
139
139
|
|
|
140
140
|
`--stream-config` is accepted on the `compile`, `client`, and `pipeline` commands. It is not accepted on `openapi`, `gateway`, or `app` because those consume a pre-compiled `catalog.json` that already carries the normalized stream metadata.
|
|
141
141
|
|
package/docs/concepts.md
CHANGED
|
@@ -232,7 +232,7 @@ details:
|
|
|
232
232
|
|
|
233
233
|
### Streaming Bypass
|
|
234
234
|
|
|
235
|
-
Operations opted into streaming with `--stream-config` bypass the success envelope on the `200` response path. The OpenAPI response content is declared as the configured stream media type
|
|
235
|
+
Operations opted into streaming with `--stream-config` bypass the success envelope on the `200` response path. The OpenAPI response content is declared as the configured stream media type, and the gateway writes raw NDJSON lines or one streamed JSON array straight to the response body. Error responses (400, 502, and the rest) still use the normal envelope so clients always see structured failures before the first record. See [ADR-002](decisions/002-streamable-responses.md) for the full rationale.
|
|
236
236
|
|
|
237
237
|
### Envelope Naming
|
|
238
238
|
|
|
@@ -368,7 +368,7 @@ The catalog is the source of truth for stream metadata. Each opted-in operation
|
|
|
368
368
|
|
|
369
369
|
### Terminal-Error Policy
|
|
370
370
|
|
|
371
|
-
Errors before the first record use the normal gateway error envelope because the response headers and status have not been committed yet. Errors mid-stream truncate the
|
|
371
|
+
Errors before the first record use the normal gateway error envelope because the response headers and status have not been committed yet. Errors mid-stream truncate the response; NDJSON consumers detect this as an incomplete HTTP response, and JSON array consumers see an incomplete or invalid JSON document. Both cases must be treated as failed streams. This behavior is documented for operators in the [Production Guide](production.md#terminal-error-policy).
|
|
372
372
|
|
|
373
373
|
## Companion Catalogs and Shape Resolution
|
|
374
374
|
|
package/docs/configuration.md
CHANGED
|
@@ -125,6 +125,7 @@ operations not listed in the file.
|
|
|
125
125
|
|
|
126
126
|
- `recordType` and `recordPath` are required; `format` defaults to `ndjson` and
|
|
127
127
|
`mediaType` to `application/x-ndjson`.
|
|
128
|
+
- `format` accepts `ndjson` and `json-array`; JSON array streams default `mediaType` to `application/json` and emit one valid JSON array document.
|
|
128
129
|
- `shapeCatalog` references a `shapeCatalogs` entry and is only needed when
|
|
129
130
|
the record type lives in a different WSDL than the one driving generation.
|
|
130
131
|
- Shape catalogs accept either `wsdlSource` (fetched and compiled on the fly)
|
|
@@ -101,7 +101,7 @@ Add a stream configuration file passed with `--stream-config`. The same option s
|
|
|
101
101
|
|
|
102
102
|
`recordType` is the TypeScript and schema model to emit for each streamed record.
|
|
103
103
|
|
|
104
|
-
`format`
|
|
104
|
+
`format` supports `ndjson` and `json-array`. `ndjson` remains the default; `json-array` is available for clients that require a single JSON array response.
|
|
105
105
|
|
|
106
106
|
## CLI and Programmatic API
|
|
107
107
|
|
|
@@ -197,7 +197,7 @@ The converter must reuse catalog metadata for:
|
|
|
197
197
|
- Text content and `$value`
|
|
198
198
|
- Primitive mapping
|
|
199
199
|
|
|
200
|
-
The converter should collect SOAP faults, response errors, and warnings when they appear before the first record. After streaming starts, terminal errors cannot be represented as a normal JSON error envelope
|
|
200
|
+
The converter should collect SOAP faults, response errors, and warnings when they appear before the first record. After streaming starts, terminal errors cannot be represented as a normal JSON error envelope. The shipped behavior aborts the stream; NDJSON clients see an incomplete HTTP response, and JSON array clients see an incomplete or invalid JSON document.
|
|
201
201
|
|
|
202
202
|
## OpenAPI Output
|
|
203
203
|
|
|
@@ -222,25 +222,47 @@ Stream operations should not use the standard success envelope for `200` respons
|
|
|
222
222
|
|
|
223
223
|
OpenAPI 3.1 cannot fully describe an NDJSON sequence as a standard JSON Schema document. The `x-wsdl-tsc-stream` extension makes the item schema explicit for generated gateways, documentation tools, and future SDK generators.
|
|
224
224
|
|
|
225
|
+
For `format: "json-array"`, OpenAPI uses an array schema under the configured media type while keeping the same extension:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"description": "Successful streamed SOAP operation response",
|
|
230
|
+
"content": {
|
|
231
|
+
"application/json": {
|
|
232
|
+
"schema": {
|
|
233
|
+
"type": "array",
|
|
234
|
+
"items": {
|
|
235
|
+
"$ref": "#/components/schemas/UnitDescriptiveContentType"
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"x-wsdl-tsc-stream": {
|
|
239
|
+
"format": "json-array",
|
|
240
|
+
"itemSchema": {
|
|
241
|
+
"$ref": "#/components/schemas/UnitDescriptiveContentType"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
225
249
|
## Gateway Output
|
|
226
250
|
|
|
227
251
|
Generated stream routes should call the stream operation and send a Node stream through Fastify.
|
|
228
252
|
|
|
229
253
|
```typescript
|
|
230
|
-
import { Readable } from "node:stream";
|
|
231
|
-
|
|
232
254
|
handler: async (request, reply) => {
|
|
233
255
|
const client = fastify.escapiaContentClient;
|
|
234
256
|
const result = await client.UnitDescriptiveInfoStream(request.body);
|
|
235
257
|
|
|
236
258
|
reply.type("application/x-ndjson");
|
|
237
|
-
return reply.send(
|
|
259
|
+
return reply.send(toNdjson(result.records));
|
|
238
260
|
}
|
|
239
261
|
```
|
|
240
262
|
|
|
241
263
|
The generated operation schema should keep request validation but omit the Fastify response serialization schema for streamed `200` responses. Fastify cannot serialize an unbounded stream with a normal JSON response schema.
|
|
242
264
|
|
|
243
|
-
The gateway runtime
|
|
265
|
+
The gateway runtime adds `toNdjson()` and `toJsonArray()` helpers. These helpers are deterministic, backpressure-aware, and safe for large payloads.
|
|
244
266
|
|
|
245
267
|
## Test Strategy
|
|
246
268
|
|
|
@@ -250,7 +272,7 @@ Add converter tests that split XML chunks across element boundaries to prove the
|
|
|
250
272
|
|
|
251
273
|
Add a local Escapia-like WSDL fixture with `xs:any` output wrappers and a companion WSDL fixture with the concrete record types.
|
|
252
274
|
|
|
253
|
-
Add an integration test with a fake SOAP HTTP server that writes one record, waits, writes another record, and then closes the envelope. The test should assert that the gateway emits the first NDJSON line before the upstream response completes.
|
|
275
|
+
Add an integration test with a fake SOAP HTTP server that writes one record, waits, writes another record, and then closes the envelope. The test should assert that the gateway emits the first NDJSON line or JSON array record before the upstream response completes.
|
|
254
276
|
|
|
255
277
|
Add snapshot tests for generated `client.ts`, `operations.ts`, OpenAPI output, gateway route files, and gateway runtime helpers.
|
|
256
278
|
|
|
@@ -273,7 +295,7 @@ Run `npm run smoke:pipeline` after implementation to verify ordinary buffered ge
|
|
|
273
295
|
|
|
274
296
|
- Existing generated output is unchanged when no stream config is provided.
|
|
275
297
|
- A stream-configured Escapia content WSDL generates typed stream client methods.
|
|
276
|
-
- The generated gateway emits
|
|
298
|
+
- The generated gateway emits configured stream records incrementally.
|
|
277
299
|
- The generated OpenAPI document identifies stream operations and record schemas.
|
|
278
300
|
- The converter maps XML attributes, arrays, text values, and nillable values consistently with buffered responses.
|
|
279
301
|
- The chunked integration test proves the first record is sent before the full SOAP response is available.
|
|
@@ -286,7 +308,7 @@ The generator gains a second response execution model. This increases complexity
|
|
|
286
308
|
|
|
287
309
|
The catalog becomes more important as the shared source of truth because OpenAPI alone cannot carry enough stream conversion metadata.
|
|
288
310
|
|
|
289
|
-
NDJSON
|
|
311
|
+
NDJSON remains the default stream format because it is simple and broadly consumable. JSON array streaming is available for clients that require one JSON document and still streams without buffering the full response.
|
|
290
312
|
|
|
291
313
|
Companion catalogs are required for vendors that split stream wrappers and concrete record shapes across separate WSDLs.
|
|
292
314
|
|
|
@@ -298,7 +320,7 @@ Captured after the 0.17.0 ship for future maintainers:
|
|
|
298
320
|
- `GenerateOpenAPIOptions` and `GenerateGatewayOptions` in the programmatic API do not carry stream-config fields for the same reason. `compileWsdlToProject` and `runGenerationPipeline` (PipelineOptions) do.
|
|
299
321
|
- The client stream transport is emitted from two templates (`clientStreamMethods.tpl.txt`, `operationsStreamHelper.tpl.txt`). They embed the `StreamOperationResponse<T>` type and a `callStream()` method that POSTs a hand-built SOAP envelope via global `fetch`, bypassing `node-soap` as required by the phase-0 finding.
|
|
300
322
|
- `saxes ^6.0.0` was promoted from devDependency to runtime dependency on the package, and added as a pinned dependency in the generated app scaffold so stream-enabled consumers install it automatically.
|
|
301
|
-
- The `json-array` format is
|
|
323
|
+
- The `json-array` format is implemented in `0.28.0`. The helper prefetches the first record before emitting `[`, which preserves normal error envelopes for failures before the first record and documents truncation or invalid JSON for failures after streaming starts.
|
|
302
324
|
|
|
303
325
|
## References
|
|
304
326
|
|
package/docs/gateway-guide.md
CHANGED
|
@@ -183,10 +183,7 @@ export async function registerRoute_v1_weather_getcityforecastbyzip(fastify: Fas
|
|
|
183
183
|
|
|
184
184
|
### Streaming Handlers
|
|
185
185
|
|
|
186
|
-
Operations opted in via `--stream-config` emit
|
|
187
|
-
instead of the standard envelope. The generated handler streams records as
|
|
188
|
-
they arrive, and the Fastify response is flushed line-by-line with
|
|
189
|
-
backpressure (ADR-002).
|
|
186
|
+
Operations opted in via `--stream-config` emit a stream response instead of the standard envelope. The generated handler streams records as they arrive, and the Fastify response is flushed with backpressure. The default format is NDJSON; `format: "json-array"` emits one JSON array document.
|
|
190
187
|
|
|
191
188
|
```typescript
|
|
192
189
|
import type { FastifyInstance } from "fastify";
|
|
@@ -194,7 +191,7 @@ import type { GetWeatherInformation } from "../../client/types.js";
|
|
|
194
191
|
import schema from "../schemas/operations/getweatherinformation.json" with { type: "json" };
|
|
195
192
|
import { toNdjson } from "../runtime.js";
|
|
196
193
|
|
|
197
|
-
// Response schema omitted: stream operations
|
|
194
|
+
// Response schema omitted: stream operations send a Readable directly
|
|
198
195
|
const { response: _response, ...routeSchema } = schema as Record<string, unknown>;
|
|
199
196
|
|
|
200
197
|
export async function registerRoute_v1_weather_getweatherinformation(fastify: FastifyInstance) {
|
|
@@ -212,11 +209,9 @@ export async function registerRoute_v1_weather_getweatherinformation(fastify: Fa
|
|
|
212
209
|
}
|
|
213
210
|
```
|
|
214
211
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
use the normal error envelope
|
|
218
|
-
response's truncation. Consumers detect these via the absence of a clean
|
|
219
|
-
terminating zero-chunk.
|
|
212
|
+
For `format: "json-array"`, the same handler imports `toJsonArray`, sets `reply.type("application/json")`, and sends `reply.send(toJsonArray(result.records))`.
|
|
213
|
+
|
|
214
|
+
The client method returns `StreamOperationResponse<RecordType>` with a `records: AsyncIterable<RecordType>`. Errors raised before the first record use the normal error envelope. Errors raised mid-stream truncate the response; NDJSON clients see an incomplete HTTP response, and JSON array clients see an incomplete or invalid JSON document.
|
|
220
215
|
|
|
221
216
|
## Error Handling
|
|
222
217
|
|
package/docs/generated-code.md
CHANGED
|
@@ -176,7 +176,7 @@ Key features of the generated handlers:
|
|
|
176
176
|
- Envelope wrapping: `buildSuccessEnvelope()` wraps the raw SOAP response in the standard `{ status, message, data, error }` envelope
|
|
177
177
|
- Route file header comments include propagated operation summary and description when present
|
|
178
178
|
|
|
179
|
-
Stream-configured operations generate a different handler shape: the response serialization schema is omitted
|
|
179
|
+
Stream-configured operations generate a different handler shape: the response serialization schema is omitted and the handler streams `result.records` through the helper for the configured format. NDJSON handlers use `reply.type("application/x-ndjson")` with `toNdjson(result.records)`, and JSON array handlers use `reply.type("application/json")` with `toJsonArray(result.records)`. See the [Gateway Guide Streaming Handlers section](gateway-guide.md#streaming-handlers) for the full example and terminal-error policy.
|
|
180
180
|
|
|
181
181
|
See [Gateway Guide](gateway-guide.md) for the full architecture and [CLI Reference](cli-reference.md) for generation flags.
|
|
182
182
|
|
|
@@ -89,7 +89,7 @@ npx wsdl-tsc pipeline \
|
|
|
89
89
|
--init-app
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
The opted-in operations now return `StreamOperationResponse<RecordType>` on the client, serve `application/x-ndjson` on the gateway, and advertise the record schema in OpenAPI via `x-wsdl-tsc-stream`. Operations not listed in the config are unchanged. See [Stream Configuration](configuration.md#stream-configuration) for the full file reference and [ADR-002](decisions/002-streamable-responses.md) for the terminal-error policy.
|
|
92
|
+
The opted-in operations now return `StreamOperationResponse<RecordType>` on the client, serve `application/x-ndjson` by default on the gateway, and advertise the record schema in OpenAPI via `x-wsdl-tsc-stream`. Set `format: "json-array"` for `application/json` array streaming. Operations not listed in the config are unchanged. See [Stream Configuration](configuration.md#stream-configuration) for the full file reference and [ADR-002](decisions/002-streamable-responses.md) for the terminal-error policy.
|
|
93
93
|
|
|
94
94
|
## Step 3: Generate the REST Gateway
|
|
95
95
|
|
package/docs/migration.md
CHANGED
|
@@ -35,7 +35,7 @@ This upgrade adds opt-in streamable SOAP responses. No breaking changes; generat
|
|
|
35
35
|
|
|
36
36
|
### What Changed in 0.17.x
|
|
37
37
|
|
|
38
|
-
The CLI gains a `--stream-config <file>` flag on `compile`, `client`, and `pipeline`. Operations listed in that file emit a new client method signature returning `StreamOperationResponse<RecordType>` with `records: AsyncIterable<RecordType>`, an OpenAPI 200 response typed as `application/x-ndjson` with an `x-wsdl-tsc-stream` extension, and a Fastify route that streams
|
|
38
|
+
The CLI gains a `--stream-config <file>` flag on `compile`, `client`, and `pipeline`. Operations listed in that file emit a new client method signature returning `StreamOperationResponse<RecordType>` with `records: AsyncIterable<RecordType>`, an OpenAPI 200 response typed as `application/x-ndjson` by default with an `x-wsdl-tsc-stream` extension, and a Fastify route that streams records with backpressure. Set `format: "json-array"` for `application/json` array streaming. The compiler now retains `xs:any` wildcard particles on compiled types (previously dropped silently), enabling honest stream-candidate detection and companion-catalog shape resolution. `saxes ^6.0.0` is now a runtime dependency of the package and is pinned automatically into the generated app scaffold.
|
|
39
39
|
|
|
40
40
|
### Steps to Upgrade to 0.17.x
|
|
41
41
|
|