@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.
Files changed (40) hide show
  1. package/README.md +4 -4
  2. package/dist/app/generateApp.js +2 -2
  3. package/dist/gateway/generators.d.ts +3 -2
  4. package/dist/gateway/generators.d.ts.map +1 -1
  5. package/dist/gateway/generators.js +51 -9
  6. package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
  7. package/dist/openapi/generateOpenAPI.js +4 -1
  8. package/dist/runtime/jsonArray.d.ts +24 -0
  9. package/dist/runtime/jsonArray.d.ts.map +1 -0
  10. package/dist/runtime/jsonArray.js +52 -0
  11. package/dist/test/generators.d.ts.map +1 -1
  12. package/dist/test/generators.js +25 -3
  13. package/docs/README.md +2 -2
  14. package/docs/api-reference.md +3 -1
  15. package/docs/architecture.md +1 -1
  16. package/docs/cli-reference.md +1 -1
  17. package/docs/concepts.md +2 -2
  18. package/docs/configuration.md +1 -0
  19. package/docs/decisions/002-streamable-responses.md +32 -10
  20. package/docs/gateway-guide.md +5 -10
  21. package/docs/generated-code.md +1 -1
  22. package/docs/migration-playbook.md +1 -1
  23. package/docs/migration.md +1 -1
  24. package/docs/output-anatomy.md +26 -4
  25. package/docs/production.md +2 -2
  26. package/docs/releases/v0.28.0.md +32 -0
  27. package/docs/releases/v0.29.0.md +32 -0
  28. package/docs/roadmap/README.md +31 -18
  29. package/docs/roadmap/v1.0-capability-conformance-framework.md +219 -0
  30. package/docs/roadmap/v1.0-contract-audit.md +14 -13
  31. package/docs/roadmap/v1.0-json-array-streaming.md +21 -19
  32. package/docs/roadmap/v1.0-openapi-fastify-compatibility.md +15 -13
  33. package/docs/roadmap/v1.0-release-candidate-gates.md +2 -0
  34. package/docs/roadmap/v1.0-wsdl-coverage-matrix.md +15 -13
  35. package/docs/start-here.md +4 -2
  36. package/docs/supported-patterns.md +1 -1
  37. package/docs/testing.md +1 -1
  38. package/docs/troubleshooting.md +1 -1
  39. package/package.json +5 -5
  40. 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 NDJSON streaming for large SOAP responses: client emits `AsyncIterable<RecordType>`, gateway flushes records incrementally, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`
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) | 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, `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
@@ -510,12 +510,12 @@ function generatePackageJson(appDir, force) {
510
510
  },
511
511
  dependencies: {
512
512
  fastify: "^5.8.5",
513
- "fastify-plugin": "^5.1.0",
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.1",
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 NDJSON
46
- * helper instead of envelope-wrapping a single response object.
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;;;;OAIG;IACH,MAAM,CAAC,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,QAAQ,GAAG,YAAY,CAAC;QAChC,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,0BAA0B,EAAE,MAAM,EAAE,EACpC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,GAAG,EACvH,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,MAAM,EACnC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACnC,iBAAiB,EAAE,CAyJrB;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAsBN;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CA6DN;AA4BD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,GAAG,EACb,IAAI,CAAC,EAAE;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAC,GAC5B,IAAI,CA2PN;AAsCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CAuFN;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CA6BN;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE,GAAG,GACZ,IAAI,CA0HN"}
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 NDJSON is not a single
234
- // JSON document and fast-json-stringify can't serialize it.
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 `toNdjson` mirrors the reference implementation in
650
- * src/runtime/ndjson.ts but is inlined here to avoid a cross-package import
651
- * from the generated gateway (which would require wsdl-tsc to be a runtime
652
- * dependency of the consumer's project).
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 { toNdjson } from "../runtime${suffix}";`
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 emit NDJSON, not a single JSON object"
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(toNdjson(result.records));
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,CAyTD"}
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: { type: "string" },
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,CAuER;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"}
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"}
@@ -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
- // Stream routes return NDJSON lines, not a single JSON envelope. Assert
261
- // on content-type and at least one parseable record per line.
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
@@ -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>`.
@@ -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
 
@@ -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 (default `application/x-ndjson`) and the gateway writes raw NDJSON lines 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.
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 chunked response without a terminating zero-chunk; consumers detect this as an incomplete HTTP response and must treat it as a failure. NDJSON has no native error frame, and emitting a fake one would conflict with the item schema. This behavior is documented for operators in the [Production Guide](production.md#terminal-error-policy).
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
 
@@ -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` should support `ndjson` first. `json-array` can be added later for clients that require a single JSON array response.
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 for NDJSON. The runtime should either emit a final error record with a documented extension shape or abort the stream and log the classified error.
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(Readable.from(toNdjson(result.records)));
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 should add `toNdjson()` and, later, `toJsonArrayStream()` helpers. These helpers should be deterministic, backpressure-aware, and safe for large payloads.
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 `application/x-ndjson` records incrementally.
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 becomes the recommended stream format because it is simple, broadly consumable, and does not require buffering a complete JSON array before sending data.
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 reserved in the config parser but not yet implemented by the emitters. Entries using `format: "json-array"` parse successfully and can be used to forward-declare intent; they do not currently generate routes or client methods.
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
 
@@ -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 an NDJSON response pipe
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 emit NDJSON, not a single JSON object
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
- The client method returns `StreamOperationResponse<RecordType>` with a
216
- `records: AsyncIterable<RecordType>`. Errors raised before the first record
217
- use the normal error envelope; errors raised mid-stream trip the chunked
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
 
@@ -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, `reply.type("application/x-ndjson")` is set, and `reply.send(toNdjson(result.records))` streams records with backpressure. See the [Gateway Guide Streaming Handlers section](gateway-guide.md#streaming-handlers) for the full example and terminal-error policy.
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 NDJSON with backpressure. 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.
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