@webpieces/http-routing 0.4.838 → 0.4.840
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.
|
|
3
|
+
"version": "0.4.840",
|
|
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.
|
|
26
|
-
"@webpieces/core-util": "0.4.
|
|
27
|
-
"@webpieces/gcp-identity": "0.4.
|
|
25
|
+
"@webpieces/core-context": "0.4.840",
|
|
26
|
+
"@webpieces/core-util": "0.4.840",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.840",
|
|
28
28
|
"inversify": "7.10.4",
|
|
29
29
|
"jsonwebtoken": "9.0.2",
|
|
30
30
|
"minimatch": "10.0.1"
|
|
@@ -40,7 +40,8 @@ export declare class ApiClientFactory {
|
|
|
40
40
|
* ordinary logging/auth/application filter chain runs before a writer is returned.
|
|
41
41
|
*/
|
|
42
42
|
private runStreamingMethod;
|
|
43
|
-
private
|
|
43
|
+
private runRequestStreamingMethod;
|
|
44
|
+
private runResponseStreamingMethod;
|
|
44
45
|
private deliverRequestEnvelope;
|
|
45
46
|
private requireResponseStream;
|
|
46
47
|
private requireRequestStream;
|
package/src/ApiClientFactory.js
CHANGED
|
@@ -8,6 +8,7 @@ const core_context_1 = require("@webpieces/core-context");
|
|
|
8
8
|
const MethodMeta_1 = require("./MethodMeta");
|
|
9
9
|
const RouteBuilderImpl_1 = require("./RouteBuilderImpl");
|
|
10
10
|
const ApiClient_1 = require("./ApiClient");
|
|
11
|
+
const InProcessStreamExchange_1 = require("./InProcessStreamExchange");
|
|
11
12
|
/**
|
|
12
13
|
* Every call through the proxy needs an ambient RequestContext, established ABOVE the api boundary.
|
|
13
14
|
* It is NOT auto-created here: manufacturing one silently would hide a missing top-level filter, and
|
|
@@ -120,41 +121,84 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
120
121
|
* ordinary logging/auth/application filter chain runs before a writer is returned.
|
|
121
122
|
*/
|
|
122
123
|
// webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary
|
|
123
|
-
async runStreamingMethod(routeMeta,
|
|
124
|
+
async runStreamingMethod(routeMeta,
|
|
125
|
+
// webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary
|
|
126
|
+
requestArgs,
|
|
127
|
+
// webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses
|
|
128
|
+
service) {
|
|
124
129
|
const streaming = routeMeta.streaming;
|
|
125
130
|
if (!streaming)
|
|
126
131
|
throw new Error('Streaming route metadata is required.');
|
|
127
|
-
|
|
132
|
+
if (streaming.direction === core_util_1.StreamDirection.RESPONSE) {
|
|
133
|
+
return this.runResponseStreamingMethod(routeMeta, requestArgs, service);
|
|
134
|
+
}
|
|
135
|
+
if (streaming.direction === core_util_1.StreamDirection.REQUEST) {
|
|
136
|
+
return this.runRequestStreamingMethod(routeMeta, requestArgs, service);
|
|
137
|
+
}
|
|
138
|
+
if (streaming.initialRequestSchema) {
|
|
139
|
+
new core_util_1.StreamEventValidator().validate(streaming.initialRequestSchema, requestArgs[0], 'request');
|
|
140
|
+
}
|
|
141
|
+
const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);
|
|
128
142
|
const validator = new core_util_1.StreamEventValidator();
|
|
129
|
-
const
|
|
143
|
+
const exchange = new InProcessStreamExchange_1.InProcessStreamExchange(clientResponse, streaming.responseSchema);
|
|
144
|
+
const serverResponse = exchange.response;
|
|
130
145
|
// A rejected invocation is the open/handshake failure. It deliberately escapes unchanged,
|
|
131
146
|
// 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,
|
|
147
|
+
const responseWrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse]));
|
|
133
148
|
const serverRequest = this.requireRequestStream(routeMeta, responseWrapper.response);
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
const initialResponse = serverRequest.getInitialResponse();
|
|
150
|
+
const events = new core_context_1.StreamEventContext();
|
|
151
|
+
if (streaming.initialResponseSchema) {
|
|
152
|
+
validator.validate(streaming.initialResponseSchema, initialResponse, 'response');
|
|
153
|
+
}
|
|
154
|
+
exchange.accept(serverRequest);
|
|
155
|
+
const clientRequest = new core_util_1.StreamWriter(async (envelope) => events.run(() => this.deliverRequestEnvelope(serverRequest, envelope)), (value) => validator.validate(streaming.requestSchema, value, 'request'), initialResponse, (error) => exchange.cancel(error));
|
|
156
|
+
exchange.clientRequest = clientRequest;
|
|
141
157
|
return clientRequest;
|
|
142
158
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
// webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary
|
|
160
|
+
async runRequestStreamingMethod(routeMeta,
|
|
161
|
+
// webpieces-disable no-any-unknown -- stream initial DTO is erased at the routing boundary
|
|
162
|
+
requestArgs,
|
|
163
|
+
// webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses
|
|
164
|
+
service) {
|
|
165
|
+
const streaming = routeMeta.streaming;
|
|
166
|
+
const validator = new core_util_1.StreamEventValidator();
|
|
167
|
+
if (streaming.initialRequestSchema) {
|
|
168
|
+
validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');
|
|
169
|
+
}
|
|
170
|
+
const wrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0]]));
|
|
171
|
+
const serverRequest = this.requireRequestStream(routeMeta, wrapper.response);
|
|
172
|
+
const initialResponse = serverRequest.getInitialResponse();
|
|
173
|
+
const events = new core_context_1.StreamEventContext();
|
|
174
|
+
if (streaming.initialResponseSchema) {
|
|
175
|
+
validator.validate(streaming.initialResponseSchema, initialResponse, 'response');
|
|
176
|
+
}
|
|
177
|
+
return new core_util_1.StreamWriter(async (envelope) => events.run(() => this.deliverRequestEnvelope(serverRequest, envelope)), (value) => validator.validate(streaming.requestSchema, value, 'request'), initialResponse, (error) => serverRequest.cancel(error));
|
|
178
|
+
}
|
|
179
|
+
// webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary
|
|
180
|
+
async runResponseStreamingMethod(routeMeta,
|
|
181
|
+
// webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary
|
|
182
|
+
requestArgs,
|
|
183
|
+
// webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses
|
|
184
|
+
service) {
|
|
185
|
+
if (requestArgs.length !== 2) {
|
|
186
|
+
throw new core_util_1.StreamTransportError(`${routeMeta.apiName}.${routeMeta.methodName} RESPONSE streaming requires (InitialRequest, ResponseStream).`);
|
|
157
187
|
}
|
|
188
|
+
const streaming = routeMeta.streaming;
|
|
189
|
+
const validator = new core_util_1.StreamEventValidator();
|
|
190
|
+
if (streaming.initialRequestSchema) {
|
|
191
|
+
validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');
|
|
192
|
+
}
|
|
193
|
+
const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);
|
|
194
|
+
const exchange = new InProcessStreamExchange_1.InProcessStreamExchange(clientResponse, streaming.responseSchema);
|
|
195
|
+
const serverResponse = exchange.response;
|
|
196
|
+
const wrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse]));
|
|
197
|
+
if (streaming.initialResponseSchema) {
|
|
198
|
+
validator.validate(streaming.initialResponseSchema, wrapper.response, 'response');
|
|
199
|
+
}
|
|
200
|
+
exchange.accept();
|
|
201
|
+
return wrapper.response;
|
|
158
202
|
}
|
|
159
203
|
async deliverRequestEnvelope(destination, envelope) {
|
|
160
204
|
switch (envelope.kind) {
|
|
@@ -167,9 +211,9 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
167
211
|
if (!envelope.error) {
|
|
168
212
|
throw new core_util_1.StreamTransportError('Request failure envelope has no error.');
|
|
169
213
|
}
|
|
170
|
-
return destination.
|
|
214
|
+
return destination.cancel(core_util_1.ApiErrorCodec.decode(envelope.error));
|
|
171
215
|
case 'complete':
|
|
172
|
-
return destination.
|
|
216
|
+
return destination.close();
|
|
173
217
|
}
|
|
174
218
|
}
|
|
175
219
|
// webpieces-disable no-any-unknown -- runtime structural check narrows an erased API argument
|
|
@@ -177,9 +221,8 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
177
221
|
if (typeof candidate === 'object' &&
|
|
178
222
|
candidate !== null &&
|
|
179
223
|
typeof Reflect.get(candidate, 'event') === 'function' &&
|
|
180
|
-
typeof Reflect.get(candidate, '
|
|
181
|
-
typeof Reflect.get(candidate, '
|
|
182
|
-
typeof Reflect.get(candidate, 'onCancel') === 'function') {
|
|
224
|
+
typeof Reflect.get(candidate, 'close') === 'function' &&
|
|
225
|
+
typeof Reflect.get(candidate, 'cancel') === 'function') {
|
|
183
226
|
return candidate;
|
|
184
227
|
}
|
|
185
228
|
throw new core_util_1.StreamTransportError(`${routeMeta.apiName}.${routeMeta.methodName} requires a ResponseStream argument.`);
|
|
@@ -189,8 +232,8 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
189
232
|
if (typeof candidate === 'object' &&
|
|
190
233
|
candidate !== null &&
|
|
191
234
|
typeof Reflect.get(candidate, 'event') === 'function' &&
|
|
192
|
-
typeof Reflect.get(candidate, '
|
|
193
|
-
typeof Reflect.get(candidate, '
|
|
235
|
+
typeof Reflect.get(candidate, 'getInitialResponse') === 'function' &&
|
|
236
|
+
typeof Reflect.get(candidate, 'close') === 'function' &&
|
|
194
237
|
typeof Reflect.get(candidate, 'cancel') === 'function') {
|
|
195
238
|
return candidate;
|
|
196
239
|
}
|
|
@@ -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 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.responseSchema, 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.requestSchema, 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"]}
|
|
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 StreamDirection,\n RouteMetadata,\n RouteMetadataFactory,\n StreamEnvelope,\n StreamEventValidator,\n StreamTransportError,\n StreamWriter,\n} from '@webpieces/core-util';\nimport {\n provideFrameworkSingleton,\n RequestContext,\n StreamEventContext,\n} 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';\nimport { InProcessStreamExchange } from './InProcessStreamExchange';\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 // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n // webpieces-disable no-any-unknown -- one of the three streaming contract return shapes\n ): Promise<unknown> {\n const streaming = routeMeta.streaming;\n if (!streaming) throw new Error('Streaming route metadata is required.');\n if (streaming.direction === StreamDirection.RESPONSE) {\n return this.runResponseStreamingMethod(routeMeta, requestArgs, service);\n }\n if (streaming.direction === StreamDirection.REQUEST) {\n return this.runRequestStreamingMethod(routeMeta, requestArgs, service);\n }\n if (streaming.initialRequestSchema) {\n new StreamEventValidator().validate(\n streaming.initialRequestSchema,\n requestArgs[0],\n 'request',\n );\n }\n const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);\n const validator = new StreamEventValidator();\n const exchange = new InProcessStreamExchange(clientResponse, streaming.responseSchema!);\n const serverResponse = exchange.response;\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, requestArgs[0], undefined, [requestArgs[0], serverResponse]),\n );\n const serverRequest = this.requireRequestStream(routeMeta, responseWrapper.response);\n const initialResponse = serverRequest.getInitialResponse();\n const events = new StreamEventContext();\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, initialResponse, 'response');\n }\n exchange.accept(serverRequest);\n const clientRequest = new StreamWriter<DtoValue>(\n async (envelope: StreamEnvelope<DtoValue>): Promise<void> =>\n events.run(\n (): Promise<void> => this.deliverRequestEnvelope(serverRequest, envelope),\n ),\n (value: DtoValue): void =>\n validator.validate(streaming.requestSchema!, value, 'request'),\n initialResponse,\n (error?: Error): Promise<void> => exchange.cancel(error),\n );\n exchange.clientRequest = clientRequest;\n return clientRequest;\n }\n\n // webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary\n private async runRequestStreamingMethod(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- stream initial DTO is erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<RequestStream<DtoValue, DtoValue>> {\n const streaming = routeMeta.streaming!;\n const validator = new StreamEventValidator();\n if (streaming.initialRequestSchema) {\n validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');\n }\n const wrapper = await service.invoke(\n new MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0]]),\n );\n const serverRequest = this.requireRequestStream(routeMeta, wrapper.response);\n const initialResponse = serverRequest.getInitialResponse();\n const events = new StreamEventContext();\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, initialResponse, 'response');\n }\n return new StreamWriter<DtoValue>(\n async (envelope: StreamEnvelope<DtoValue>): Promise<void> =>\n events.run(\n (): Promise<void> => this.deliverRequestEnvelope(serverRequest, envelope),\n ),\n (value: DtoValue): void =>\n validator.validate(streaming.requestSchema!, value, 'request'),\n initialResponse,\n (error?: Error): Promise<void> => serverRequest.cancel(error),\n );\n }\n\n // webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary\n private async runResponseStreamingMethod(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n // webpieces-disable no-any-unknown -- response initial DTO is contract-erased\n ): Promise<unknown> {\n if (requestArgs.length !== 2) {\n throw new StreamTransportError(\n `${routeMeta.apiName}.${routeMeta.methodName} RESPONSE streaming requires (InitialRequest, ResponseStream).`,\n );\n }\n const streaming = routeMeta.streaming!;\n const validator = new StreamEventValidator();\n if (streaming.initialRequestSchema) {\n validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');\n }\n const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);\n const exchange = new InProcessStreamExchange(clientResponse, streaming.responseSchema!);\n const serverResponse = exchange.response;\n const wrapper = await service.invoke(\n new MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse]),\n );\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, wrapper.response, 'response');\n }\n exchange.accept();\n return wrapper.response;\n }\n\n private async deliverRequestEnvelope(\n destination: RequestStream<DtoValue, 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.cancel(ApiErrorCodec.decode(envelope.error));\n case 'complete':\n return destination.close();\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, 'close') === 'function' &&\n typeof Reflect.get(candidate, 'cancel') === '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, DtoValue> {\n if (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof Reflect.get(candidate, 'event') === 'function' &&\n typeof Reflect.get(candidate, 'getInitialResponse') === 'function' &&\n typeof Reflect.get(candidate, 'close') === 'function' &&\n typeof Reflect.get(candidate, 'cancel') === 'function'\n ) {\n return candidate as RequestStream<DtoValue, 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"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ApiJsonSchema, DtoValue, RequestStream, ResponseStream, StreamWriter } from '@webpieces/core-util';
|
|
2
|
+
/** One logical exchange coordinates both directions without reopening its initial context. */
|
|
3
|
+
export declare class InProcessStreamExchange {
|
|
4
|
+
private readonly destination;
|
|
5
|
+
readonly response: StreamWriter<DtoValue>;
|
|
6
|
+
clientRequest?: RequestStream<DtoValue, DtoValue>;
|
|
7
|
+
private serverRequest?;
|
|
8
|
+
private events?;
|
|
9
|
+
private cancelled;
|
|
10
|
+
private readonly notifyClient;
|
|
11
|
+
constructor(destination: ResponseStream<DtoValue>, schema: ApiJsonSchema);
|
|
12
|
+
accept(serverRequest?: RequestStream<DtoValue, DtoValue>): void;
|
|
13
|
+
cancel(error?: Error): Promise<void>;
|
|
14
|
+
private deliver;
|
|
15
|
+
private requireAccepted;
|
|
16
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InProcessStreamExchange = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
6
|
+
/** One logical exchange coordinates both directions without reopening its initial context. */
|
|
7
|
+
class InProcessStreamExchange {
|
|
8
|
+
destination;
|
|
9
|
+
response;
|
|
10
|
+
clientRequest;
|
|
11
|
+
serverRequest;
|
|
12
|
+
events;
|
|
13
|
+
cancelled = false;
|
|
14
|
+
notifyClient;
|
|
15
|
+
constructor(destination, schema) {
|
|
16
|
+
this.destination = destination;
|
|
17
|
+
this.notifyClient = destination.cancel.bind(destination);
|
|
18
|
+
destination.cancel = (error) => this.cancel(error);
|
|
19
|
+
const validator = new core_util_1.StreamEventValidator();
|
|
20
|
+
this.response = new core_util_1.StreamWriter((record) => this.deliver(record), (value) => {
|
|
21
|
+
this.requireAccepted();
|
|
22
|
+
validator.validate(schema, value, 'response');
|
|
23
|
+
}, undefined, (error) => this.cancel(error));
|
|
24
|
+
}
|
|
25
|
+
accept(serverRequest) {
|
|
26
|
+
this.serverRequest = serverRequest;
|
|
27
|
+
this.events = new core_context_1.StreamEventContext();
|
|
28
|
+
}
|
|
29
|
+
async cancel(error) {
|
|
30
|
+
if (this.cancelled)
|
|
31
|
+
return;
|
|
32
|
+
this.cancelled = true;
|
|
33
|
+
await Promise.all([
|
|
34
|
+
this.notifyClient(error),
|
|
35
|
+
this.serverRequest?.cancel(error),
|
|
36
|
+
this.clientRequest?.cancel(error),
|
|
37
|
+
this.response.cancel(error),
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
async deliver(record) {
|
|
41
|
+
this.requireAccepted();
|
|
42
|
+
if (record.kind === 'complete')
|
|
43
|
+
return this.destination.close();
|
|
44
|
+
await this.events.runOutbound(() => this.destination.event(record.value));
|
|
45
|
+
}
|
|
46
|
+
requireAccepted() {
|
|
47
|
+
if (!this.events)
|
|
48
|
+
throw new core_util_1.StreamTransportError('Response events require a successful initial response.');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.InProcessStreamExchange = InProcessStreamExchange;
|
|
52
|
+
//# sourceMappingURL=InProcessStreamExchange.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InProcessStreamExchange.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/InProcessStreamExchange.ts"],"names":[],"mappings":";;;AAAA,oDAS8B;AAC9B,0DAA6D;AAE7D,8FAA8F;AAC9F,MAAa,uBAAuB;IASX;IARZ,QAAQ,CAAyB;IAC1C,aAAa,CAAqC;IAC1C,aAAa,CAAqC;IAClD,MAAM,CAAsB;IAC5B,SAAS,GAAG,KAAK,CAAC;IACT,YAAY,CAAmC;IAEhE,YACqB,WAAqC,EACtD,MAAqB;QADJ,gBAAW,GAAX,WAAW,CAA0B;QAGtD,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACzD,WAAW,CAAC,MAAM,GAAG,CAAC,KAAa,EAAiB,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1E,MAAM,SAAS,GAAG,IAAI,gCAAoB,EAAE,CAAC;QAC7C,IAAI,CAAC,QAAQ,GAAG,IAAI,wBAAY,CAC5B,CAAC,MAAgC,EAAiB,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EACzE,CAAC,KAAe,EAAQ,EAAE;YACtB,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;QAClD,CAAC,EACD,SAAS,EACT,CAAC,KAAa,EAAiB,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CAAC;IACN,CAAC;IAED,MAAM,CAAC,aAAiD;QACpD,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,IAAI,iCAAkB,EAAE,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAAa;QACtB,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,MAAM,OAAO,CAAC,GAAG,CAAC;YACd,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;YACxB,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,KAAK,CAAC;YACjC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,KAAK,CAAC;YACjC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC;SAC9B,CAAC,CAAC;IACP,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,MAAgC;QAClD,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QAChE,MAAM,IAAI,CAAC,MAAO,CAAC,WAAW,CAAC,GAAkB,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,KAAM,CAAC,CAAC,CAAC;IAC/F,CAAC;IAEO,eAAe;QACnB,IAAI,CAAC,IAAI,CAAC,MAAM;YACZ,MAAM,IAAI,gCAAoB,CAC1B,wDAAwD,CAC3D,CAAC;IACV,CAAC;CACJ;AAtDD,0DAsDC","sourcesContent":["import {\n ApiJsonSchema,\n DtoValue,\n RequestStream,\n ResponseStream,\n StreamEnvelope,\n StreamEventValidator,\n StreamTransportError,\n StreamWriter,\n} from '@webpieces/core-util';\nimport { StreamEventContext } from '@webpieces/core-context';\n\n/** One logical exchange coordinates both directions without reopening its initial context. */\nexport class InProcessStreamExchange {\n readonly response: StreamWriter<DtoValue>;\n clientRequest?: RequestStream<DtoValue, DtoValue>;\n private serverRequest?: RequestStream<DtoValue, DtoValue>;\n private events?: StreamEventContext;\n private cancelled = false;\n private readonly notifyClient: (error?: Error) => Promise<void>;\n\n constructor(\n private readonly destination: ResponseStream<DtoValue>,\n schema: ApiJsonSchema,\n ) {\n this.notifyClient = destination.cancel.bind(destination);\n destination.cancel = (error?: Error): Promise<void> => this.cancel(error);\n const validator = new StreamEventValidator();\n this.response = new StreamWriter<DtoValue>(\n (record: StreamEnvelope<DtoValue>): Promise<void> => this.deliver(record),\n (value: DtoValue): void => {\n this.requireAccepted();\n validator.validate(schema, value, 'response');\n },\n undefined,\n (error?: Error): Promise<void> => this.cancel(error),\n );\n }\n\n accept(serverRequest?: RequestStream<DtoValue, DtoValue>): void {\n this.serverRequest = serverRequest;\n this.events = new StreamEventContext();\n }\n\n async cancel(error?: Error): Promise<void> {\n if (this.cancelled) return;\n this.cancelled = true;\n await Promise.all([\n this.notifyClient(error),\n this.serverRequest?.cancel(error),\n this.clientRequest?.cancel(error),\n this.response.cancel(error),\n ]);\n }\n\n private async deliver(record: StreamEnvelope<DtoValue>): Promise<void> {\n this.requireAccepted();\n if (record.kind === 'complete') return this.destination.close();\n await this.events!.runOutbound((): Promise<void> => this.destination.event(record.value!));\n }\n\n private requireAccepted(): void {\n if (!this.events)\n throw new StreamTransportError(\n 'Response events require a successful initial response.',\n );\n }\n}\n"]}
|