@webpieces/http-routing 0.4.773 → 0.4.775

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-routing",
3
- "version": "0.4.773",
3
+ "version": "0.4.775",
4
4
  "description": "Decorator-based routing with auto-wiring for WebPieces",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -22,9 +22,9 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@inversifyjs/binding-decorators": "1.1.5",
25
- "@webpieces/core-context": "0.4.773",
26
- "@webpieces/core-util": "0.4.773",
27
- "@webpieces/gcp-identity": "0.4.773",
25
+ "@webpieces/core-context": "0.4.775",
26
+ "@webpieces/core-util": "0.4.775",
27
+ "@webpieces/gcp-identity": "0.4.775",
28
28
  "inversify": "7.10.4",
29
29
  "jsonwebtoken": "9.0.2",
30
30
  "minimatch": "10.0.1"
@@ -34,5 +34,15 @@ export declare class ApiClientFactory {
34
34
  apiClients(): ApiClient[];
35
35
  /** Build the proxy record (method name → invoker) from the API prototype's decorators. */
36
36
  private buildProxy;
37
+ /**
38
+ * Join the two in-memory halves through the same envelopes used by wire transports. This is
39
+ * intentionally not a controller shortcut: opening the stream still invokes `service`, so the
40
+ * ordinary logging/auth/application filter chain runs before a writer is returned.
41
+ */
42
+ private runStreamingMethod;
43
+ private deliverResponseEnvelope;
44
+ private deliverRequestEnvelope;
45
+ private requireResponseStream;
46
+ private requireRequestStream;
37
47
  private runMethod;
38
48
  }
