@webpieces/http-server 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/README.md +20 -0
- package/package.json +5 -5
- package/src/ApiErrorHttpMapper.d.ts +1 -3
- package/src/ApiErrorHttpMapper.js +3 -30
- package/src/ApiErrorHttpMapper.js.map +1 -1
- package/src/ExpressWrapper.d.ts +14 -14
- package/src/ExpressWrapper.js +11 -40
- package/src/ExpressWrapper.js.map +1 -1
- package/src/StreamExpressWrapper.d.ts +22 -0
- package/src/StreamExpressWrapper.js +293 -0
- package/src/StreamExpressWrapper.js.map +1 -0
- package/src/WebpiecesExpressRouter.d.ts +18 -0
- package/src/WebpiecesExpressRouter.js +28 -1
- package/src/WebpiecesExpressRouter.js.map +1 -1
- package/src/WebpiecesMiddleware.d.ts +7 -1
- package/src/WebpiecesMiddleware.js +10 -1
- package/src/WebpiecesMiddleware.js.map +1 -1
- package/src/body/PreConsumedBodyReader.d.ts +24 -0
- package/src/body/PreConsumedBodyReader.js +46 -0
- package/src/body/PreConsumedBodyReader.js.map +1 -0
- package/src/body/RequestBodyReader.d.ts +27 -0
- package/src/body/RequestBodyReader.js +3 -0
- package/src/body/RequestBodyReader.js.map +1 -0
- package/src/body/StreamBodyReader.d.ts +36 -0
- package/src/body/StreamBodyReader.js +82 -0
- package/src/body/StreamBodyReader.js.map +1 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +5 -1
- package/src/index.js.map +1 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StreamExpressWrapper = void 0;
|
|
4
|
+
const node_events_1 = require("node:events");
|
|
5
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
+
const ExpressWrapper_1 = require("./ExpressWrapper");
|
|
8
|
+
const KEEP_ALIVE_MILLIS = 15_000;
|
|
9
|
+
const MAX_LINE_BYTES = 1024 * 1024;
|
|
10
|
+
const MAX_HANDSHAKE_BUFFER_BYTES = 64 * 1024;
|
|
11
|
+
/** Owns ordered SSE writes and the small pre-handshake response buffer. */
|
|
12
|
+
class SseResponseChannel {
|
|
13
|
+
res;
|
|
14
|
+
opened = false;
|
|
15
|
+
ended = false;
|
|
16
|
+
queuedBytes = 0;
|
|
17
|
+
queued = [];
|
|
18
|
+
finish = () => undefined;
|
|
19
|
+
pendingWrite = Promise.resolve();
|
|
20
|
+
finished = new Promise((resolve) => {
|
|
21
|
+
this.finish = resolve;
|
|
22
|
+
});
|
|
23
|
+
writer;
|
|
24
|
+
constructor(res, route, validator) {
|
|
25
|
+
this.res = res;
|
|
26
|
+
this.writer = new core_util_1.StreamWriter((envelope) => this.deliver(envelope), (value) => validator.validate(route.streaming.responseEventClass, value, 'response'), route.streaming?.supportsNonTerminalFailures);
|
|
27
|
+
}
|
|
28
|
+
isEnded() {
|
|
29
|
+
return this.ended;
|
|
30
|
+
}
|
|
31
|
+
async open() {
|
|
32
|
+
this.res.status(200);
|
|
33
|
+
this.res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
34
|
+
this.res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
35
|
+
this.res.setHeader('Connection', 'keep-alive');
|
|
36
|
+
this.res.setHeader('X-Accel-Buffering', 'no');
|
|
37
|
+
this.res.flushHeaders();
|
|
38
|
+
this.opened = true;
|
|
39
|
+
for (const frame of this.queued)
|
|
40
|
+
await this.enqueueWrite(frame);
|
|
41
|
+
this.queued.length = 0;
|
|
42
|
+
if (this.ended)
|
|
43
|
+
this.res.end();
|
|
44
|
+
}
|
|
45
|
+
async keepAlive() {
|
|
46
|
+
if (!this.ended)
|
|
47
|
+
await this.enqueueWrite(': keepalive\n\n');
|
|
48
|
+
}
|
|
49
|
+
// webpieces-disable no-any-unknown -- cancellation reasons are transport-neutral
|
|
50
|
+
async cancel(reason) {
|
|
51
|
+
await this.writer.cancel(reason);
|
|
52
|
+
this.finish();
|
|
53
|
+
}
|
|
54
|
+
async deliver(envelope) {
|
|
55
|
+
const frame = `event: message\ndata: ${JSON.stringify(envelope)}\n\n`;
|
|
56
|
+
if (this.opened) {
|
|
57
|
+
await this.enqueueWrite(frame);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.queuedBytes += Buffer.byteLength(frame);
|
|
61
|
+
if (this.queuedBytes > MAX_HANDSHAKE_BUFFER_BYTES) {
|
|
62
|
+
throw new core_util_1.StreamTransportError(`Streaming controller wrote more than ${MAX_HANDSHAKE_BUFFER_BYTES} bytes before its request writer was ready.`);
|
|
63
|
+
}
|
|
64
|
+
this.queued.push(frame);
|
|
65
|
+
}
|
|
66
|
+
if (envelope.kind === 'complete' || (envelope.kind === 'failure' && envelope.terminal)) {
|
|
67
|
+
this.ended = true;
|
|
68
|
+
if (this.opened)
|
|
69
|
+
this.res.end();
|
|
70
|
+
this.finish();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
enqueueWrite(data) {
|
|
74
|
+
const delivery = this.pendingWrite.then(() => this.write(data));
|
|
75
|
+
this.pendingWrite = delivery.catch(() => undefined);
|
|
76
|
+
return delivery;
|
|
77
|
+
}
|
|
78
|
+
async write(data) {
|
|
79
|
+
if (this.res.destroyed || this.res.writableEnded) {
|
|
80
|
+
throw new core_util_1.StreamTransportError('Cannot write to a closed streaming HTTP response.');
|
|
81
|
+
}
|
|
82
|
+
if (this.res.write(data))
|
|
83
|
+
return;
|
|
84
|
+
await Promise.race([
|
|
85
|
+
(0, node_events_1.once)(this.res, 'drain').then(() => undefined),
|
|
86
|
+
(0, node_events_1.once)(this.res, 'close').then(() => {
|
|
87
|
+
throw new core_util_1.StreamTransportError('Streaming HTTP peer disconnected during write.');
|
|
88
|
+
}),
|
|
89
|
+
(0, node_events_1.once)(this.res, 'error').then((args) => {
|
|
90
|
+
throw new core_util_1.StreamTransportError('Streaming HTTP response failed.', undefined, undefined, (0, core_util_1.toError)(args[0]));
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Express adapter for a `@WpStream` request-NDJSON / response-SSE contract. */
|
|
96
|
+
class StreamExpressWrapper {
|
|
97
|
+
clientMethod;
|
|
98
|
+
route;
|
|
99
|
+
headers;
|
|
100
|
+
ordinaryErrors;
|
|
101
|
+
validator = new core_util_1.StreamEventValidator();
|
|
102
|
+
constructor(
|
|
103
|
+
// webpieces-disable no-any-unknown -- stream contract types are erased at the express boundary
|
|
104
|
+
clientMethod, route, headers) {
|
|
105
|
+
this.clientMethod = clientMethod;
|
|
106
|
+
this.route = route;
|
|
107
|
+
this.headers = headers;
|
|
108
|
+
this.ordinaryErrors = new ExpressWrapper_1.ExpressWrapper(clientMethod, route.path, headers, false, false, undefined, route);
|
|
109
|
+
}
|
|
110
|
+
async execute(req, res, next) {
|
|
111
|
+
void next;
|
|
112
|
+
await core_context_1.RequestContext.run(async () => this.executeInContext(req, res));
|
|
113
|
+
}
|
|
114
|
+
async executeInContext(req, res) {
|
|
115
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- owning streaming HTTP error boundary; handshake failures still use ordinary status/error mapping
|
|
116
|
+
try {
|
|
117
|
+
this.headers.fillFromRequest(this.toWebpiecesRequest(req));
|
|
118
|
+
this.requireNdjson(req);
|
|
119
|
+
const outbound = new SseResponseChannel(res, this.route, this.validator);
|
|
120
|
+
const candidate = await this.clientMethod(outbound.writer);
|
|
121
|
+
if (!this.isRequestWriter(candidate)) {
|
|
122
|
+
throw new core_util_1.StreamTransportError(`${this.route.apiName ?? 'API'}.${this.route.methodName} did not return a RequestStream.`);
|
|
123
|
+
}
|
|
124
|
+
const inbound = candidate;
|
|
125
|
+
await outbound.open();
|
|
126
|
+
if (outbound.isEnded()) {
|
|
127
|
+
await inbound.cancel(new core_util_1.StreamTransportError('Response stream completed.'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await this.runOpenStream(req, res, inbound, outbound);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const error = (0, core_util_1.toError)(err);
|
|
134
|
+
this.ordinaryErrors.handleError(res, error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async runOpenStream(req, res, inbound, outbound) {
|
|
138
|
+
let disconnected = false;
|
|
139
|
+
const cancel = async () => {
|
|
140
|
+
if (disconnected || outbound.isEnded())
|
|
141
|
+
return;
|
|
142
|
+
disconnected = true;
|
|
143
|
+
const error = new core_util_1.StreamTransportError('Streaming HTTP peer disconnected.');
|
|
144
|
+
await inbound.cancel(error);
|
|
145
|
+
await outbound.cancel(error);
|
|
146
|
+
};
|
|
147
|
+
req.once('aborted', () => void cancel());
|
|
148
|
+
res.once('close', () => void cancel());
|
|
149
|
+
const keepAlive = setInterval(() => {
|
|
150
|
+
if (!disconnected)
|
|
151
|
+
void outbound.keepAlive().catch(() => cancel());
|
|
152
|
+
}, KEEP_ALIVE_MILLIS);
|
|
153
|
+
keepAlive.unref();
|
|
154
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- after SSE opens, wire failures must become an in-band typed terminal event
|
|
155
|
+
try {
|
|
156
|
+
const inboundFinished = this.consumeNdjson(req, inbound);
|
|
157
|
+
const responseFinishedFirst = await Promise.race([
|
|
158
|
+
inboundFinished.then(() => false),
|
|
159
|
+
outbound.finished.then(() => true),
|
|
160
|
+
]);
|
|
161
|
+
if (responseFinishedFirst) {
|
|
162
|
+
if (!disconnected) {
|
|
163
|
+
await inbound.cancel(new core_util_1.StreamTransportError('Response stream completed.'));
|
|
164
|
+
}
|
|
165
|
+
req.destroy();
|
|
166
|
+
await inboundFinished.catch(() => undefined);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await outbound.finished;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
const error = (0, core_util_1.toError)(err);
|
|
173
|
+
const streamError = err instanceof core_util_1.StreamTransportError
|
|
174
|
+
? err
|
|
175
|
+
: new core_util_1.StreamTransportError('Streaming HTTP request failed.', undefined, undefined, error);
|
|
176
|
+
if (!disconnected) {
|
|
177
|
+
if (!outbound.isEnded())
|
|
178
|
+
await outbound.writer.fail(streamError);
|
|
179
|
+
await inbound.fail(streamError);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
clearInterval(keepAlive);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async consumeNdjson(req, writer) {
|
|
187
|
+
let pending = Buffer.alloc(0);
|
|
188
|
+
for await (const raw of req) {
|
|
189
|
+
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
190
|
+
pending = Buffer.concat([pending, chunk]);
|
|
191
|
+
if (pending.length > MAX_LINE_BYTES && pending.indexOf(10) < 0) {
|
|
192
|
+
throw new core_util_1.StreamTransportError(`NDJSON frame exceeds ${MAX_LINE_BYTES} bytes.`);
|
|
193
|
+
}
|
|
194
|
+
let newline = pending.indexOf(10);
|
|
195
|
+
while (newline >= 0) {
|
|
196
|
+
const line = pending.subarray(0, newline).toString('utf8').trim();
|
|
197
|
+
pending = pending.subarray(newline + 1);
|
|
198
|
+
if (line !== '')
|
|
199
|
+
await this.dispatchLine(line, writer);
|
|
200
|
+
newline = pending.indexOf(10);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const finalLine = pending.toString('utf8').trim();
|
|
204
|
+
if (finalLine !== '')
|
|
205
|
+
await this.dispatchLine(finalLine, writer);
|
|
206
|
+
}
|
|
207
|
+
async dispatchLine(line, writer) {
|
|
208
|
+
let value;
|
|
209
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted NDJSON boundary and is translated to StreamTransportError
|
|
210
|
+
try {
|
|
211
|
+
value = JSON.parse(line);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
const error = (0, core_util_1.toError)(err);
|
|
215
|
+
throw new core_util_1.StreamTransportError('Malformed NDJSON stream frame.', undefined, undefined, error);
|
|
216
|
+
}
|
|
217
|
+
const envelope = this.parseEnvelope(value);
|
|
218
|
+
if (envelope.kind === 'event') {
|
|
219
|
+
this.validator.validate(this.route.streaming.requestEventClass, envelope.value, 'request');
|
|
220
|
+
await writer.event(envelope.value, envelope.correlation);
|
|
221
|
+
}
|
|
222
|
+
else if (envelope.kind === 'failure') {
|
|
223
|
+
await writer.fail(core_util_1.ApiErrorCodec.decode(envelope.error), envelope.correlation, {
|
|
224
|
+
terminal: envelope.terminal,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
await writer.complete();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
parseEnvelope(value) {
|
|
232
|
+
if (typeof value !== 'object' || value === null) {
|
|
233
|
+
throw new core_util_1.StreamTransportError('NDJSON stream frame must be an object.');
|
|
234
|
+
}
|
|
235
|
+
const record = value;
|
|
236
|
+
const kind = record['kind'];
|
|
237
|
+
const correlation = this.parseCorrelation(record['correlation']);
|
|
238
|
+
if (kind === 'event')
|
|
239
|
+
return new core_util_1.StreamEnvelope('event', record['value'], undefined, correlation);
|
|
240
|
+
if (kind === 'complete') {
|
|
241
|
+
return new core_util_1.StreamEnvelope('complete', undefined, undefined, correlation);
|
|
242
|
+
}
|
|
243
|
+
if (kind === 'failure' && core_util_1.ApiErrorCodec.isPayload(record['error'])) {
|
|
244
|
+
const terminal = record['terminal'];
|
|
245
|
+
if (terminal !== undefined && typeof terminal !== 'boolean') {
|
|
246
|
+
throw new core_util_1.StreamTransportError('NDJSON failure terminal flag must be boolean.');
|
|
247
|
+
}
|
|
248
|
+
const error = core_util_1.ApiErrorCodec.encode(core_util_1.ApiErrorCodec.decode(record['error']));
|
|
249
|
+
return new core_util_1.StreamEnvelope('failure', undefined, error, correlation, terminal);
|
|
250
|
+
}
|
|
251
|
+
throw new core_util_1.StreamTransportError(`Unknown or malformed NDJSON stream frame kind '${String(kind)}'.`);
|
|
252
|
+
}
|
|
253
|
+
parseCorrelation(value) {
|
|
254
|
+
if (value === undefined)
|
|
255
|
+
return undefined;
|
|
256
|
+
if (typeof value !== 'object' || value === null) {
|
|
257
|
+
throw new core_util_1.StreamTransportError('Stream correlation must be an object.');
|
|
258
|
+
}
|
|
259
|
+
const record = value;
|
|
260
|
+
const key = record['key'];
|
|
261
|
+
const requestId = record['requestId'];
|
|
262
|
+
if ((typeof key !== 'string' && typeof key !== 'number') ||
|
|
263
|
+
(requestId !== undefined && typeof requestId !== 'string')) {
|
|
264
|
+
throw new core_util_1.StreamTransportError('Stream correlation has an invalid key or requestId.');
|
|
265
|
+
}
|
|
266
|
+
return new core_util_1.StreamCorrelation(key, requestId);
|
|
267
|
+
}
|
|
268
|
+
requireNdjson(req) {
|
|
269
|
+
const contentType = req.get('content-type')?.split(';')[0]?.trim().toLowerCase();
|
|
270
|
+
if (contentType !== 'application/x-ndjson') {
|
|
271
|
+
throw new core_util_1.ApiBadRequestError('Streaming endpoints require Content-Type application/x-ndjson.');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// webpieces-disable no-any-unknown -- runtime guard narrows an erased API return value
|
|
275
|
+
isRequestWriter(value) {
|
|
276
|
+
if (typeof value !== 'object' || value === null)
|
|
277
|
+
return false;
|
|
278
|
+
const candidate = value;
|
|
279
|
+
return ['event', 'fail', 'complete', 'cancel'].every((name) => typeof candidate[name] === 'function');
|
|
280
|
+
}
|
|
281
|
+
toWebpiecesRequest(req) {
|
|
282
|
+
const headers = new Map();
|
|
283
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
284
|
+
if (typeof value === 'string')
|
|
285
|
+
headers.set(name.toLowerCase(), [value]);
|
|
286
|
+
else if (Array.isArray(value))
|
|
287
|
+
headers.set(name.toLowerCase(), value);
|
|
288
|
+
}
|
|
289
|
+
return new core_context_1.HttpRequest(req.method, req.path ?? this.route.path, headers);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
exports.StreamExpressWrapper = StreamExpressWrapper;
|
|
293
|
+
//# sourceMappingURL=StreamExpressWrapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StreamExpressWrapper.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/StreamExpressWrapper.ts"],"names":[],"mappings":";;;AAAA,6CAAmC;AAEnC,oDAa8B;AAC9B,0DAA6F;AAC7F,qDAAkD;AAElD,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACjC,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAAC;AACnC,MAAM,0BAA0B,GAAG,EAAE,GAAG,IAAI,CAAC;AAE7C,2EAA2E;AAC3E,MAAM,kBAAkB;IAaC;IAZb,MAAM,GAAG,KAAK,CAAC;IACf,KAAK,GAAG,KAAK,CAAC;IACd,WAAW,GAAG,CAAC,CAAC;IACP,MAAM,GAAa,EAAE,CAAC;IAC/B,MAAM,GAAe,GAAG,EAAE,CAAC,SAAS,CAAC;IACrC,YAAY,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAC/C,QAAQ,GAAG,IAAI,OAAO,CAAO,CAAC,OAAmB,EAAQ,EAAE;QAChE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;IAC1B,CAAC,CAAC,CAAC;IACM,MAAM,CAAyB;IAExC,YACqB,GAAa,EAC9B,KAAoB,EACpB,SAA+B;QAFd,QAAG,GAAH,GAAG,CAAU;QAI9B,IAAI,CAAC,MAAM,GAAG,IAAI,wBAAY,CAC1B,CAAC,QAAkC,EAAiB,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAC7E,CAAC,KAAe,EAAQ,EAAE,CACtB,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAU,CAAC,kBAAkB,EAAE,KAAK,EAAE,UAAU,CAAC,EAC9E,KAAK,CAAC,SAAS,EAAE,2BAA2B,CAC/C,CAAC;IACN,CAAC;IAED,OAAO;QACH,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,IAAI;QACN,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrB,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,kCAAkC,CAAC,CAAC;QACvE,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,wBAAwB,CAAC,CAAC;QAC9D,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC/C,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;QAC9C,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QACvB,IAAI,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,SAAS;QACX,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;IAChE,CAAC;IAED,iFAAiF;IACjF,KAAK,CAAC,MAAM,CAAC,MAAgB;QACzB,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,EAAE,CAAC;IAClB,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,QAAkC;QACpD,MAAM,KAAK,GAAG,yBAAyB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC;QACtE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YAC7C,IAAI,IAAI,CAAC,WAAW,GAAG,0BAA0B,EAAE,CAAC;gBAChD,MAAM,IAAI,gCAAoB,CAC1B,wCAAwC,0BAA0B,6CAA6C,CAClH,CAAC;YACN,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC;QACD,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,SAAS,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrF,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,IAAI,IAAI,CAAC,MAAM;gBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;YAChC,IAAI,CAAC,MAAM,EAAE,CAAC;QAClB,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,IAAY;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAkB,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QAC/E,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAS,EAAE,CAAC,SAAS,CAAC,CAAC;QAC1D,OAAO,QAAQ,CAAC;IACpB,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,IAAY;QAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;YAC/C,MAAM,IAAI,gCAAoB,CAAC,mDAAmD,CAAC,CAAC;QACxF,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,OAAO;QACjC,MAAM,OAAO,CAAC,IAAI,CAAC;YACf,IAAA,kBAAI,EAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAS,EAAE,CAAC,SAAS,CAAC;YACnD,IAAA,kBAAI,EAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAU,EAAE;gBACrC,MAAM,IAAI,gCAAoB,CAAC,gDAAgD,CAAC,CAAC;YACrF,CAAC,CAAC;YACF,IAAA,kBAAI,EAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAS,EAAE;gBACzC,MAAM,IAAI,gCAAoB,CAC1B,iCAAiC,EACjC,SAAS,EACT,SAAS,EACT,IAAA,mBAAO,EAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CACnB,CAAC;YACN,CAAC,CAAC;SACL,CAAC,CAAC;IACP,CAAC;CACJ;AAED,gFAAgF;AAChF,MAAa,oBAAoB;IAMR;IACA;IACA;IAPJ,cAAc,CAAiB;IAC/B,SAAS,GAAG,IAAI,gCAAoB,EAAE,CAAC;IAExD;IACI,+FAA+F;IAC9E,YAAsD,EACtD,KAAoB,EACpB,OAA8B;QAF9B,iBAAY,GAAZ,YAAY,CAA0C;QACtD,UAAK,GAAL,KAAK,CAAe;QACpB,YAAO,GAAP,OAAO,CAAuB;QAE/C,IAAI,CAAC,cAAc,GAAG,IAAI,+BAAc,CACpC,YAAY,EACZ,KAAK,CAAC,IAAI,EACV,OAAO,EACP,KAAK,EACL,KAAK,EACL,SAAS,EACT,KAAK,CACR,CAAC;IACN,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;QACzD,KAAK,IAAI,CAAC;QACV,MAAM,6BAAc,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1E,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,GAAY,EAAE,GAAa;QACtD,kKAAkK;QAClK,IAAI,CAAC;YACD,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;YAC3D,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YACxB,MAAM,QAAQ,GAAG,IAAI,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YACzE,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAkC,CAAC,CAAC;YACvF,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;gBACnC,MAAM,IAAI,gCAAoB,CAC1B,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,kCAAkC,CAC5F,CAAC;YACN,CAAC;YACD,MAAM,OAAO,GAAG,SAAS,CAAC;YAC1B,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACtB,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;gBACrB,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,gCAAoB,CAAC,4BAA4B,CAAC,CAAC,CAAC;gBAC7E,OAAO;YACX,CAAC;YACD,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAChD,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,aAAa,CACvB,GAAY,EACZ,GAAa,EACb,OAAgC,EAChC,QAA4B;QAE5B,IAAI,YAAY,GAAG,KAAK,CAAC;QACzB,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;YACrC,IAAI,YAAY,IAAI,QAAQ,CAAC,OAAO,EAAE;gBAAE,OAAO;YAC/C,YAAY,GAAG,IAAI,CAAC;YACpB,MAAM,KAAK,GAAG,IAAI,gCAAoB,CAAC,mCAAmC,CAAC,CAAC;YAC5E,MAAM,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC5B,MAAM,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,GAAS,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;QAC/C,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,GAAS,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,WAAW,CAAC,GAAS,EAAE;YACrC,IAAI,CAAC,YAAY;gBAAE,KAAK,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;QACvE,CAAC,EAAE,iBAAiB,CAAC,CAAC;QACtB,SAAS,CAAC,KAAK,EAAE,CAAC;QAElB,4IAA4I;QAC5I,IAAI,CAAC;YACD,MAAM,eAAe,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACzD,MAAM,qBAAqB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;gBAC7C,eAAe,CAAC,IAAI,CAAC,GAAY,EAAE,CAAC,KAAK,CAAC;gBAC1C,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAY,EAAE,CAAC,IAAI,CAAC;aAC9C,CAAC,CAAC;YACH,IAAI,qBAAqB,EAAE,CAAC;gBACxB,IAAI,CAAC,YAAY,EAAE,CAAC;oBAChB,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,gCAAoB,CAAC,4BAA4B,CAAC,CAAC,CAAC;gBACjF,CAAC;gBACD,GAAG,CAAC,OAAO,EAAE,CAAC;gBACd,MAAM,eAAe,CAAC,KAAK,CAAC,GAAS,EAAE,CAAC,SAAS,CAAC,CAAC;gBACnD,OAAO;YACX,CAAC;YACD,MAAM,QAAQ,CAAC,QAAQ,CAAC;QAC5B,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,WAAW,GACb,GAAG,YAAY,gCAAoB;gBAC/B,CAAC,CAAC,GAAG;gBACL,CAAC,CAAC,IAAI,gCAAoB,CACpB,gCAAgC,EAChC,SAAS,EACT,SAAS,EACT,KAAK,CACR,CAAC;YACZ,IAAI,CAAC,YAAY,EAAE,CAAC;gBAChB,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE;oBAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBACjE,MAAM,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YACpC,CAAC;QACL,CAAC;gBAAS,CAAC;YACP,aAAa,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,GAAY,EAAE,MAA+B;QACrE,IAAI,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9B,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC5D,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;YAC1C,IAAI,OAAO,CAAC,MAAM,GAAG,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC7D,MAAM,IAAI,gCAAoB,CAAC,wBAAwB,cAAc,SAAS,CAAC,CAAC;YACpF,CAAC;YACD,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAClC,OAAO,OAAO,IAAI,CAAC,EAAE,CAAC;gBAClB,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;gBAClE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;gBACxC,IAAI,IAAI,KAAK,EAAE;oBAAE,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBACvD,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAClC,CAAC;QACL,CAAC;QACD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,IAAI,SAAS,KAAK,EAAE;YAAE,MAAM,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACrE,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,IAAY,EAAE,MAA+B;QACpE,IAAI,KAAe,CAAC;QACpB,uJAAuJ;QACvJ,IAAI,CAAC;YACD,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAa,CAAC;QACzC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,IAAI,gCAAoB,CAC1B,gCAAgC,EAChC,SAAS,EACT,SAAS,EACT,KAAK,CACR,CAAC;QACN,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,SAAS,CAAC,QAAQ,CACnB,IAAI,CAAC,KAAK,CAAC,SAAU,CAAC,iBAAiB,EACvC,QAAQ,CAAC,KAAK,EACd,SAAS,CACZ,CAAC;YACF,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC9D,CAAC;aAAM,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,MAAM,CAAC,IAAI,CAAC,yBAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,WAAW,EAAE;gBAC1E,QAAQ,EAAE,QAAQ,CAAC,QAAQ;aAC9B,CAAC,CAAC;QACP,CAAC;aAAM,CAAC;YACJ,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC5B,CAAC;IACL,CAAC;IAEO,aAAa,CAAC,KAAe;QACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAC9C,MAAM,IAAI,gCAAoB,CAAC,wCAAwC,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,MAAM,GAAG,KAA6C,CAAC;QAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;QACjE,IAAI,IAAI,KAAK,OAAO;YAChB,OAAO,IAAI,0BAAc,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;QAChF,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;YACtB,OAAO,IAAI,0BAAc,CAAW,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,IAAI,KAAK,SAAS,IAAI,yBAAa,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;YACpC,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC1D,MAAM,IAAI,gCAAoB,CAAC,+CAA+C,CAAC,CAAC;YACpF,CAAC;YACD,MAAM,KAAK,GAAG,yBAAa,CAAC,MAAM,CAAC,yBAAa,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC1E,OAAO,IAAI,0BAAc,CAAW,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,IAAI,gCAAoB,CAC1B,kDAAkD,MAAM,CAAC,IAAI,CAAC,IAAI,CACrE,CAAC;IACN,CAAC;IAEO,gBAAgB,CAAC,KAA2B;QAChD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAC9C,MAAM,IAAI,gCAAoB,CAAC,uCAAuC,CAAC,CAAC;QAC5E,CAAC;QACD,MAAM,MAAM,GAAG,KAA6C,CAAC;QAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1B,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACtC,IACI,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ,CAAC;YACpD,CAAC,SAAS,KAAK,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,CAAC,EAC5D,CAAC;YACC,MAAM,IAAI,gCAAoB,CAAC,qDAAqD,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO,IAAI,6BAAiB,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACjD,CAAC;IAEO,aAAa,CAAC,GAAY;QAC9B,MAAM,WAAW,GAAG,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACjF,IAAI,WAAW,KAAK,sBAAsB,EAAE,CAAC;YACzC,MAAM,IAAI,8BAAkB,CACxB,gEAAgE,CACnE,CAAC;QACN,CAAC;IACL,CAAC;IAED,uFAAuF;IAC/E,eAAe,CAAC,KAAc;QAClC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC9D,MAAM,SAAS,GAAG,KAA6C,CAAC;QAChE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,KAAK,CAChD,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,SAAS,CAAC,IAAI,CAAC,KAAK,UAAU,CAC1D,CAAC;IACN,CAAC;IAEO,kBAAkB,CAAC,GAAY;QACnC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;QAC5C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACtD,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;iBACnE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,KAAK,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,IAAI,0BAAW,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7E,CAAC;CACJ;AAnOD,oDAmOC","sourcesContent":["import { once } from 'node:events';\nimport { NextFunction, Request, Response } from 'express';\nimport {\n ApiBadRequestError,\n ApiErrorCodec,\n DtoValue,\n RequestStream,\n ResponseStream,\n RouteMetadata,\n StreamCorrelation,\n StreamEnvelope,\n StreamEventValidator,\n StreamTransportError,\n StreamWriter,\n toError,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { ExpressWrapper } from './ExpressWrapper';\n\nconst KEEP_ALIVE_MILLIS = 15_000;\nconst MAX_LINE_BYTES = 1024 * 1024;\nconst MAX_HANDSHAKE_BUFFER_BYTES = 64 * 1024;\n\n/** Owns ordered SSE writes and the small pre-handshake response buffer. */\nclass SseResponseChannel {\n private opened = false;\n private ended = false;\n private queuedBytes = 0;\n private readonly queued: string[] = [];\n private finish: () => void = () => undefined;\n private pendingWrite: Promise<void> = Promise.resolve();\n readonly finished = new Promise<void>((resolve: () => void): void => {\n this.finish = resolve;\n });\n readonly writer: StreamWriter<DtoValue>;\n\n constructor(\n private readonly res: Response,\n route: RouteMetadata,\n validator: StreamEventValidator,\n ) {\n this.writer = new StreamWriter<DtoValue>(\n (envelope: StreamEnvelope<DtoValue>): Promise<void> => this.deliver(envelope),\n (value: DtoValue): void =>\n validator.validate(route.streaming!.responseEventClass, value, 'response'),\n route.streaming?.supportsNonTerminalFailures,\n );\n }\n\n isEnded(): boolean {\n return this.ended;\n }\n\n async open(): Promise<void> {\n this.res.status(200);\n this.res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n this.res.setHeader('Cache-Control', 'no-cache, no-transform');\n this.res.setHeader('Connection', 'keep-alive');\n this.res.setHeader('X-Accel-Buffering', 'no');\n this.res.flushHeaders();\n this.opened = true;\n for (const frame of this.queued) await this.enqueueWrite(frame);\n this.queued.length = 0;\n if (this.ended) this.res.end();\n }\n\n async keepAlive(): Promise<void> {\n if (!this.ended) await this.enqueueWrite(': keepalive\\n\\n');\n }\n\n // webpieces-disable no-any-unknown -- cancellation reasons are transport-neutral\n async cancel(reason?: unknown): Promise<void> {\n await this.writer.cancel(reason);\n this.finish();\n }\n\n private async deliver(envelope: StreamEnvelope<DtoValue>): Promise<void> {\n const frame = `event: message\\ndata: ${JSON.stringify(envelope)}\\n\\n`;\n if (this.opened) {\n await this.enqueueWrite(frame);\n } else {\n this.queuedBytes += Buffer.byteLength(frame);\n if (this.queuedBytes > MAX_HANDSHAKE_BUFFER_BYTES) {\n throw new StreamTransportError(\n `Streaming controller wrote more than ${MAX_HANDSHAKE_BUFFER_BYTES} bytes before its request writer was ready.`,\n );\n }\n this.queued.push(frame);\n }\n if (envelope.kind === 'complete' || (envelope.kind === 'failure' && envelope.terminal)) {\n this.ended = true;\n if (this.opened) this.res.end();\n this.finish();\n }\n }\n\n private enqueueWrite(data: string): Promise<void> {\n const delivery = this.pendingWrite.then((): Promise<void> => this.write(data));\n this.pendingWrite = delivery.catch((): void => undefined);\n return delivery;\n }\n\n private async write(data: string): Promise<void> {\n if (this.res.destroyed || this.res.writableEnded) {\n throw new StreamTransportError('Cannot write to a closed streaming HTTP response.');\n }\n if (this.res.write(data)) return;\n await Promise.race([\n once(this.res, 'drain').then((): void => undefined),\n once(this.res, 'close').then((): never => {\n throw new StreamTransportError('Streaming HTTP peer disconnected during write.');\n }),\n once(this.res, 'error').then((args): never => {\n throw new StreamTransportError(\n 'Streaming HTTP response failed.',\n undefined,\n undefined,\n toError(args[0]),\n );\n }),\n ]);\n }\n}\n\n/** Express adapter for a `@WpStream` request-NDJSON / response-SSE contract. */\nexport class StreamExpressWrapper {\n private readonly ordinaryErrors: ExpressWrapper;\n private readonly validator = new StreamEventValidator();\n\n constructor(\n // webpieces-disable no-any-unknown -- stream contract types are erased at the express boundary\n private readonly clientMethod: (...args: unknown[]) => Promise<unknown>,\n private readonly route: RouteMetadata,\n private readonly headers: RequestContextHeaders,\n ) {\n this.ordinaryErrors = new ExpressWrapper(\n clientMethod,\n route.path,\n headers,\n false,\n false,\n undefined,\n route,\n );\n }\n\n async execute(req: Request, res: Response, next: NextFunction): Promise<void> {\n void next;\n await RequestContext.run(async () => this.executeInContext(req, res));\n }\n\n private async executeInContext(req: Request, res: Response): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- owning streaming HTTP error boundary; handshake failures still use ordinary status/error mapping\n try {\n this.headers.fillFromRequest(this.toWebpiecesRequest(req));\n this.requireNdjson(req);\n const outbound = new SseResponseChannel(res, this.route, this.validator);\n const candidate = await this.clientMethod(outbound.writer as ResponseStream<DtoValue>);\n if (!this.isRequestWriter(candidate)) {\n throw new StreamTransportError(\n `${this.route.apiName ?? 'API'}.${this.route.methodName} did not return a RequestStream.`,\n );\n }\n const inbound = candidate;\n await outbound.open();\n if (outbound.isEnded()) {\n await inbound.cancel(new StreamTransportError('Response stream completed.'));\n return;\n }\n await this.runOpenStream(req, res, inbound, outbound);\n } catch (err: unknown) {\n const error = toError(err);\n this.ordinaryErrors.handleError(res, error);\n }\n }\n\n private async runOpenStream(\n req: Request,\n res: Response,\n inbound: RequestStream<DtoValue>,\n outbound: SseResponseChannel,\n ): Promise<void> {\n let disconnected = false;\n const cancel = async (): Promise<void> => {\n if (disconnected || outbound.isEnded()) return;\n disconnected = true;\n const error = new StreamTransportError('Streaming HTTP peer disconnected.');\n await inbound.cancel(error);\n await outbound.cancel(error);\n };\n req.once('aborted', (): void => void cancel());\n res.once('close', (): void => void cancel());\n const keepAlive = setInterval((): void => {\n if (!disconnected) void outbound.keepAlive().catch(() => cancel());\n }, KEEP_ALIVE_MILLIS);\n keepAlive.unref();\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- after SSE opens, wire failures must become an in-band typed terminal event\n try {\n const inboundFinished = this.consumeNdjson(req, inbound);\n const responseFinishedFirst = await Promise.race([\n inboundFinished.then((): boolean => false),\n outbound.finished.then((): boolean => true),\n ]);\n if (responseFinishedFirst) {\n if (!disconnected) {\n await inbound.cancel(new StreamTransportError('Response stream completed.'));\n }\n req.destroy();\n await inboundFinished.catch((): void => undefined);\n return;\n }\n await outbound.finished;\n } catch (err: unknown) {\n const error = toError(err);\n const streamError =\n err instanceof StreamTransportError\n ? err\n : new StreamTransportError(\n 'Streaming HTTP request failed.',\n undefined,\n undefined,\n error,\n );\n if (!disconnected) {\n if (!outbound.isEnded()) await outbound.writer.fail(streamError);\n await inbound.fail(streamError);\n }\n } finally {\n clearInterval(keepAlive);\n }\n }\n\n private async consumeNdjson(req: Request, writer: RequestStream<DtoValue>): Promise<void> {\n let pending = Buffer.alloc(0);\n for await (const raw of req) {\n const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n pending = Buffer.concat([pending, chunk]);\n if (pending.length > MAX_LINE_BYTES && pending.indexOf(10) < 0) {\n throw new StreamTransportError(`NDJSON frame exceeds ${MAX_LINE_BYTES} bytes.`);\n }\n let newline = pending.indexOf(10);\n while (newline >= 0) {\n const line = pending.subarray(0, newline).toString('utf8').trim();\n pending = pending.subarray(newline + 1);\n if (line !== '') await this.dispatchLine(line, writer);\n newline = pending.indexOf(10);\n }\n }\n const finalLine = pending.toString('utf8').trim();\n if (finalLine !== '') await this.dispatchLine(finalLine, writer);\n }\n\n private async dispatchLine(line: string, writer: RequestStream<DtoValue>): Promise<void> {\n let value: DtoValue;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted NDJSON boundary and is translated to StreamTransportError\n try {\n value = JSON.parse(line) as DtoValue;\n } catch (err: unknown) {\n const error = toError(err);\n throw new StreamTransportError(\n 'Malformed NDJSON stream frame.',\n undefined,\n undefined,\n error,\n );\n }\n const envelope = this.parseEnvelope(value);\n if (envelope.kind === 'event') {\n this.validator.validate(\n this.route.streaming!.requestEventClass,\n envelope.value,\n 'request',\n );\n await writer.event(envelope.value!, envelope.correlation);\n } else if (envelope.kind === 'failure') {\n await writer.fail(ApiErrorCodec.decode(envelope.error), envelope.correlation, {\n terminal: envelope.terminal,\n });\n } else {\n await writer.complete();\n }\n }\n\n private parseEnvelope(value: DtoValue): StreamEnvelope<DtoValue> {\n if (typeof value !== 'object' || value === null) {\n throw new StreamTransportError('NDJSON stream frame must be an object.');\n }\n const record = value as Record<string, DtoValue | undefined>;\n const kind = record['kind'];\n const correlation = this.parseCorrelation(record['correlation']);\n if (kind === 'event')\n return new StreamEnvelope('event', record['value'], undefined, correlation);\n if (kind === 'complete') {\n return new StreamEnvelope<DtoValue>('complete', undefined, undefined, correlation);\n }\n if (kind === 'failure' && ApiErrorCodec.isPayload(record['error'])) {\n const terminal = record['terminal'];\n if (terminal !== undefined && typeof terminal !== 'boolean') {\n throw new StreamTransportError('NDJSON failure terminal flag must be boolean.');\n }\n const error = ApiErrorCodec.encode(ApiErrorCodec.decode(record['error']));\n return new StreamEnvelope<DtoValue>('failure', undefined, error, correlation, terminal);\n }\n throw new StreamTransportError(\n `Unknown or malformed NDJSON stream frame kind '${String(kind)}'.`,\n );\n }\n\n private parseCorrelation(value: DtoValue | undefined): StreamCorrelation | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null) {\n throw new StreamTransportError('Stream correlation must be an object.');\n }\n const record = value as Record<string, DtoValue | undefined>;\n const key = record['key'];\n const requestId = record['requestId'];\n if (\n (typeof key !== 'string' && typeof key !== 'number') ||\n (requestId !== undefined && typeof requestId !== 'string')\n ) {\n throw new StreamTransportError('Stream correlation has an invalid key or requestId.');\n }\n return new StreamCorrelation(key, requestId);\n }\n\n private requireNdjson(req: Request): void {\n const contentType = req.get('content-type')?.split(';')[0]?.trim().toLowerCase();\n if (contentType !== 'application/x-ndjson') {\n throw new ApiBadRequestError(\n 'Streaming endpoints require Content-Type application/x-ndjson.',\n );\n }\n }\n\n // webpieces-disable no-any-unknown -- runtime guard narrows an erased API return value\n private isRequestWriter(value: unknown): value is RequestStream<DtoValue> {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Record<string, DtoValue | undefined>;\n return ['event', 'fail', 'complete', 'cancel'].every(\n (name: string) => typeof candidate[name] === 'function',\n );\n }\n\n private toWebpiecesRequest(req: Request): HttpRequest {\n const headers = new Map<string, string[]>();\n for (const [name, value] of Object.entries(req.headers)) {\n if (typeof value === 'string') headers.set(name.toLowerCase(), [value]);\n else if (Array.isArray(value)) headers.set(name.toLowerCase(), value);\n }\n return new HttpRequest(req.method, req.path ?? this.route.path, headers);\n }\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Express } from 'express';
|
|
2
2
|
import { ApiFactory, WebpiecesConfig } from '@webpieces/http-routing';
|
|
3
|
+
import { RequestBodyReader } from './body/RequestBodyReader';
|
|
3
4
|
/** The value returned by express `app.listen(...)` (a node http.Server). */
|
|
4
5
|
type HttpServer = ReturnType<Express['listen']>;
|
|
5
6
|
/**
|
|
@@ -26,7 +27,24 @@ type HttpServer = ReturnType<Express['listen']>;
|
|
|
26
27
|
export declare class WebpiecesExpressRouter {
|
|
27
28
|
private readonly apiFactory;
|
|
28
29
|
private readonly middleware;
|
|
30
|
+
/** Where every mounted route reads its body bytes from. See {@link setBodyReader}. */
|
|
31
|
+
private bodyReader;
|
|
32
|
+
/** Set by {@link bindExpress}; a reader chosen after that would reach no route. */
|
|
33
|
+
private bound;
|
|
29
34
|
constructor(apiFactory: ApiFactory);
|
|
35
|
+
/**
|
|
36
|
+
* Choose how routes read the request body. Call it BEFORE {@link bindExpress}: each route captures
|
|
37
|
+
* the reader when it is mounted.
|
|
38
|
+
*
|
|
39
|
+
* The default ({@link StreamBodyReader}) reads the request stream, which is right whenever
|
|
40
|
+
* webpieces is mounted ahead of any body parser. A host that parses the body before the app runs
|
|
41
|
+
* (Cloud Functions gen2 / Firebase, which keep the verbatim bytes on `req.rawBody`) needs
|
|
42
|
+
* `router.setBodyReader(new PreConsumedBodyReader())`, or every POST fails fast (issue #937).
|
|
43
|
+
*
|
|
44
|
+
* @throws Error when called after {@link bindExpress}, because the already-mounted routes would
|
|
45
|
+
* silently keep the old reader.
|
|
46
|
+
*/
|
|
47
|
+
setBodyReader(bodyReader: RequestBodyReader): void;
|
|
30
48
|
/**
|
|
31
49
|
* Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,
|
|
32
50
|
* express-tier + api-tier filter chain, error→JSON) onto the caller's express app.
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WebpiecesExpressRouter = void 0;
|
|
4
4
|
const core_util_1 = require("@webpieces/core-util");
|
|
5
5
|
const WebpiecesMiddleware_1 = require("./WebpiecesMiddleware");
|
|
6
|
+
const StreamBodyReader_1 = require("./body/StreamBodyReader");
|
|
6
7
|
const log = core_util_1.LogManager.getLogger('WebpiecesExpressRouter');
|
|
7
8
|
/**
|
|
8
9
|
* WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only
|
|
@@ -28,9 +29,32 @@ const log = core_util_1.LogManager.getLogger('WebpiecesExpressRouter');
|
|
|
28
29
|
class WebpiecesExpressRouter {
|
|
29
30
|
apiFactory;
|
|
30
31
|
middleware = new WebpiecesMiddleware_1.WebpiecesMiddleware();
|
|
32
|
+
/** Where every mounted route reads its body bytes from. See {@link setBodyReader}. */
|
|
33
|
+
bodyReader = new StreamBodyReader_1.StreamBodyReader();
|
|
34
|
+
/** Set by {@link bindExpress}; a reader chosen after that would reach no route. */
|
|
35
|
+
bound = false;
|
|
31
36
|
constructor(apiFactory) {
|
|
32
37
|
this.apiFactory = apiFactory;
|
|
33
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Choose how routes read the request body. Call it BEFORE {@link bindExpress}: each route captures
|
|
41
|
+
* the reader when it is mounted.
|
|
42
|
+
*
|
|
43
|
+
* The default ({@link StreamBodyReader}) reads the request stream, which is right whenever
|
|
44
|
+
* webpieces is mounted ahead of any body parser. A host that parses the body before the app runs
|
|
45
|
+
* (Cloud Functions gen2 / Firebase, which keep the verbatim bytes on `req.rawBody`) needs
|
|
46
|
+
* `router.setBodyReader(new PreConsumedBodyReader())`, or every POST fails fast (issue #937).
|
|
47
|
+
*
|
|
48
|
+
* @throws Error when called after {@link bindExpress}, because the already-mounted routes would
|
|
49
|
+
* silently keep the old reader.
|
|
50
|
+
*/
|
|
51
|
+
setBodyReader(bodyReader) {
|
|
52
|
+
if (this.bound) {
|
|
53
|
+
throw new Error('setBodyReader() was called after bindExpress(); the mounted routes already captured ' +
|
|
54
|
+
'their body reader. Call setBodyReader() before bindExpress(app).');
|
|
55
|
+
}
|
|
56
|
+
this.bodyReader = bodyReader;
|
|
57
|
+
}
|
|
34
58
|
/**
|
|
35
59
|
* Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,
|
|
36
60
|
* express-tier + api-tier filter chain, error→JSON) onto the caller's express app.
|
|
@@ -39,6 +63,7 @@ class WebpiecesExpressRouter {
|
|
|
39
63
|
* routes must stay untouched. The caller owns app.listen() and any global middleware.
|
|
40
64
|
*/
|
|
41
65
|
bindExpress(app) {
|
|
66
|
+
this.bound = true;
|
|
42
67
|
let count = 0;
|
|
43
68
|
for (const apiClient of this.apiFactory.apiClients()) {
|
|
44
69
|
count += this.mountApiClient(app, apiClient);
|
|
@@ -118,7 +143,9 @@ class WebpiecesExpressRouter {
|
|
|
118
143
|
const path = this.expressPath(route);
|
|
119
144
|
// The parser is chosen by the @Endpoint annotation, not the request Content-Type — and
|
|
120
145
|
// so is whether the verbatim bytes survive the parse for an @WpAuthWebhook hook to verify.
|
|
121
|
-
const wrapper =
|
|
146
|
+
const wrapper = route.streaming
|
|
147
|
+
? this.middleware.createStreamExpressWrapper(apiClient.client[route.methodName], route)
|
|
148
|
+
: this.middleware.createExpressWrapper(apiClient.client[route.methodName], route, this.bodyReader);
|
|
122
149
|
this.registerHandler(app, route.httpMethod, path, wrapper.execute.bind(wrapper));
|
|
123
150
|
count++;
|
|
124
151
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesExpressRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesExpressRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Express } from 'express';\nimport { ApiFactory, ApiClient, WebpiecesConfig } from '@webpieces/http-routing';\nimport { LogManager, RouteMetadata } from '@webpieces/core-util';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\n\nconst log = LogManager.getLogger('WebpiecesExpressRouter');\n\n/** The value returned by express `app.listen(...)` (a node http.Server). */\ntype HttpServer = ReturnType<Express['listen']>;\n\n/**\n * WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only\n * {@link ApiFactory} (a WebpiecesRouter). It is the ONLY place express lifecycle lives.\n *\n * It never reaches into routing internals: it asks the ApiFactory for `apiClients()` — each\n * an api + routeMeta + composed filter-chain→controller impl — and binds each to an express\n * route (`app.<verb>(path, handler)`) invoked when the matching HTTP request arrives. The\n * RouteBuilder stays hidden inside the ApiFactory.\n *\n * ```typescript\n * const apiFactory = await WebpiecesRouterFactory.create(config, { appBindings });\n * apiFactory.addRoutes(SaveApi, SaveController);\n * const express = new WebpiecesExpressRouter(apiFactory);\n *\n * // legacy / side-by-side: mount onto an existing app; you own listen + your middleware\n * express.bindExpress(existingApp);\n *\n * // non-legacy: add webpieces global middleware + listen for you\n * await express.bindAndStartExpress(express(), 8080);\n * ```\n */\nexport class WebpiecesExpressRouter {\n private readonly middleware = new WebpiecesMiddleware();\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n /**\n * Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,\n * express-tier + api-tier filter chain, error→JSON) onto the caller's express app.\n *\n * Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose other\n * routes must stay untouched. The caller owns app.listen() and any global middleware.\n */\n bindExpress(app: Express): void {\n let count = 0;\n for (const apiClient of this.apiFactory.apiClients()) {\n count += this.mountApiClient(app, apiClient);\n }\n log.info(`Mounted ${count} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (optional CORS), bind the routes, mount the top-level\n * error handler AFTER them, then app.listen(port). Convenience for a non-legacy webpieces server where\n * webpieces owns the whole express app. Resolves with the http.Server once listening.\n *\n * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and\n * {@link WebpiecesMiddleware.corsMiddleware}.\n */\n async bindAndStartExpress(\n app: Express,\n port: number = 8080,\n config?: WebpiecesConfig,\n ): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n // CORS is OPT-IN, and stays OFF in production. A server that serves its own browser app does\n // not need it — a browser applies no cors check to a same-origin request — so mounting it\n // would only hand credentialed cross-origin read access to whatever it allows, for nothing.\n // It is needed solely when a browser on ANOTHER origin calls this api: `ng serve` in dev, or\n // a UI hosted on a different host. Those say so via corsOrigins.\n const corsOrigins = config?.corsOrigins ?? [];\n if (corsOrigins.length > 0) {\n app.use(this.middleware.corsMiddleware(config));\n }\n\n this.bindExpress(app);\n\n // Top-level error handler is mounted LAST (AFTER the routes). Express only forwards a\n // downstream failure to a 4-arg error middleware that sits BELOW the failing route — it does\n // NOT bubble errors back up through next(). See WebpiecesMiddleware.errorHandler.\n app.use(this.middleware.errorHandler.bind(this.middleware));\n\n return new Promise<HttpServer>(\n (resolve: (server: HttpServer) => void, reject: (err: Error) => void) => {\n const server: HttpServer = app.listen(port, (error?: Error) => {\n if (error) {\n log.error(`Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`Listening on http://localhost:${port}`);\n this.logStartupBanner(port);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * The \"Svr Ready!!\" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and\n * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied\n * verbatim from the production service it was ported from, so a familiar splash marks \"the server is up and reachable\".\n */\n private logStartupBanner(port: number): void {\n if (process.env['K_SERVICE']) {\n return;\n }\n log.info(`\n ___ _____ _\n/ _| | _ \\\\ | |\n\\\\ \\`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _\n \\`--. \\\\/ _ \\\\ '_\\\\ \\\\ / / _ \\\\ '_| | // _ \\\\/ _\\` |/ _\\` | | | |\n/\\\\_/ / _/ | \\\\ V / _/ | | |\\\\ \\\\ _/ (_| | (_| | |_| |\n\\\\___/ \\\\_|_| \\\\_/ \\\\_|_| \\\\_| \\\\_\\\\_|\\\\_,_|\\\\_,_|\\\\_, |\n _/ |\n |_/\n\n Svr Ready!! port=${port}\n`);\n }\n\n /**\n * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;\n * for each we wrap the matching client method (the proxy — RequestContext.run + header read +\n * JSON body parse + error→ApiErrorPayload all live in the wrapper/chain) and register the route.\n * This is one-to-one with a test: an HTTP POST maps straight to `client[method](dto)`.\n *\n * @returns the number of routes mounted for this api.\n */\n private mountApiClient(app: Express, apiClient: ApiClient): number {\n let count = 0;\n for (const route of apiClient.routes) {\n const path = this.expressPath(route);\n // The parser is chosen by the @Endpoint annotation, not the request Content-Type — and\n // so is whether the verbatim bytes survive the parse for an @WpAuthWebhook hook to verify.\n const wrapper = this.middleware.createExpressWrapper(\n apiClient.client[route.methodName],\n route,\n );\n this.registerHandler(app, route.httpMethod, path, wrapper.execute.bind(wrapper));\n count++;\n }\n return count;\n }\n\n /** Contract `{name}` placeholders -> Express 5 `:name` route syntax. */\n private expressPath(route: RouteMetadata): string {\n return route.path.replace(/\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, ':$1');\n }\n\n private registerHandler(\n app: Express,\n httpMethod: string,\n path: string,\n expressHandler: ExpressRouteHandler,\n ): void {\n switch (httpMethod.toLowerCase()) {\n case 'get':\n app.get(path, expressHandler);\n break;\n case 'post':\n app.post(path, expressHandler);\n break;\n case 'put':\n app.put(path, expressHandler);\n break;\n case 'delete':\n app.delete(path, expressHandler);\n break;\n case 'patch':\n app.patch(path, expressHandler);\n break;\n default:\n log.warn(`Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesExpressRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesExpressRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Express } from 'express';\nimport { ApiFactory, ApiClient, WebpiecesConfig } from '@webpieces/http-routing';\nimport { LogManager, RouteMetadata } from '@webpieces/core-util';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\nimport { RequestBodyReader } from './body/RequestBodyReader';\nimport { StreamBodyReader } from './body/StreamBodyReader';\n\nconst log = LogManager.getLogger('WebpiecesExpressRouter');\n\n/** The value returned by express `app.listen(...)` (a node http.Server). */\ntype HttpServer = ReturnType<Express['listen']>;\n\n/**\n * WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only\n * {@link ApiFactory} (a WebpiecesRouter). It is the ONLY place express lifecycle lives.\n *\n * It never reaches into routing internals: it asks the ApiFactory for `apiClients()` — each\n * an api + routeMeta + composed filter-chain→controller impl — and binds each to an express\n * route (`app.<verb>(path, handler)`) invoked when the matching HTTP request arrives. The\n * RouteBuilder stays hidden inside the ApiFactory.\n *\n * ```typescript\n * const apiFactory = await WebpiecesRouterFactory.create(config, { appBindings });\n * apiFactory.addRoutes(SaveApi, SaveController);\n * const express = new WebpiecesExpressRouter(apiFactory);\n *\n * // legacy / side-by-side: mount onto an existing app; you own listen + your middleware\n * express.bindExpress(existingApp);\n *\n * // non-legacy: add webpieces global middleware + listen for you\n * await express.bindAndStartExpress(express(), 8080);\n * ```\n */\nexport class WebpiecesExpressRouter {\n private readonly middleware = new WebpiecesMiddleware();\n /** Where every mounted route reads its body bytes from. See {@link setBodyReader}. */\n private bodyReader: RequestBodyReader = new StreamBodyReader();\n /** Set by {@link bindExpress}; a reader chosen after that would reach no route. */\n private bound = false;\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n /**\n * Choose how routes read the request body. Call it BEFORE {@link bindExpress}: each route captures\n * the reader when it is mounted.\n *\n * The default ({@link StreamBodyReader}) reads the request stream, which is right whenever\n * webpieces is mounted ahead of any body parser. A host that parses the body before the app runs\n * (Cloud Functions gen2 / Firebase, which keep the verbatim bytes on `req.rawBody`) needs\n * `router.setBodyReader(new PreConsumedBodyReader())`, or every POST fails fast (issue #937).\n *\n * @throws Error when called after {@link bindExpress}, because the already-mounted routes would\n * silently keep the old reader.\n */\n setBodyReader(bodyReader: RequestBodyReader): void {\n if (this.bound) {\n throw new Error(\n 'setBodyReader() was called after bindExpress(); the mounted routes already captured ' +\n 'their body reader. Call setBodyReader() before bindExpress(app).',\n );\n }\n this.bodyReader = bodyReader;\n }\n\n /**\n * Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,\n * express-tier + api-tier filter chain, error→JSON) onto the caller's express app.\n *\n * Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose other\n * routes must stay untouched. The caller owns app.listen() and any global middleware.\n */\n bindExpress(app: Express): void {\n this.bound = true;\n let count = 0;\n for (const apiClient of this.apiFactory.apiClients()) {\n count += this.mountApiClient(app, apiClient);\n }\n log.info(`Mounted ${count} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (optional CORS), bind the routes, mount the top-level\n * error handler AFTER them, then app.listen(port). Convenience for a non-legacy webpieces server where\n * webpieces owns the whole express app. Resolves with the http.Server once listening.\n *\n * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and\n * {@link WebpiecesMiddleware.corsMiddleware}.\n */\n async bindAndStartExpress(\n app: Express,\n port: number = 8080,\n config?: WebpiecesConfig,\n ): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n // CORS is OPT-IN, and stays OFF in production. A server that serves its own browser app does\n // not need it — a browser applies no cors check to a same-origin request — so mounting it\n // would only hand credentialed cross-origin read access to whatever it allows, for nothing.\n // It is needed solely when a browser on ANOTHER origin calls this api: `ng serve` in dev, or\n // a UI hosted on a different host. Those say so via corsOrigins.\n const corsOrigins = config?.corsOrigins ?? [];\n if (corsOrigins.length > 0) {\n app.use(this.middleware.corsMiddleware(config));\n }\n\n this.bindExpress(app);\n\n // Top-level error handler is mounted LAST (AFTER the routes). Express only forwards a\n // downstream failure to a 4-arg error middleware that sits BELOW the failing route — it does\n // NOT bubble errors back up through next(). See WebpiecesMiddleware.errorHandler.\n app.use(this.middleware.errorHandler.bind(this.middleware));\n\n return new Promise<HttpServer>(\n (resolve: (server: HttpServer) => void, reject: (err: Error) => void) => {\n const server: HttpServer = app.listen(port, (error?: Error) => {\n if (error) {\n log.error(`Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`Listening on http://localhost:${port}`);\n this.logStartupBanner(port);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * The \"Svr Ready!!\" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and\n * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied\n * verbatim from the production service it was ported from, so a familiar splash marks \"the server is up and reachable\".\n */\n private logStartupBanner(port: number): void {\n if (process.env['K_SERVICE']) {\n return;\n }\n log.info(`\n ___ _____ _\n/ _| | _ \\\\ | |\n\\\\ \\`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _\n \\`--. \\\\/ _ \\\\ '_\\\\ \\\\ / / _ \\\\ '_| | // _ \\\\/ _\\` |/ _\\` | | | |\n/\\\\_/ / _/ | \\\\ V / _/ | | |\\\\ \\\\ _/ (_| | (_| | |_| |\n\\\\___/ \\\\_|_| \\\\_/ \\\\_|_| \\\\_| \\\\_\\\\_|\\\\_,_|\\\\_,_|\\\\_, |\n _/ |\n |_/\n\n Svr Ready!! port=${port}\n`);\n }\n\n /**\n * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;\n * for each we wrap the matching client method (the proxy — RequestContext.run + header read +\n * JSON body parse + error→ApiErrorPayload all live in the wrapper/chain) and register the route.\n * This is one-to-one with a test: an HTTP POST maps straight to `client[method](dto)`.\n *\n * @returns the number of routes mounted for this api.\n */\n private mountApiClient(app: Express, apiClient: ApiClient): number {\n let count = 0;\n for (const route of apiClient.routes) {\n const path = this.expressPath(route);\n // The parser is chosen by the @Endpoint annotation, not the request Content-Type — and\n // so is whether the verbatim bytes survive the parse for an @WpAuthWebhook hook to verify.\n const wrapper = route.streaming\n ? this.middleware.createStreamExpressWrapper(\n apiClient.client[route.methodName],\n route,\n )\n : this.middleware.createExpressWrapper(\n apiClient.client[route.methodName],\n route,\n this.bodyReader,\n );\n this.registerHandler(app, route.httpMethod, path, wrapper.execute.bind(wrapper));\n count++;\n }\n return count;\n }\n\n /** Contract `{name}` placeholders -> Express 5 `:name` route syntax. */\n private expressPath(route: RouteMetadata): string {\n return route.path.replace(/\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, ':$1');\n }\n\n private registerHandler(\n app: Express,\n httpMethod: string,\n path: string,\n expressHandler: ExpressRouteHandler,\n ): void {\n switch (httpMethod.toLowerCase()) {\n case 'get':\n app.get(path, expressHandler);\n break;\n case 'post':\n app.post(path, expressHandler);\n break;\n case 'put':\n app.put(path, expressHandler);\n break;\n case 'delete':\n app.delete(path, expressHandler);\n break;\n case 'patch':\n app.patch(path, expressHandler);\n break;\n default:\n log.warn(`Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
|
|
@@ -2,6 +2,8 @@ import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
|
2
2
|
import { WebpiecesConfig } from '@webpieces/http-routing';
|
|
3
3
|
import { RouteMetadata } from '@webpieces/core-util';
|
|
4
4
|
import { ExpressWrapper } from './ExpressWrapper';
|
|
5
|
+
import { StreamExpressWrapper } from './StreamExpressWrapper';
|
|
6
|
+
import { RequestBodyReader } from './body/RequestBodyReader';
|
|
5
7
|
/**
|
|
6
8
|
* Express route handler function type. Lives in http-server (the express adapter),
|
|
7
9
|
* NOT in the node-only http-routing package, so http-routing stays express-free.
|
|
@@ -115,7 +117,11 @@ export declare class WebpiecesMiddleware {
|
|
|
115
117
|
* @param rawBody - True for an @Endpoint(..., { rawBody: true }) route: retain the verbatim
|
|
116
118
|
* bytes + absolute url on the HttpRequest so an @WpAuthWebhook hook can verify a vendor
|
|
117
119
|
* signature over them. Default false = the bytes are dropped once parsed.
|
|
120
|
+
* @param bodyReader - Where the body bytes come from (the router's choice, see
|
|
121
|
+
* `WebpiecesExpressRouter.setBodyReader`). Required so the router's choice can never be
|
|
122
|
+
* silently dropped on the way to the wrapper.
|
|
118
123
|
* @returns ExpressWrapper instance
|
|
119
124
|
*/
|
|
120
|
-
createExpressWrapper(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata): ExpressWrapper;
|
|
125
|
+
createExpressWrapper(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata, bodyReader: RequestBodyReader): ExpressWrapper;
|
|
126
|
+
createStreamExpressWrapper(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata): StreamExpressWrapper;
|
|
121
127
|
}
|
|
@@ -8,6 +8,7 @@ const core_util_1 = require("@webpieces/core-util");
|
|
|
8
8
|
const core_context_1 = require("@webpieces/core-context");
|
|
9
9
|
const core_util_2 = require("@webpieces/core-util");
|
|
10
10
|
const ExpressWrapper_1 = require("./ExpressWrapper");
|
|
11
|
+
const StreamExpressWrapper_1 = require("./StreamExpressWrapper");
|
|
11
12
|
const log = core_util_2.LogManager.getLogger('WebpiecesMiddleware');
|
|
12
13
|
// CORS mount/allow/block lines log under their own name (not [WebpiecesMiddleware]); the backend
|
|
13
14
|
// prepends "[CORS]" for us, so the message strings below carry no literal prefix of their own.
|
|
@@ -196,12 +197,20 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
196
197
|
* @param rawBody - True for an @Endpoint(..., { rawBody: true }) route: retain the verbatim
|
|
197
198
|
* bytes + absolute url on the HttpRequest so an @WpAuthWebhook hook can verify a vendor
|
|
198
199
|
* signature over them. Default false = the bytes are dropped once parsed.
|
|
200
|
+
* @param bodyReader - Where the body bytes come from (the router's choice, see
|
|
201
|
+
* `WebpiecesExpressRouter.setBodyReader`). Required so the router's choice can never be
|
|
202
|
+
* silently dropped on the way to the wrapper.
|
|
199
203
|
* @returns ExpressWrapper instance
|
|
200
204
|
*/
|
|
201
205
|
createExpressWrapper(
|
|
202
206
|
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
207
|
+
clientMethod, route, bodyReader) {
|
|
208
|
+
return new ExpressWrapper_1.ExpressWrapper(clientMethod, route.path, this.headers, route.formPost, route.rawBody, undefined, route, bodyReader);
|
|
209
|
+
}
|
|
210
|
+
createStreamExpressWrapper(
|
|
211
|
+
// webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary
|
|
203
212
|
clientMethod, route) {
|
|
204
|
-
return new
|
|
213
|
+
return new StreamExpressWrapper_1.StreamExpressWrapper(clientMethod, route, this.headers);
|
|
205
214
|
}
|
|
206
215
|
};
|
|
207
216
|
exports.WebpiecesMiddleware = WebpiecesMiddleware;
|