@techspokes/typescript-wsdl-client 0.27.0 → 0.28.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 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
 
@@ -234,7 +234,7 @@ See [CLI Reference](docs/cli-reference.md) for all flags and examples.
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
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` |
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
@@ -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":"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"}
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
@@ -30,7 +30,7 @@ Human-maintained reference documents for `@techspokes/typescript-wsdl-client`. T
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
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
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
 
@@ -75,7 +75,7 @@ CLI OpenAPI generation always validates the generated spec using `@apidevtools/s
75
75
 
76
76
  ### Stream Schema Extension
77
77
 
78
- Stream operations do not use the standard success envelope for `200` responses. The response content declares the configured stream media type (default `application/x-ndjson`) with `schema: { "type": "string" }` and an `x-wsdl-tsc-stream` extension that carries the record schema reference:
78
+ Stream operations do not use the standard success envelope for `200` responses. NDJSON streams declare the configured stream media type (default `application/x-ndjson`) with `schema: { "type": "string" }` and an `x-wsdl-tsc-stream` extension that carries the record schema reference:
79
79
 
80
80
  ```json
81
81
  {
@@ -94,7 +94,29 @@ Stream operations do not use the standard success envelope for `200` responses.
94
94
  }
95
95
  ```
96
96
 
97
- OpenAPI 3.1 cannot fully describe an NDJSON sequence as a standard JSON Schema document, so the extension makes the item schema explicit for generated gateways, documentation tools, and future SDK generators. Error responses (400, 502, and the rest) still use the normal envelope.
97
+ JSON array streams default to `application/json` and declare the record schema as the array item schema while keeping the same extension:
98
+
99
+ ```json
100
+ {
101
+ "200": {
102
+ "description": "Successful streamed SOAP operation response",
103
+ "content": {
104
+ "application/json": {
105
+ "schema": {
106
+ "type": "array",
107
+ "items": { "$ref": "#/components/schemas/UnitDescriptiveContentType" }
108
+ },
109
+ "x-wsdl-tsc-stream": {
110
+ "format": "json-array",
111
+ "itemSchema": { "$ref": "#/components/schemas/UnitDescriptiveContentType" }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ OpenAPI 3.1 cannot fully describe an NDJSON sequence as a standard JSON Schema document, so the extension makes the item schema explicit for generated gateways, documentation tools, and future SDK generators. JSON array streams use a normal array schema and keep the extension for downstream tooling. Error responses (400, 502, and the rest) still use the normal envelope.
98
120
 
99
121
  ## Gateway Output
100
122
 
@@ -117,11 +139,11 @@ Each route file in `routes/` follows the same pattern: validate the JSON request
117
139
 
118
140
  ### Stream Routes
119
141
 
120
- Stream-configured operations generate a different route shape. The Fastify response serialization schema is omitted because Fastify cannot serialize an unbounded stream with a normal JSON response schema. The handler sets `reply.type("application/x-ndjson")` and returns `reply.send(toNdjson(result.records))`. `runtime.ts` gains a `toNdjson<T>(records: AsyncIterable<T>): Readable` helper that wraps the async iterable in a backpressure-aware Node `Readable`. See the [Gateway Guide](gateway-guide.md#streaming-handlers) for the full handler example and terminal-error policy.
142
+ Stream-configured operations generate a different route shape. The Fastify response serialization schema is omitted because the handler sends a `Readable` directly. NDJSON handlers set `reply.type("application/x-ndjson")` and return `reply.send(toNdjson(result.records))`; JSON array handlers set `reply.type("application/json")` and return `reply.send(toJsonArray(result.records))`. `runtime.ts` gains `toNdjson<T>(records: AsyncIterable<T>): Readable` and `toJsonArray<T>(records: AsyncIterable<T>): Readable` helpers. See the [Gateway Guide](gateway-guide.md#streaming-handlers) for the full handler example and terminal-error policy.
121
143
 
122
144
  ### Generated Test Surface
123
145
 
124
- When `--test-dir` is combined with `--stream-config`, the generated happy-path tests for stream operations assert on the `application/x-ndjson` content-type and parse each line as a separate JSON record. Mock clients use async-generator overrides that yield records to drive those tests; see the [Testing Guide](testing.md) for the pattern.
146
+ When `--test-dir` is combined with `--stream-config`, the generated happy-path tests for stream operations assert on the configured content type. NDJSON tests parse each line as a separate JSON record, and JSON array tests parse the response body once as an array. Mock clients use async-generator overrides that yield records to drive those tests; see the [Testing Guide](testing.md) for the pattern.
125
147
 
126
148
  ### Plugin registration
127
149
 
@@ -83,7 +83,7 @@ Log the time to first record, not just the time to response completion. First-re
83
83
 
84
84
  ### Terminal-Error Policy
85
85
 
86
- Errors raised before the first record use the normal gateway error envelope (client sees a standard JSON error). Errors raised mid-stream truncate the chunked response without a terminating zero-chunk. Consumers detect this as an incomplete HTTP response. Document this behavior for downstream API consumers so they distinguish truncation from a legitimate empty stream.
86
+ Errors raised before the first record use the normal gateway error envelope because the JSON array helper prefetches the first record before emitting `[`. Errors raised mid-stream truncate the response. NDJSON consumers detect this as an incomplete HTTP response, and JSON array consumers must treat an incomplete or invalid JSON document as a failed stream. Document this behavior for downstream API consumers so they distinguish truncation from a legitimate empty stream.
87
87
 
88
88
  ## Known Limitations
89
89
 
@@ -105,7 +105,7 @@ Single-child sequences with maxOccurs>1 become array schemas. Sequences with mul
105
105
 
106
106
  ### Stream Format Coverage
107
107
 
108
- Only `ndjson` is emitted today. `json-array` is reserved in the config schema but not yet implemented; opting in with `format: "json-array"` parses successfully but generates no routes for that operation.
108
+ `ndjson` is the default stream format. `json-array` is supported for operations that need a single JSON document response; it streams records incrementally as a JSON array and does not buffer the full SOAP response.
109
109
 
110
110
  ### Stream Transport Bypasses node-soap
111
111
 
@@ -0,0 +1,32 @@
1
+ # TypeScript WSDL Client v0.28.0
2
+
3
+ ## JSON Array Streaming
4
+
5
+ This release implements `format: "json-array"` for operations opted into streaming with `--stream-config`.
6
+
7
+ ## What This Improves
8
+
9
+ Teams can now serve large SOAP response records as one streamed `application/json` array without buffering the full upstream response. NDJSON remains the default stream format, and JSON array operations keep the same generated client return shape: `StreamOperationResponse<RecordType>` with `records: AsyncIterable<RecordType>`.
10
+
11
+ ## Highlights
12
+
13
+ - Adds `toJsonArray()` runtime streaming alongside the existing NDJSON helper.
14
+ - Emits OpenAPI `application/json` array schemas for JSON array stream operations.
15
+ - Generates Fastify routes that call `toJsonArray(result.records)` for `format: "json-array"`.
16
+ - Generates route tests that parse JSON array stream responses as one array document.
17
+ - Documents the terminal-error behavior for JSON array streams.
18
+
19
+ ## Upgrade Notes
20
+
21
+ No special upgrade steps. Existing stream operations keep `ndjson` unless the stream config opts an operation into `format: "json-array"`.
22
+
23
+ ## Validation
24
+
25
+ - CI passed.
26
+ - NPM package contents were validated.
27
+ - Documentation links and TypeScript fenced snippets were validated.
28
+ - Agent skill artifact was validated and packaged.
29
+
30
+ ## Notes
31
+
32
+ Release tag: `v0.28.0`.
@@ -1,6 +1,6 @@
1
1
  # Version 1.0 Roadmap Plan
2
2
 
3
- Detailed plan for moving `@techspokes/typescript-wsdl-client` from `0.26.x` to a stable `1.0.0` release.
3
+ Detailed plan for moving `@techspokes/typescript-wsdl-client` from `0.28.x` to a stable `1.0.0` release.
4
4
 
5
5
  See the root [README.md](../../README.md) for project overview and the root [ROADMAP.md](../../ROADMAP.md) for the public roadmap summary.
6
6
 
@@ -37,7 +37,7 @@ Choice union mode is complete in `0.26.0`. The default `all-optional` mode remai
37
37
 
38
38
  ### Slice 4: JSON Array Streaming
39
39
 
40
- Implement streaming JSON array output after stream error behavior is researched. This is the next implementation slice after `0.26.0`, and the result must stream records incrementally without buffering the full SOAP response.
40
+ JSON array streaming is complete in `0.28.0`. The default `ndjson` format remains unchanged, and `format: "json-array"` streams records incrementally without buffering the full SOAP response.
41
41
 
42
42
  ### Slice 5: WSDL Coverage Matrix
43
43
 
@@ -36,7 +36,7 @@ This slice prevents later work from building on ambiguous behavior. It also give
36
36
 
37
37
  ### JSON Array Streaming
38
38
 
39
- `format: "json-array"` must stay in the 1.0 roadmap because 1.0 requires the format. The audit should identify every parser, client, OpenAPI, gateway, runtime, and docs surface that currently assumes NDJSON.
39
+ `format: "json-array"` is implemented in `0.28.0`. The audit should keep every parser, client, OpenAPI, gateway, runtime, and docs surface aligned across both stream formats.
40
40
 
41
41
  ### Roadmap State
42
42
 
@@ -1,8 +1,8 @@
1
1
  # Version 1.0 JSON Array Streaming
2
2
 
3
- Plan for implementing `format: "json-array"` streaming for SOAP operations that already use the stream configuration model.
3
+ Status: implemented for `0.28.0`.
4
4
 
5
- This is the next planned feature slice after the `0.26.0` choice union mode release.
5
+ This document records the implementation slice for `format: "json-array"` streaming for SOAP operations that already use the stream configuration model.
6
6
 
7
7
  See the root [README.md](../../README.md) for project overview and [Version 1.0 Roadmap Plan](README.md) for the complete 1.0 route.
8
8
 
@@ -12,9 +12,9 @@ See the root [README.md](../../README.md) for project overview and [Version 1.0
12
12
 
13
13
  ## Design Direction
14
14
 
15
- JSON array streaming should write an opening bracket, stream comma-separated JSON records, and write a closing bracket after the async iterable completes. The implementation must preserve backpressure by converting the async iterable into a Node `Readable` stream.
15
+ JSON array streaming writes an opening bracket with the first record, streams comma-separated JSON records, and writes a closing bracket after the async iterable completes. The implementation preserves backpressure by converting the async iterable into a Node `Readable` stream.
16
16
 
17
- The key research question is error timing. If the route can observe an upstream error before sending the first byte, it should still return the normal error envelope. Once the JSON array response has started, a later error must truncate the response and clients must treat the JSON parse failure as a failed stream.
17
+ The key researched constraint is error timing. The helper prefetches the first record before sending the first byte, so an upstream error before the first record still returns the normal error envelope. Once the JSON array response has started, a later error truncates the response and clients must treat the JSON parse failure as a failed stream.
18
18
 
19
19
  ## Scope
20
20
 
@@ -39,7 +39,7 @@ Research whether generated routes should prefetch the first record before callin
39
39
 
40
40
  ### Empty Stream Handling
41
41
 
42
- Confirm that an empty stream returns `[]` with `application/json`. The helper must emit a syntactically valid empty array without special route code where possible.
42
+ An empty stream returns `[]` with `application/json`. The helper emits a syntactically valid empty array without special route code.
43
43
 
44
44
  ### Backpressure Handling
45
45
 
@@ -47,7 +47,7 @@ Confirm that the helper does not pull from the async iterable faster than the HT
47
47
 
48
48
  ### Terminal Error Handling
49
49
 
50
- Confirm how Fastify and Node surface stream errors after the JSON array has begun. Document the client-visible behavior in production docs and troubleshooting docs.
50
+ Fastify surfaces stream errors after the JSON array has begun as a destroyed or reset response under inject and as a truncated response to real clients. Production docs and troubleshooting docs describe this as a failed stream.
51
51
 
52
52
  ## Implementation Units
53
53
 
@@ -92,7 +92,9 @@ Minimal `stream.config.json`:
92
92
  }
93
93
  ```
94
94
 
95
- Stream operations return `StreamOperationResponse<RecordType>` on the client (`records: AsyncIterable<RecordType>`), emit `application/x-ndjson` on the gateway, and advertise the record schema in OpenAPI via the `x-wsdl-tsc-stream` extension.
95
+ Stream operations return `StreamOperationResponse<RecordType>` on the client (`records: AsyncIterable<RecordType>`), emit `application/x-ndjson` by default on the gateway, and advertise the record schema in OpenAPI via the `x-wsdl-tsc-stream` extension.
96
+
97
+ Set `"format": "json-array"` in the stream config when downstream clients need one `application/json` array document instead of NDJSON lines.
96
98
 
97
99
  Next: [ADR-002: Streamable Responses](decisions/002-streamable-responses.md) for rationale and terminal-error policy, then [Stream Configuration](configuration.md#stream-configuration) for the full file reference.
98
100
 
@@ -118,5 +120,5 @@ For more on scope boundaries, see the "When NOT to Use This" section of the [REA
118
120
  | Plan a full SOAP-to-REST migration | [Migration Playbook](migration-playbook.md) |
119
121
  | Set up testing for generated code | [Testing Guide](testing.md) |
120
122
  | Review all CLI flags | [CLI Reference](cli-reference.md) |
121
- | Opt specific operations into NDJSON streaming | [ADR-002](decisions/002-streamable-responses.md) and [Stream Configuration](configuration.md#stream-configuration) |
123
+ | Opt specific operations into response streaming | [ADR-002](decisions/002-streamable-responses.md) and [Stream Configuration](configuration.md#stream-configuration) |
122
124
  | Install package-specific agent guidance | [Agent Skill Artifact](agent-skill.md) |
@@ -20,7 +20,7 @@ These patterns are handled end-to-end: WSDL parsing, TypeScript type generation,
20
20
  - Circular type references detected and broken with minimal stub types
21
21
  - Multiple WSDL ports and bindings; the first SOAP binding is selected, all ports are documented in service metadata
22
22
  - SOAP 1.1 and SOAP 1.2 binding detection
23
- - Streamable SOAP responses, opt-in per operation via `--stream-config` (ADR-002): client exposes `AsyncIterable<RecordType>`, gateway emits NDJSON with backpressure, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`
23
+ - Streamable SOAP responses, opt-in per operation via `--stream-config` (ADR-002): client exposes `AsyncIterable<RecordType>`, gateway emits NDJSON or JSON array streams with backpressure, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`
24
24
  - `xs:any` wildcard particles retained on compiled types (they used to be dropped silently) — enables honest stream-candidate detection and companion-catalog shape resolution
25
25
 
26
26
  ### Named simple types and same-name elements
package/docs/testing.md CHANGED
@@ -246,7 +246,7 @@ const client = createMockClient({
246
246
 
247
247
  Mock responses use the pre-unwrap SOAP wrapper shape. The generated `unwrapArrayWrappers()` function handles conversion at runtime.
248
248
 
249
- For operations opted in via `--stream-config`, the mock returns `records: AsyncIterable<RecordType>` (via a small `asyncIterableOf` helper) and the generated happy-path test asserts on the NDJSON content-type and parseable record lines. Override a stream op with a multi-record iterable to exercise downstream backpressure:
249
+ For operations opted in via `--stream-config`, the mock returns `records: AsyncIterable<RecordType>` via a small `asyncIterableOf` helper. Generated happy-path tests assert on the configured content type; NDJSON tests parse record lines, and JSON array tests parse the response body once as an array. Override a stream op with a multi-record iterable to exercise downstream backpressure:
250
250
 
251
251
  ```typescript
252
252
  const client = createMockClient({
@@ -20,7 +20,7 @@ See [README](../README.md) for quick start and [CLI Reference](cli-reference.md)
20
20
  | Stream config references unknown operation | Operation name must match the WSDL exactly; check spelling and casing |
21
21
  | Stream record type not found | `recordType` must exist in the main catalog or a companion `shapeCatalog` must supply it; confirm the companion WSDL compiles cleanly in isolation |
22
22
  | Structural collision between main and companion catalog | Two types share a name but differ structurally; rename in the companion source or point `recordType` at a distinct subtree |
23
- | NDJSON response ends abruptly | Mid-stream upstream error per the terminal-error policy; check gateway logs for the classified error |
23
+ | Stream response ends abruptly or JSON array parse fails | Mid-stream upstream error per the terminal-error policy; check gateway logs for the classified error |
24
24
  | Stream recordPath does not match | SAX matching is positional and case-sensitive; verify duplicate local-name segments are spelled exactly |
25
25
  | Stream client throws "stream request failed" | The upstream SOAP endpoint rejected the hand-built envelope; check `requestRaw` on the response and verify SOAP action and namespaces match the WSDL binding |
26
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techspokes/typescript-wsdl-client",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Turn legacy WSDL/SOAP services into typed TypeScript clients, OpenAPI 3.1 specs, and production-ready Fastify REST gateways. Built for enterprise SOAP modernization.",
5
5
  "keywords": [
6
6
  "wsdl",
@@ -0,0 +1,55 @@
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
+ /**
17
+ * Wrap an async iterable of records in a Node `Readable` stream that emits a
18
+ * JSON array. Downstream backpressure is honored via `Readable.from`'s default
19
+ * behavior: the iterator's `next()` is not called until the internal buffer has
20
+ * room.
21
+ *
22
+ * Source errors are forwarded to the returned stream's `error` event.
23
+ */
24
+ export function toJsonArray<T>(records: AsyncIterable<T>): Readable {
25
+ return Readable.from(encode(records), {objectMode: false, encoding: "utf-8"});
26
+ }
27
+
28
+ async function* encode<T>(records: AsyncIterable<T>): AsyncIterable<string> {
29
+ const iterator = records[Symbol.asyncIterator]();
30
+ let complete = false;
31
+ try {
32
+ const first = await iterator.next();
33
+ if (first.done) {
34
+ complete = true;
35
+ yield "[]";
36
+ return;
37
+ }
38
+
39
+ yield "[" + JSON.stringify(first.value);
40
+
41
+ while (true) {
42
+ const next = await iterator.next();
43
+ if (next.done) {
44
+ complete = true;
45
+ yield "]";
46
+ return;
47
+ }
48
+ yield "," + JSON.stringify(next.value);
49
+ }
50
+ } finally {
51
+ if (!complete && typeof iterator.return === "function") {
52
+ await iterator.return();
53
+ }
54
+ }
55
+ }