@@ -89,7 +89,8 @@ let ApiClientFactory = class ApiClientFactory {
89
89
  ? registeredRoutes.map((route) => route.methodName)
90
90
  : Object.keys(endpoints);
91
91
  for (const methodName of methodNames) {
92
- const contractRoute = registeredRoutes?.find((route) => route.methodName === methodName) ?? core_util_1.RouteMetadataFactory.create(apiPrototype, methodName);
92
+ const contractRoute = registeredRoutes?.find((route) => route.methodName === methodName) ??
93
+ core_util_1.RouteMetadataFactory.create(apiPrototype, methodName);
93
94
  const httpMethod = contractRoute.httpMethod;
94
95
  const path = contractRoute.path;
95
96
  // Use the REGISTERED route's metadata — it carries the real controller name AND api
@@ -102,6 +103,9 @@ let ApiClientFactory = class ApiClientFactory {
102
103
  // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
103
104
  proxy[methodName] = async (...args) => {
104
105
  requireActiveContext(routeMeta);
106
+ if (routeMeta.streaming) {
107
+ return this.runStreamingMethod(routeMeta, args, service);
108
+ }
105
109
  // Run the shared mapping even in-process. It validates the exact path/query/body
106
110
  // shape production clients use, then the controller receives the original typed args.
107
111
  const mapped = core_util_1.HttpContractMapper.toWire(routeMeta.path, routeMeta.parameterBindings, routeMeta.bodyParameterIndex, args);
@@ -110,6 +114,88 @@ let ApiClientFactory = class ApiClientFactory {
110
114
  }
111
115
  return proxy;
112
116
  }
117
+ /**
118
+ * Join the two in-memory halves through the same envelopes used by wire transports. This is
119
+ * intentionally not a controller shortcut: opening the stream still invokes `service`, so the
120
+ * ordinary logging/auth/application filter chain runs before a writer is returned.
121
+ */
122
+ // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary
123
+ async runStreamingMethod(routeMeta, requestArgs, service) {
124
+ const streaming = routeMeta.streaming;
125
+ if (!streaming)
126
+ throw new Error('Streaming route metadata is required.');
127
+ const clientResponse = this.requireResponseStream(routeMeta, requestArgs[0]);
128
+ const validator = new core_util_1.StreamEventValidator();
129
+ const serverResponse = new core_util_1.StreamWriter(async (envelope) => this.deliverResponseEnvelope(clientResponse, envelope), (value) => validator.validate(streaming.responseEventClass, value, 'response'), streaming.supportsNonTerminalFailures);
130
+ // A rejected invocation is the open/handshake failure. It deliberately escapes unchanged,
131
+ // matching unary in-process calls and allowing an HTTP adapter to apply its normal mapper.
132
+ const responseWrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, undefined, undefined, [serverResponse]));
133
+ const serverRequest = this.requireRequestStream(routeMeta, responseWrapper.response);
134
+ const clientRequest = new core_util_1.StreamWriter(async (envelope) => this.deliverRequestEnvelope(serverRequest, envelope), (value) => validator.validate(streaming.requestEventClass, value, 'request'), streaming.supportsNonTerminalFailures);
135
+ // webpieces-disable no-any-unknown -- cancellation reasons are deliberately transport-neutral
136
+ clientRequest.onCancel(async (reason) => {
137
+ // Both sides observe one cancellation. Promise.all ensures a faulty callback on one
138
+ // half cannot prevent the other half from being notified.
139
+ await Promise.all([serverResponse.cancel(reason), serverRequest.cancel(reason)]);
140
+ });
141
+ return clientRequest;
142
+ }
143
+ async deliverResponseEnvelope(destination, envelope) {
144
+ switch (envelope.kind) {
145
+ case 'event':
146
+ if (envelope.value === undefined) {
147
+ throw new core_util_1.StreamTransportError('Response event envelope has no value.');
148
+ }
149
+ return destination.event(envelope.value, envelope.correlation);
150
+ case 'failure':
151
+ if (!envelope.error) {
152
+ throw new core_util_1.StreamTransportError('Response failure envelope has no error.');
153
+ }
154
+ return destination.fail(core_util_1.ApiErrorCodec.decode(envelope.error), envelope.correlation, { terminal: envelope.terminal });
155
+ case 'complete':
156
+ return destination.complete();
157
+ }
158
+ }
159
+ async deliverRequestEnvelope(destination, envelope) {
160
+ switch (envelope.kind) {
161
+ case 'event':
162
+ if (envelope.value === undefined) {
163
+ throw new core_util_1.StreamTransportError('Request event envelope has no value.');
164
+ }
165
+ return destination.event(envelope.value, envelope.correlation);
166
+ case 'failure':
167
+ if (!envelope.error) {
168
+ throw new core_util_1.StreamTransportError('Request failure envelope has no error.');
169
+ }
170
+ return destination.fail(core_util_1.ApiErrorCodec.decode(envelope.error), envelope.correlation, { terminal: envelope.terminal });
171
+ case 'complete':
172
+ return destination.complete();
173
+ }
174
+ }
175
+ // webpieces-disable no-any-unknown -- runtime structural check narrows an erased API argument
176
+ requireResponseStream(routeMeta, candidate) {
177
+ if (typeof candidate === 'object' &&
178
+ candidate !== null &&
179
+ typeof Reflect.get(candidate, 'event') === 'function' &&
180
+ typeof Reflect.get(candidate, 'fail') === 'function' &&
181
+ typeof Reflect.get(candidate, 'complete') === 'function' &&
182
+ typeof Reflect.get(candidate, 'onCancel') === 'function') {
183
+ return candidate;
184
+ }
185
+ throw new core_util_1.StreamTransportError(`${routeMeta.apiName}.${routeMeta.methodName} requires a ResponseStream argument.`);
186
+ }
187
+ // webpieces-disable no-any-unknown -- runtime structural check narrows an erased controller result
188
+ requireRequestStream(routeMeta, candidate) {
189
+ if (typeof candidate === 'object' &&
190
+ candidate !== null &&
191
+ typeof Reflect.get(candidate, 'event') === 'function' &&
192
+ typeof Reflect.get(candidate, 'fail') === 'function' &&
193
+ typeof Reflect.get(candidate, 'complete') === 'function' &&
194
+ typeof Reflect.get(candidate, 'cancel') === 'function') {
195
+ return candidate;
196
+ }
197
+ throw new core_util_1.StreamTransportError(`${routeMeta.controllerClassName}.${routeMeta.methodName} did not return a RequestStream.`);
198
+ }
113
199
  // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
114
200
  async runMethod(routeMeta, requestDto, requestArgs,
115
201
  // webpieces-disable no-any-unknown -- filter service carries arbitrary contract response DTOs
@@ -1 +1 @@
1
- {"version":3,"file":"ApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClientFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject } from 'inversify';\nimport {\n getEndpoints,\n HttpContractMapper,\n RouteMetadata,\n RouteMetadataFactory,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { MethodMeta } from './MethodMeta';\nimport { Service } from '@webpieces/core-util';\nimport { WpResponse } from './WpResponse';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiClient, ApiClientProxy } from './ApiClient';\nimport { ClassType } from './ApiRoutingFactory';\n\n/**\n * Every call through the proxy needs an ambient RequestContext, established ABOVE the api boundary.\n * It is NOT auto-created here: manufacturing one silently would hide a missing top-level filter, and\n * every log line, outbound call, and enqueued task under it would quietly lose its request id.\n */\n// webpieces-disable no-function-outside-class -- a guard over ambient state; a class would own nothing\nfunction requireActiveContext(routeMeta: RouteMetadata): void {\n if (RequestContext.isActive()) {\n return;\n }\n throw new Error(\n `${routeMeta.controllerClassName}.${routeMeta.methodName} was called with no active RequestContext. ` +\n `A server transport must wrap each request in RequestContext.run(...) (WebpiecesMiddleware does). ` +\n `In a test, wrap the call yourself: await RequestContext.run(async () => api.foo(req));`,\n );\n}\n\n/**\n * ApiClientFactory - THE piece that wires api → Proxy → filters → controller.\n *\n * For an API prototype (its @ApiPath/@Endpoint decorators) it builds a proxy whose methods\n * invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what\n * createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the\n * express layer binds each method through it. There is no express dependency here, so the proxy\n * is the single invocation path for BOTH in-process (tests) and HTTP.\n *\n * Establishing the request scope is a PRECONDITION of calling in here, never this class's job. The\n * caller above the api boundary opens `RequestContext.run(...)`, publishes the inbound\n * `HttpRequest`, and calls `RequestContextHeaders.fillFromRequest()` to move its headers into the\n * context. `WebpiecesMiddleware` does all three for you; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. This proxy only CHECKS that it happened —\n * manufacturing a context here would hide a missing filter and silently strip every request id.\n *\n * @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).\n */\n@provideFrameworkSingleton()\nexport class ApiClientFactory {\n constructor(@inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl) {}\n\n /**\n * Create an API client proxy (cast to the API interface T). The proxy's methods run the full\n * filter chain + controller; used by tests in-process AND driven by the express adapter.\n */\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T {\n return this.buildProxy(apiPrototype) as T;\n }\n\n /**\n * Reify every registered API as an {@link ApiClient} — the contract + its proxy (the\n * createApiClient object). The transport reads the api's decorators to bind each endpoint to\n * the proxy's matching method, so no route metadata needs to leave here.\n */\n apiClients(): ApiClient[] {\n const routesByApi = new Map<ClassType, RouteMetadata[]>();\n for (const route of this.routeBuilder.getRoutes()) {\n const api = route.definition.apiClass as ClassType;\n const routes = routesByApi.get(api) ?? [];\n routes.push(route.definition.routeMeta);\n routesByApi.set(api, routes);\n }\n // apiClients() just loops createApiClient — the EXACT method tests call — so the platform\n // (HTTP) and tests (in-process) bind the identical proxy, 1-to-1.\n const clients: ApiClient[] = [];\n for (const entry of routesByApi.entries()) {\n const api = entry[0];\n const routes = entry[1];\n // Build only the routes that were actually registered. A @WpAuthLocalOnly method is\n // deliberately absent off-local and must not make mounting the rest of its API fail.\n const client = this.buildProxy(api, routes);\n clients.push(new ApiClient(api, client, routes));\n }\n return clients;\n }\n\n /** Build the proxy record (method name → invoker) from the API prototype's decorators. */\n private buildProxy(\n // webpieces-disable no-any-unknown -- accepts any ClassType / abstract-constructor API prototype\n apiPrototype: any,\n registeredRoutes?: readonly RouteMetadata[],\n ): ApiClientProxy {\n const endpoints = getEndpoints(apiPrototype) || {};\n const proxy: ApiClientProxy = {};\n const methodNames = registeredRoutes\n ? registeredRoutes.map((route: RouteMetadata) => route.methodName)\n : Object.keys(endpoints);\n\n for (const methodName of methodNames) {\n const contractRoute =\n registeredRoutes?.find(\n (route: RouteMetadata) => route.methodName === methodName,\n ) ?? RouteMetadataFactory.create(apiPrototype, methodName);\n const httpMethod = contractRoute.httpMethod;\n const path = contractRoute.path;\n\n // Use the REGISTERED route's metadata — it carries the real controller name AND api\n // name (so logging/recording read the right one); createRouteInvoker composes its chain.\n const routeMeta = this.routeBuilder.getRouteMeta(httpMethod, path);\n if (!routeMeta) {\n throw new Error(\n `No registered route for ${apiPrototype.name}.${methodName} (${httpMethod} ${path}) — call addRoutes(api, controller) first.`,\n );\n }\n const service = this.routeBuilder.createRouteInvoker(httpMethod, path);\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n proxy[methodName] = async (...args: unknown[]): Promise<unknown> => {\n requireActiveContext(routeMeta);\n // Run the shared mapping even in-process. It validates the exact path/query/body\n // shape production clients use, then the controller receives the original typed args.\n const mapped = HttpContractMapper.toWire(\n routeMeta.path,\n routeMeta.parameterBindings,\n routeMeta.bodyParameterIndex,\n args,\n );\n return this.runMethod(\n routeMeta,\n mapped.body === undefined ? args : mapped.body,\n args,\n service,\n );\n };\n }\n\n return proxy;\n }\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private async runMethod(\n routeMeta: RouteMetadata,\n requestDto: unknown,\n requestArgs: unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract response DTOs\n service: Service<MethodMeta, WpResponse<unknown>>,\n // webpieces-disable no-any-unknown -- API return type is erased at the routing proxy boundary\n ): Promise<unknown> {\n const responseWrapper = await service.invoke(\n new MethodMeta(routeMeta, requestDto, undefined, requestArgs),\n );\n return responseWrapper.response;\n }\n}\n"]}
1
+ {"version":3,"file":"ApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClientFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject } from 'inversify';\nimport {\n ApiErrorCodec,\n DtoValue,\n getEndpoints,\n HttpContractMapper,\n RequestStream,\n ResponseStream,\n RouteMetadata,\n RouteMetadataFactory,\n StreamEnvelope,\n StreamEventValidator,\n StreamTransportError,\n StreamWriter,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { MethodMeta } from './MethodMeta';\nimport { Service } from '@webpieces/core-util';\nimport { WpResponse } from './WpResponse';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiClient, ApiClientProxy } from './ApiClient';\nimport { ClassType } from './ApiRoutingFactory';\n\n/**\n * Every call through the proxy needs an ambient RequestContext, established ABOVE the api boundary.\n * It is NOT auto-created here: manufacturing one silently would hide a missing top-level filter, and\n * every log line, outbound call, and enqueued task under it would quietly lose its request id.\n */\n// webpieces-disable no-function-outside-class -- a guard over ambient state; a class would own nothing\nfunction requireActiveContext(routeMeta: RouteMetadata): void {\n if (RequestContext.isActive()) {\n return;\n }\n throw new Error(\n `${routeMeta.controllerClassName}.${routeMeta.methodName} was called with no active RequestContext. ` +\n `A server transport must wrap each request in RequestContext.run(...) (WebpiecesMiddleware does). ` +\n `In a test, wrap the call yourself: await RequestContext.run(async () => api.foo(req));`,\n );\n}\n\n/**\n * ApiClientFactory - THE piece that wires api → Proxy → filters → controller.\n *\n * For an API prototype (its @ApiPath/@Endpoint decorators) it builds a proxy whose methods\n * invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what\n * createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the\n * express layer binds each method through it. There is no express dependency here, so the proxy\n * is the single invocation path for BOTH in-process (tests) and HTTP.\n *\n * Establishing the request scope is a PRECONDITION of calling in here, never this class's job. The\n * caller above the api boundary opens `RequestContext.run(...)`, publishes the inbound\n * `HttpRequest`, and calls `RequestContextHeaders.fillFromRequest()` to move its headers into the\n * context. `WebpiecesMiddleware` does all three for you; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. This proxy only CHECKS that it happened —\n * manufacturing a context here would hide a missing filter and silently strip every request id.\n *\n * @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).\n */\n@provideFrameworkSingleton()\nexport class ApiClientFactory {\n constructor(@inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl) {}\n\n /**\n * Create an API client proxy (cast to the API interface T). The proxy's methods run the full\n * filter chain + controller; used by tests in-process AND driven by the express adapter.\n */\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T {\n return this.buildProxy(apiPrototype) as T;\n }\n\n /**\n * Reify every registered API as an {@link ApiClient} — the contract + its proxy (the\n * createApiClient object). The transport reads the api's decorators to bind each endpoint to\n * the proxy's matching method, so no route metadata needs to leave here.\n */\n apiClients(): ApiClient[] {\n const routesByApi = new Map<ClassType, RouteMetadata[]>();\n for (const route of this.routeBuilder.getRoutes()) {\n const api = route.definition.apiClass as ClassType;\n const routes = routesByApi.get(api) ?? [];\n routes.push(route.definition.routeMeta);\n routesByApi.set(api, routes);\n }\n // apiClients() just loops createApiClient — the EXACT method tests call — so the platform\n // (HTTP) and tests (in-process) bind the identical proxy, 1-to-1.\n const clients: ApiClient[] = [];\n for (const entry of routesByApi.entries()) {\n const api = entry[0];\n const routes = entry[1];\n // Build only the routes that were actually registered. A @WpAuthLocalOnly method is\n // deliberately absent off-local and must not make mounting the rest of its API fail.\n const client = this.buildProxy(api, routes);\n clients.push(new ApiClient(api, client, routes));\n }\n return clients;\n }\n\n /** Build the proxy record (method name → invoker) from the API prototype's decorators. */\n private buildProxy(\n // webpieces-disable no-any-unknown -- accepts any ClassType / abstract-constructor API prototype\n apiPrototype: any,\n registeredRoutes?: readonly RouteMetadata[],\n ): ApiClientProxy {\n const endpoints = getEndpoints(apiPrototype) || {};\n const proxy: ApiClientProxy = {};\n const methodNames = registeredRoutes\n ? registeredRoutes.map((route: RouteMetadata) => route.methodName)\n : Object.keys(endpoints);\n\n for (const methodName of methodNames) {\n const contractRoute =\n registeredRoutes?.find((route: RouteMetadata) => route.methodName === methodName) ??\n RouteMetadataFactory.create(apiPrototype, methodName);\n const httpMethod = contractRoute.httpMethod;\n const path = contractRoute.path;\n\n // Use the REGISTERED route's metadata — it carries the real controller name AND api\n // name (so logging/recording read the right one); createRouteInvoker composes its chain.\n const routeMeta = this.routeBuilder.getRouteMeta(httpMethod, path);\n if (!routeMeta) {\n throw new Error(\n `No registered route for ${apiPrototype.name}.${methodName} (${httpMethod} ${path}) — call addRoutes(api, controller) first.`,\n );\n }\n const service = this.routeBuilder.createRouteInvoker(httpMethod, path);\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n proxy[methodName] = async (...args: unknown[]): Promise<unknown> => {\n requireActiveContext(routeMeta);\n if (routeMeta.streaming) {\n return this.runStreamingMethod(routeMeta, args, service);\n }\n // Run the shared mapping even in-process. It validates the exact path/query/body\n // shape production clients use, then the controller receives the original typed args.\n const mapped = HttpContractMapper.toWire(\n routeMeta.path,\n routeMeta.parameterBindings,\n routeMeta.bodyParameterIndex,\n args,\n );\n return this.runMethod(\n routeMeta,\n mapped.body === undefined ? args : mapped.body,\n args,\n service,\n );\n };\n }\n\n return proxy;\n }\n\n /**\n * Join the two in-memory halves through the same envelopes used by wire transports. This is\n * intentionally not a controller shortcut: opening the stream still invokes `service`, so the\n * ordinary logging/auth/application filter chain runs before a writer is returned.\n */\n // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n private async runStreamingMethod(\n routeMeta: RouteMetadata,\n requestArgs: readonly unknown[],\n service: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<RequestStream<DtoValue>> {\n const streaming = routeMeta.streaming;\n if (!streaming) throw new Error('Streaming route metadata is required.');\n const clientResponse = this.requireResponseStream(routeMeta, requestArgs[0]);\n const validator = new StreamEventValidator();\n const serverResponse = new StreamWriter<DtoValue>(\n async (envelope: StreamEnvelope<DtoValue>): Promise<void> =>\n this.deliverResponseEnvelope(clientResponse, envelope),\n (value: DtoValue): void =>\n validator.validate(streaming.responseEventClass, value, 'response'),\n streaming.supportsNonTerminalFailures,\n );\n\n // A rejected invocation is the open/handshake failure. It deliberately escapes unchanged,\n // matching unary in-process calls and allowing an HTTP adapter to apply its normal mapper.\n const responseWrapper = await service.invoke(\n new MethodMeta(routeMeta, undefined, undefined, [serverResponse]),\n );\n const serverRequest = this.requireRequestStream(routeMeta, responseWrapper.response);\n const clientRequest = new StreamWriter<DtoValue>(\n async (envelope: StreamEnvelope<DtoValue>): Promise<void> =>\n this.deliverRequestEnvelope(serverRequest, envelope),\n (value: DtoValue): void =>\n validator.validate(streaming.requestEventClass, value, 'request'),\n streaming.supportsNonTerminalFailures,\n );\n // webpieces-disable no-any-unknown -- cancellation reasons are deliberately transport-neutral\n clientRequest.onCancel(async (reason?: unknown): Promise<void> => {\n // Both sides observe one cancellation. Promise.all ensures a faulty callback on one\n // half cannot prevent the other half from being notified.\n await Promise.all([serverResponse.cancel(reason), serverRequest.cancel(reason)]);\n });\n return clientRequest;\n }\n\n private async deliverResponseEnvelope(\n destination: ResponseStream<DtoValue>,\n envelope: StreamEnvelope<DtoValue>,\n ): Promise<void> {\n switch (envelope.kind) {\n case 'event':\n if (envelope.value === undefined) {\n throw new StreamTransportError('Response event envelope has no value.');\n }\n return destination.event(envelope.value, envelope.correlation);\n case 'failure':\n if (!envelope.error) {\n throw new StreamTransportError('Response failure envelope has no error.');\n }\n return destination.fail(\n ApiErrorCodec.decode(envelope.error),\n envelope.correlation,\n { terminal: envelope.terminal },\n );\n case 'complete':\n return destination.complete();\n }\n }\n\n private async deliverRequestEnvelope(\n destination: RequestStream<DtoValue>,\n envelope: StreamEnvelope<DtoValue>,\n ): Promise<void> {\n switch (envelope.kind) {\n case 'event':\n if (envelope.value === undefined) {\n throw new StreamTransportError('Request event envelope has no value.');\n }\n return destination.event(envelope.value, envelope.correlation);\n case 'failure':\n if (!envelope.error) {\n throw new StreamTransportError('Request failure envelope has no error.');\n }\n return destination.fail(\n ApiErrorCodec.decode(envelope.error),\n envelope.correlation,\n { terminal: envelope.terminal },\n );\n case 'complete':\n return destination.complete();\n }\n }\n\n // webpieces-disable no-any-unknown -- runtime structural check narrows an erased API argument\n private requireResponseStream(\n routeMeta: RouteMetadata,\n candidate: unknown,\n ): ResponseStream<DtoValue> {\n if (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof Reflect.get(candidate, 'event') === 'function' &&\n typeof Reflect.get(candidate, 'fail') === 'function' &&\n typeof Reflect.get(candidate, 'complete') === 'function' &&\n typeof Reflect.get(candidate, 'onCancel') === 'function'\n ) {\n return candidate as ResponseStream<DtoValue>;\n }\n throw new StreamTransportError(\n `${routeMeta.apiName}.${routeMeta.methodName} requires a ResponseStream argument.`,\n );\n }\n\n // webpieces-disable no-any-unknown -- runtime structural check narrows an erased controller result\n private requireRequestStream(\n routeMeta: RouteMetadata,\n candidate: unknown,\n ): RequestStream<DtoValue> {\n if (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof Reflect.get(candidate, 'event') === 'function' &&\n typeof Reflect.get(candidate, 'fail') === 'function' &&\n typeof Reflect.get(candidate, 'complete') === 'function' &&\n typeof Reflect.get(candidate, 'cancel') === 'function'\n ) {\n return candidate as RequestStream<DtoValue>;\n }\n throw new StreamTransportError(\n `${routeMeta.controllerClassName}.${routeMeta.methodName} did not return a RequestStream.`,\n );\n }\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private async runMethod(\n routeMeta: RouteMetadata,\n requestDto: unknown,\n requestArgs: unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract response DTOs\n service: Service<MethodMeta, WpResponse<unknown>>,\n // webpieces-disable no-any-unknown -- API return type is erased at the routing proxy boundary\n ): Promise<unknown> {\n const responseWrapper = await service.invoke(\n new MethodMeta(routeMeta, requestDto, undefined, requestArgs),\n );\n return responseWrapper.response;\n }\n}\n"]}