@webpieces/http-server 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 +5 -5
- package/src/JsonlDuplexExpressCall.d.ts +38 -0
- package/src/JsonlDuplexExpressCall.js +256 -0
- package/src/JsonlDuplexExpressCall.js.map +1 -0
- package/src/StreamExpressWrapper.d.ts +5 -9
- package/src/StreamExpressWrapper.js +47 -238
- package/src/StreamExpressWrapper.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.840",
|
|
4
4
|
"description": "WebPieces server with filter chain and dependency injection",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-context": "0.4.
|
|
26
|
-
"@webpieces/core-util": "0.4.
|
|
27
|
-
"@webpieces/gcp-identity": "0.4.
|
|
28
|
-
"@webpieces/http-routing": "0.4.
|
|
25
|
+
"@webpieces/core-context": "0.4.840",
|
|
26
|
+
"@webpieces/core-util": "0.4.840",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.840",
|
|
28
|
+
"@webpieces/http-routing": "0.4.840",
|
|
29
29
|
"cors": "2.8.5",
|
|
30
30
|
"express": "5.1.0",
|
|
31
31
|
"inversify": "7.10.4"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import { DtoValue, RouteMetadata, StreamEventValidator } from '@webpieces/core-util';
|
|
3
|
+
import { RequestContextHeaders } from '@webpieces/core-context';
|
|
4
|
+
/** Ordered raw-JSONL response writer with an atomic initial-response boundary. */
|
|
5
|
+
export declare class JsonlResponseWriter {
|
|
6
|
+
private readonly res;
|
|
7
|
+
private readonly route;
|
|
8
|
+
private readonly validator;
|
|
9
|
+
private opened;
|
|
10
|
+
private ended;
|
|
11
|
+
private pendingWrite;
|
|
12
|
+
private readonly queued;
|
|
13
|
+
private queuedBytes;
|
|
14
|
+
private failure?;
|
|
15
|
+
constructor(res: Response, route: RouteMetadata, validator: StreamEventValidator);
|
|
16
|
+
readonly stream: {
|
|
17
|
+
event: (value: DtoValue) => Promise<void>;
|
|
18
|
+
close: () => Promise<void>;
|
|
19
|
+
cancel: (error?: Error) => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
open(initial: DtoValue, headers: Map<string, string>): Promise<void>;
|
|
22
|
+
cancel(error?: Error): Promise<void>;
|
|
23
|
+
private flushOpening;
|
|
24
|
+
private deliver;
|
|
25
|
+
private write;
|
|
26
|
+
}
|
|
27
|
+
/** Handles Node-only REQUEST/FULL application/x-webpieces-jsonl routes. */
|
|
28
|
+
export declare class JsonlDuplexExpressCall {
|
|
29
|
+
private readonly clientMethod;
|
|
30
|
+
private readonly route;
|
|
31
|
+
private readonly headers;
|
|
32
|
+
private readonly validator;
|
|
33
|
+
constructor(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata, headers: RequestContextHeaders);
|
|
34
|
+
execute(req: Request, res: Response): Promise<void>;
|
|
35
|
+
private invoke;
|
|
36
|
+
private requestStream;
|
|
37
|
+
private parseData;
|
|
38
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JsonlDuplexExpressCall = exports.JsonlResponseWriter = 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 MAX_LINE_BYTES = 1024 * 1024;
|
|
8
|
+
/** Stateful line reader that preserves bytes received after the initial request record. */
|
|
9
|
+
class JsonlRequestReader {
|
|
10
|
+
iterator;
|
|
11
|
+
pending = Buffer.alloc(0);
|
|
12
|
+
done = false;
|
|
13
|
+
constructor(req) {
|
|
14
|
+
this.iterator = req[Symbol.asyncIterator]();
|
|
15
|
+
}
|
|
16
|
+
async nextLine() {
|
|
17
|
+
for (;;) {
|
|
18
|
+
const newline = this.pending.indexOf(10);
|
|
19
|
+
if (newline >= 0) {
|
|
20
|
+
if (newline > MAX_LINE_BYTES)
|
|
21
|
+
throw new core_util_1.StreamProtocolError(`JSONL record exceeds ${MAX_LINE_BYTES} bytes.`);
|
|
22
|
+
const line = this.pending.subarray(0, newline).toString('utf8').replace(/\r$/, '');
|
|
23
|
+
this.pending = this.pending.subarray(newline + 1);
|
|
24
|
+
if (line === '')
|
|
25
|
+
continue;
|
|
26
|
+
return line;
|
|
27
|
+
}
|
|
28
|
+
if (this.done) {
|
|
29
|
+
if (this.pending.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
throw new core_util_1.StreamProtocolError('JSONL request ended mid-record.');
|
|
32
|
+
}
|
|
33
|
+
const chunk = await this.iterator.next();
|
|
34
|
+
this.done = chunk.done === true;
|
|
35
|
+
if (chunk.value !== undefined) {
|
|
36
|
+
const bytes = Buffer.isBuffer(chunk.value) ? chunk.value : Buffer.from(chunk.value);
|
|
37
|
+
this.pending = Buffer.concat([this.pending, bytes]);
|
|
38
|
+
}
|
|
39
|
+
if (this.pending.length > MAX_LINE_BYTES && this.pending.indexOf(10) < 0) {
|
|
40
|
+
throw new core_util_1.StreamTransportError(`JSONL record exceeds ${MAX_LINE_BYTES} bytes.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Ordered raw-JSONL response writer with an atomic initial-response boundary. */
|
|
46
|
+
class JsonlResponseWriter {
|
|
47
|
+
res;
|
|
48
|
+
route;
|
|
49
|
+
validator;
|
|
50
|
+
opened = false;
|
|
51
|
+
ended = false;
|
|
52
|
+
pendingWrite = Promise.resolve();
|
|
53
|
+
queued = [];
|
|
54
|
+
queuedBytes = 0;
|
|
55
|
+
failure;
|
|
56
|
+
constructor(res, route, validator) {
|
|
57
|
+
this.res = res;
|
|
58
|
+
this.route = route;
|
|
59
|
+
this.validator = validator;
|
|
60
|
+
res.once('close', () => {
|
|
61
|
+
if (!res.writableEnded)
|
|
62
|
+
this.failure = new core_util_1.StreamTransportError('Streaming HTTP peer disconnected.');
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
stream = {
|
|
66
|
+
event: async (value) => {
|
|
67
|
+
if (this.failure)
|
|
68
|
+
throw this.failure;
|
|
69
|
+
if (this.ended)
|
|
70
|
+
throw new core_util_1.StreamTransportError('Cannot write after stream termination.');
|
|
71
|
+
if (this.route.streaming?.responseSchema) {
|
|
72
|
+
this.validator.validate(this.route.streaming.responseSchema, value, 'response');
|
|
73
|
+
}
|
|
74
|
+
await this.deliver(`${JSON.stringify(value)}\n`);
|
|
75
|
+
},
|
|
76
|
+
close: async () => {
|
|
77
|
+
this.ended = true;
|
|
78
|
+
await this.pendingWrite;
|
|
79
|
+
if (this.opened)
|
|
80
|
+
this.res.end();
|
|
81
|
+
},
|
|
82
|
+
cancel: async (error) => this.cancel(error),
|
|
83
|
+
};
|
|
84
|
+
async open(initial, headers) {
|
|
85
|
+
if (this.route.streaming?.initialResponseSchema) {
|
|
86
|
+
this.validator.validate(this.route.streaming.initialResponseSchema, initial, 'response');
|
|
87
|
+
}
|
|
88
|
+
this.res.status(200);
|
|
89
|
+
this.res.setHeader('Content-Type', 'application/x-webpieces-jsonl; charset=utf-8');
|
|
90
|
+
this.res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
91
|
+
this.res.setHeader('X-Accel-Buffering', 'no');
|
|
92
|
+
for (const entry of headers)
|
|
93
|
+
this.res.setHeader(entry[0], entry[1]);
|
|
94
|
+
this.res.flushHeaders();
|
|
95
|
+
this.opened = true;
|
|
96
|
+
this.pendingWrite = this.flushOpening(initial);
|
|
97
|
+
await this.pendingWrite;
|
|
98
|
+
}
|
|
99
|
+
async cancel(error) {
|
|
100
|
+
if (this.failure)
|
|
101
|
+
return;
|
|
102
|
+
if (!error || this.ended) {
|
|
103
|
+
this.failure = error ?? new core_util_1.StreamTransportError('Local stream cancellation.');
|
|
104
|
+
this.res.destroy(this.failure);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a failed error-control write must reset rather than send an invented remote DTO
|
|
108
|
+
try {
|
|
109
|
+
await this.deliver(new core_util_1.StreamErrorControl().encode(error));
|
|
110
|
+
this.failure = error;
|
|
111
|
+
this.ended = true;
|
|
112
|
+
if (this.opened)
|
|
113
|
+
this.res.end();
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const error = (0, core_util_1.toError)(err);
|
|
117
|
+
this.failure = error;
|
|
118
|
+
this.res.destroy(error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async flushOpening(initial) {
|
|
122
|
+
await this.write(`${JSON.stringify(initial)}\n`);
|
|
123
|
+
for (const record of this.queued)
|
|
124
|
+
await this.write(record);
|
|
125
|
+
this.queued.length = 0;
|
|
126
|
+
if (this.ended)
|
|
127
|
+
this.res.end();
|
|
128
|
+
}
|
|
129
|
+
async deliver(record) {
|
|
130
|
+
if (!this.opened) {
|
|
131
|
+
this.queuedBytes += Buffer.byteLength(record);
|
|
132
|
+
if (this.queuedBytes > 64 * 1024)
|
|
133
|
+
throw new core_util_1.StreamTransportError('Pre-handshake response buffer exceeds 65536 bytes.');
|
|
134
|
+
this.queued.push(record);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const delivery = this.pendingWrite.then(() => this.write(record));
|
|
138
|
+
this.pendingWrite = delivery.catch(() => undefined);
|
|
139
|
+
await delivery;
|
|
140
|
+
}
|
|
141
|
+
async write(record) {
|
|
142
|
+
if (this.res.destroyed || this.res.writableEnded) {
|
|
143
|
+
throw new core_util_1.StreamTransportError('Cannot write to a closed streaming HTTP response.');
|
|
144
|
+
}
|
|
145
|
+
if (this.res.write(record))
|
|
146
|
+
return;
|
|
147
|
+
await Promise.race([
|
|
148
|
+
(0, node_events_1.once)(this.res, 'drain'),
|
|
149
|
+
(0, node_events_1.once)(this.res, 'close').then(() => {
|
|
150
|
+
throw new core_util_1.StreamTransportError('Streaming HTTP peer disconnected during write.');
|
|
151
|
+
}),
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.JsonlResponseWriter = JsonlResponseWriter;
|
|
156
|
+
/** Handles Node-only REQUEST/FULL application/x-webpieces-jsonl routes. */
|
|
157
|
+
class JsonlDuplexExpressCall {
|
|
158
|
+
clientMethod;
|
|
159
|
+
route;
|
|
160
|
+
headers;
|
|
161
|
+
validator = new core_util_1.StreamEventValidator();
|
|
162
|
+
constructor(
|
|
163
|
+
// webpieces-disable no-any-unknown -- controller boundary has contract-erased DTO types
|
|
164
|
+
clientMethod, route, headers) {
|
|
165
|
+
this.clientMethod = clientMethod;
|
|
166
|
+
this.route = route;
|
|
167
|
+
this.headers = headers;
|
|
168
|
+
}
|
|
169
|
+
async execute(req, res) {
|
|
170
|
+
const reader = new JsonlRequestReader(req);
|
|
171
|
+
const initial = this.parseData(await reader.nextLine(), 'initial request');
|
|
172
|
+
const metadata = this.route.streaming;
|
|
173
|
+
if (metadata.initialRequestSchema) {
|
|
174
|
+
this.validator.validate(metadata.initialRequestSchema, initial, 'request');
|
|
175
|
+
}
|
|
176
|
+
const outbound = new JsonlResponseWriter(res, this.route, this.validator);
|
|
177
|
+
const returned = await this.invoke(initial, outbound);
|
|
178
|
+
const inbound = this.requestStream(returned);
|
|
179
|
+
const initialResponse = inbound.getInitialResponse();
|
|
180
|
+
const events = new core_context_1.StreamEventContext();
|
|
181
|
+
await outbound.open(initialResponse, this.headers.buildResponseHeaders());
|
|
182
|
+
let cancelled = false;
|
|
183
|
+
const cancel = async () => {
|
|
184
|
+
if (cancelled)
|
|
185
|
+
return;
|
|
186
|
+
cancelled = true;
|
|
187
|
+
const error = new core_util_1.StreamTransportError('Streaming HTTP peer disconnected.');
|
|
188
|
+
await inbound.cancel(error);
|
|
189
|
+
};
|
|
190
|
+
req.once('aborted', () => void cancel());
|
|
191
|
+
res.once('close', () => {
|
|
192
|
+
if (!res.writableEnded)
|
|
193
|
+
void cancel();
|
|
194
|
+
});
|
|
195
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- post-open failures must be written as sideband controls
|
|
196
|
+
try {
|
|
197
|
+
for (;;) {
|
|
198
|
+
const line = await reader.nextLine();
|
|
199
|
+
if (line === undefined)
|
|
200
|
+
break;
|
|
201
|
+
if (line.charCodeAt(0) === 0x1e) {
|
|
202
|
+
const error = new core_util_1.StreamErrorControl().decode(line);
|
|
203
|
+
await inbound.cancel(error);
|
|
204
|
+
await outbound.cancel(error);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const value = this.parseData(line, 'request event');
|
|
208
|
+
if (metadata.requestSchema) {
|
|
209
|
+
this.validator.validate(metadata.requestSchema, value, 'request');
|
|
210
|
+
}
|
|
211
|
+
await events.run(() => inbound.event(value));
|
|
212
|
+
}
|
|
213
|
+
await inbound.close();
|
|
214
|
+
if (metadata.direction === core_util_1.StreamDirection.REQUEST)
|
|
215
|
+
await outbound.stream.close();
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const error = (0, core_util_1.toError)(err);
|
|
219
|
+
await outbound.cancel(error);
|
|
220
|
+
await inbound.cancel(error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// webpieces-disable no-any-unknown -- controller return is validated as a typed RequestStream before opening
|
|
224
|
+
async invoke(initial, outbound) {
|
|
225
|
+
if (this.route.streaming?.direction === core_util_1.StreamDirection.FULL) {
|
|
226
|
+
return this.clientMethod(initial, outbound.stream);
|
|
227
|
+
}
|
|
228
|
+
return this.clientMethod(initial);
|
|
229
|
+
}
|
|
230
|
+
// webpieces-disable no-any-unknown -- runtime guard narrows controller return value
|
|
231
|
+
requestStream(value) {
|
|
232
|
+
if (typeof value !== 'object' ||
|
|
233
|
+
value === null ||
|
|
234
|
+
typeof Reflect.get(value, 'getInitialResponse') !== 'function' ||
|
|
235
|
+
typeof Reflect.get(value, 'event') !== 'function' ||
|
|
236
|
+
typeof Reflect.get(value, 'close') !== 'function' ||
|
|
237
|
+
typeof Reflect.get(value, 'cancel') !== 'function') {
|
|
238
|
+
throw new core_util_1.StreamTransportError('Streaming controller did not return a RequestStream.');
|
|
239
|
+
}
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
parseData(line, label) {
|
|
243
|
+
if (line === undefined)
|
|
244
|
+
throw new core_util_1.StreamTransportError(`Missing ${label} JSONL record.`);
|
|
245
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted JSONL boundary
|
|
246
|
+
try {
|
|
247
|
+
return JSON.parse(line);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const error = (0, core_util_1.toError)(err);
|
|
251
|
+
throw new core_util_1.StreamTransportError(`Malformed ${label} JSONL record.`, undefined, undefined, error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
exports.JsonlDuplexExpressCall = JsonlDuplexExpressCall;
|
|
256
|
+
//# sourceMappingURL=JsonlDuplexExpressCall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JsonlDuplexExpressCall.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/JsonlDuplexExpressCall.ts"],"names":[],"mappings":"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","sourcesContent":["import { once } from 'node:events';\nimport { Request, Response } from 'express';\nimport {\n StreamErrorControl,\n DtoValue,\n RequestStream,\n RouteMetadata,\n StreamDirection,\n StreamEventValidator,\n StreamTransportError,\n StreamProtocolError,\n toError,\n} from '@webpieces/core-util';\nimport { RequestContextHeaders, StreamEventContext } from '@webpieces/core-context';\n\nconst MAX_LINE_BYTES = 1024 * 1024;\n\n/** Stateful line reader that preserves bytes received after the initial request record. */\nclass JsonlRequestReader {\n private readonly iterator: AsyncIterator<Buffer | string>;\n private pending = Buffer.alloc(0);\n private done = false;\n\n constructor(req: Request) {\n this.iterator = req[Symbol.asyncIterator]() as AsyncIterator<Buffer | string>;\n }\n\n async nextLine(): Promise<string | undefined> {\n for (;;) {\n const newline = this.pending.indexOf(10);\n if (newline >= 0) {\n if (newline > MAX_LINE_BYTES)\n throw new StreamProtocolError(`JSONL record exceeds ${MAX_LINE_BYTES} bytes.`);\n const line = this.pending.subarray(0, newline).toString('utf8').replace(/\\r$/, '');\n this.pending = this.pending.subarray(newline + 1);\n if (line === '') continue;\n return line;\n }\n if (this.done) {\n if (this.pending.length === 0) return undefined;\n throw new StreamProtocolError('JSONL request ended mid-record.');\n }\n const chunk = await this.iterator.next();\n this.done = chunk.done === true;\n if (chunk.value !== undefined) {\n const bytes = Buffer.isBuffer(chunk.value) ? chunk.value : Buffer.from(chunk.value);\n this.pending = Buffer.concat([this.pending, bytes]);\n }\n if (this.pending.length > MAX_LINE_BYTES && this.pending.indexOf(10) < 0) {\n throw new StreamTransportError(`JSONL record exceeds ${MAX_LINE_BYTES} bytes.`);\n }\n }\n }\n}\n\n/** Ordered raw-JSONL response writer with an atomic initial-response boundary. */\nexport class JsonlResponseWriter {\n private opened = false;\n private ended = false;\n private pendingWrite: Promise<void> = Promise.resolve();\n private readonly queued: string[] = [];\n private queuedBytes = 0;\n private failure?: Error;\n\n constructor(\n private readonly res: Response,\n private readonly route: RouteMetadata,\n private readonly validator: StreamEventValidator,\n ) {\n res.once('close', (): void => {\n if (!res.writableEnded)\n this.failure = new StreamTransportError('Streaming HTTP peer disconnected.');\n });\n }\n\n readonly stream = {\n event: async (value: DtoValue): Promise<void> => {\n if (this.failure) throw this.failure;\n if (this.ended)\n throw new StreamTransportError('Cannot write after stream termination.');\n if (this.route.streaming?.responseSchema) {\n this.validator.validate(this.route.streaming.responseSchema, value, 'response');\n }\n await this.deliver(`${JSON.stringify(value)}\\n`);\n },\n close: async (): Promise<void> => {\n this.ended = true;\n await this.pendingWrite;\n if (this.opened) this.res.end();\n },\n cancel: async (error?: Error): Promise<void> => this.cancel(error),\n };\n\n async open(initial: DtoValue, headers: Map<string, string>): Promise<void> {\n if (this.route.streaming?.initialResponseSchema) {\n this.validator.validate(\n this.route.streaming.initialResponseSchema,\n initial,\n 'response',\n );\n }\n this.res.status(200);\n this.res.setHeader('Content-Type', 'application/x-webpieces-jsonl; charset=utf-8');\n this.res.setHeader('Cache-Control', 'no-cache, no-transform');\n this.res.setHeader('X-Accel-Buffering', 'no');\n for (const entry of headers) this.res.setHeader(entry[0], entry[1]);\n this.res.flushHeaders();\n this.opened = true;\n this.pendingWrite = this.flushOpening(initial);\n await this.pendingWrite;\n }\n\n async cancel(error?: Error): Promise<void> {\n if (this.failure) return;\n if (!error || this.ended) {\n this.failure = error ?? new StreamTransportError('Local stream cancellation.');\n this.res.destroy(this.failure);\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a failed error-control write must reset rather than send an invented remote DTO\n try {\n await this.deliver(new StreamErrorControl().encode(error));\n this.failure = error;\n this.ended = true;\n if (this.opened) this.res.end();\n } catch (err: unknown) {\n const error = toError(err);\n this.failure = error;\n this.res.destroy(error);\n }\n }\n\n private async flushOpening(initial: DtoValue): Promise<void> {\n await this.write(`${JSON.stringify(initial)}\\n`);\n for (const record of this.queued) await this.write(record);\n this.queued.length = 0;\n if (this.ended) this.res.end();\n }\n\n private async deliver(record: string): Promise<void> {\n if (!this.opened) {\n this.queuedBytes += Buffer.byteLength(record);\n if (this.queuedBytes > 64 * 1024)\n throw new StreamTransportError(\n 'Pre-handshake response buffer exceeds 65536 bytes.',\n );\n this.queued.push(record);\n return;\n }\n const delivery = this.pendingWrite.then((): Promise<void> => this.write(record));\n this.pendingWrite = delivery.catch((): void => undefined);\n await delivery;\n }\n\n private async write(record: 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(record)) return;\n await Promise.race([\n once(this.res, 'drain'),\n once(this.res, 'close').then((): never => {\n throw new StreamTransportError('Streaming HTTP peer disconnected during write.');\n }),\n ]);\n }\n}\n\n/** Handles Node-only REQUEST/FULL application/x-webpieces-jsonl routes. */\nexport class JsonlDuplexExpressCall {\n private readonly validator = new StreamEventValidator();\n\n constructor(\n // webpieces-disable no-any-unknown -- controller boundary has contract-erased DTO types\n private readonly clientMethod: (...args: unknown[]) => Promise<unknown>,\n private readonly route: RouteMetadata,\n private readonly headers: RequestContextHeaders,\n ) {}\n\n async execute(req: Request, res: Response): Promise<void> {\n const reader = new JsonlRequestReader(req);\n const initial = this.parseData(await reader.nextLine(), 'initial request');\n const metadata = this.route.streaming!;\n if (metadata.initialRequestSchema) {\n this.validator.validate(metadata.initialRequestSchema, initial, 'request');\n }\n const outbound = new JsonlResponseWriter(res, this.route, this.validator);\n const returned = await this.invoke(initial, outbound);\n const inbound = this.requestStream(returned);\n const initialResponse = inbound.getInitialResponse();\n const events = new StreamEventContext();\n await outbound.open(initialResponse, this.headers.buildResponseHeaders());\n let cancelled = false;\n const cancel = async (): Promise<void> => {\n if (cancelled) return;\n cancelled = true;\n const error = new StreamTransportError('Streaming HTTP peer disconnected.');\n await inbound.cancel(error);\n };\n req.once('aborted', (): void => void cancel());\n res.once('close', (): void => {\n if (!res.writableEnded) void cancel();\n });\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- post-open failures must be written as sideband controls\n try {\n for (;;) {\n const line = await reader.nextLine();\n if (line === undefined) break;\n if (line.charCodeAt(0) === 0x1e) {\n const error = new StreamErrorControl().decode(line);\n await inbound.cancel(error);\n await outbound.cancel(error);\n return;\n }\n const value = this.parseData(line, 'request event');\n if (metadata.requestSchema) {\n this.validator.validate(metadata.requestSchema, value, 'request');\n }\n await events.run((): Promise<void> => inbound.event(value));\n }\n await inbound.close();\n if (metadata.direction === StreamDirection.REQUEST) await outbound.stream.close();\n } catch (err: unknown) {\n const error = toError(err);\n await outbound.cancel(error);\n await inbound.cancel(error);\n }\n }\n\n // webpieces-disable no-any-unknown -- controller return is validated as a typed RequestStream before opening\n private async invoke(initial: DtoValue, outbound: JsonlResponseWriter): Promise<unknown> {\n if (this.route.streaming?.direction === StreamDirection.FULL) {\n return this.clientMethod(initial, outbound.stream);\n }\n return this.clientMethod(initial);\n }\n\n // webpieces-disable no-any-unknown -- runtime guard narrows controller return value\n private requestStream(value: unknown): RequestStream<DtoValue, DtoValue> {\n if (\n typeof value !== 'object' ||\n value === null ||\n typeof Reflect.get(value, 'getInitialResponse') !== 'function' ||\n typeof Reflect.get(value, 'event') !== 'function' ||\n typeof Reflect.get(value, 'close') !== 'function' ||\n typeof Reflect.get(value, 'cancel') !== 'function'\n ) {\n throw new StreamTransportError('Streaming controller did not return a RequestStream.');\n }\n return value as RequestStream<DtoValue, DtoValue>;\n }\n\n private parseData(line: string | undefined, label: string): DtoValue {\n if (line === undefined) throw new StreamTransportError(`Missing ${label} JSONL record.`);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted JSONL boundary\n try {\n return JSON.parse(line) as DtoValue;\n } catch (err: unknown) {\n const error = toError(err);\n throw new StreamTransportError(\n `Malformed ${label} JSONL record.`,\n undefined,\n undefined,\n error,\n );\n }\n }\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextFunction, Request, Response } from 'express';
|
|
2
2
|
import { RouteMetadata } from '@webpieces/core-util';
|
|
3
3
|
import { RequestContextHeaders } from '@webpieces/core-context';
|
|
4
|
-
/** Express adapter for
|
|
4
|
+
/** Express adapter for the three typed application/x-webpieces-jsonl contract forms. */
|
|
5
5
|
export declare class StreamExpressWrapper {
|
|
6
6
|
private readonly clientMethod;
|
|
7
7
|
private readonly route;
|
|
@@ -9,14 +9,10 @@ export declare class StreamExpressWrapper {
|
|
|
9
9
|
private readonly ordinaryErrors;
|
|
10
10
|
private readonly validator;
|
|
11
11
|
constructor(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata, headers: RequestContextHeaders);
|
|
12
|
-
execute(req: Request, res: Response,
|
|
12
|
+
execute(req: Request, res: Response, _next: NextFunction): Promise<void>;
|
|
13
13
|
private executeInContext;
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private
|
|
17
|
-
private parseEnvelope;
|
|
18
|
-
private parseCorrelation;
|
|
19
|
-
private requireNdjson;
|
|
20
|
-
private isRequestWriter;
|
|
14
|
+
private executeResponseStream;
|
|
15
|
+
private readInitial;
|
|
16
|
+
private requireContentType;
|
|
21
17
|
private toWebpiecesRequest;
|
|
22
18
|
}
|
|
@@ -1,98 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.StreamExpressWrapper = void 0;
|
|
4
|
-
const node_events_1 = require("node:events");
|
|
5
4
|
const core_util_1 = require("@webpieces/core-util");
|
|
6
5
|
const core_context_1 = require("@webpieces/core-context");
|
|
7
6
|
const ExpressWrapper_1 = require("./ExpressWrapper");
|
|
8
|
-
const
|
|
9
|
-
|
|
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.responseSchema, 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. */
|
|
7
|
+
const JsonlDuplexExpressCall_1 = require("./JsonlDuplexExpressCall");
|
|
8
|
+
/** Express adapter for the three typed application/x-webpieces-jsonl contract forms. */
|
|
96
9
|
class StreamExpressWrapper {
|
|
97
10
|
clientMethod;
|
|
98
11
|
route;
|
|
@@ -107,184 +20,80 @@ class StreamExpressWrapper {
|
|
|
107
20
|
this.headers = headers;
|
|
108
21
|
this.ordinaryErrors = new ExpressWrapper_1.ExpressWrapper(clientMethod, route.path, headers, false, false, undefined, route);
|
|
109
22
|
}
|
|
110
|
-
async execute(req, res,
|
|
111
|
-
void next;
|
|
23
|
+
async execute(req, res, _next) {
|
|
112
24
|
await core_context_1.RequestContext.run(async () => this.executeInContext(req, res));
|
|
113
25
|
}
|
|
114
26
|
async executeInContext(req, res) {
|
|
115
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- owning
|
|
27
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- owning HTTP boundary maps pre-open failures with ordinary HTTP errors
|
|
116
28
|
try {
|
|
117
|
-
this.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
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.'));
|
|
29
|
+
const request = this.toWebpiecesRequest(req);
|
|
30
|
+
core_context_1.RequestContext.setRequest(request);
|
|
31
|
+
this.headers.fillFromRequest(request);
|
|
32
|
+
this.requireContentType(req);
|
|
33
|
+
if (this.route.streaming?.direction === core_util_1.StreamDirection.RESPONSE) {
|
|
34
|
+
await this.executeResponseStream(req, res);
|
|
128
35
|
return;
|
|
129
36
|
}
|
|
130
|
-
await this.
|
|
37
|
+
await new JsonlDuplexExpressCall_1.JsonlDuplexExpressCall(this.clientMethod, this.route, this.headers).execute(req, res);
|
|
131
38
|
}
|
|
132
39
|
catch (err) {
|
|
133
40
|
const error = (0, core_util_1.toError)(err);
|
|
134
|
-
|
|
41
|
+
if (res.headersSent)
|
|
42
|
+
res.destroy(error);
|
|
43
|
+
else
|
|
44
|
+
this.ordinaryErrors.handleError(res, error);
|
|
135
45
|
}
|
|
136
46
|
}
|
|
137
|
-
async
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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);
|
|
47
|
+
async executeResponseStream(req, res) {
|
|
48
|
+
const initial = await this.readInitial(req);
|
|
49
|
+
if (this.route.streaming?.initialRequestSchema) {
|
|
50
|
+
this.validator.validate(this.route.streaming.initialRequestSchema, initial, 'request');
|
|
184
51
|
}
|
|
52
|
+
const outbound = new JsonlDuplexExpressCall_1.JsonlResponseWriter(res, this.route, this.validator);
|
|
53
|
+
const response = (await this.clientMethod(initial, outbound.stream));
|
|
54
|
+
await outbound.open(response, this.headers.buildResponseHeaders());
|
|
185
55
|
}
|
|
186
|
-
async
|
|
187
|
-
|
|
56
|
+
async readInitial(req) {
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let bytes = 0;
|
|
188
59
|
for await (const raw of req) {
|
|
189
60
|
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
throw new core_util_1.StreamTransportError(
|
|
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);
|
|
61
|
+
bytes += chunk.length;
|
|
62
|
+
if (bytes > 1024 * 1024) {
|
|
63
|
+
throw new core_util_1.StreamTransportError('Initial JSONL record exceeds 1048576 bytes.');
|
|
201
64
|
}
|
|
65
|
+
chunks.push(chunk);
|
|
202
66
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
67
|
+
const lines = Buffer.concat(chunks)
|
|
68
|
+
.toString('utf8')
|
|
69
|
+
.split(/\r?\n/)
|
|
70
|
+
.filter((line) => line !== '');
|
|
71
|
+
if (lines.length !== 1) {
|
|
72
|
+
throw new core_util_1.StreamTransportError('RESPONSE streaming requires exactly one initial request record.');
|
|
73
|
+
}
|
|
74
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted request boundary
|
|
210
75
|
try {
|
|
211
|
-
|
|
76
|
+
return JSON.parse(lines[0]);
|
|
212
77
|
}
|
|
213
78
|
catch (err) {
|
|
214
79
|
const error = (0, core_util_1.toError)(err);
|
|
215
|
-
throw new core_util_1.StreamTransportError('Malformed
|
|
216
|
-
}
|
|
217
|
-
const envelope = this.parseEnvelope(value);
|
|
218
|
-
if (envelope.kind === 'event') {
|
|
219
|
-
this.validator.validate(this.route.streaming.requestSchema, 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();
|
|
80
|
+
throw new core_util_1.StreamTransportError('Malformed initial JSONL request.', undefined, undefined, error);
|
|
229
81
|
}
|
|
230
82
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
83
|
+
requireContentType(req) {
|
|
84
|
+
const contentType = req.get('content-type')?.split(';', 1)[0].trim().toLowerCase();
|
|
85
|
+
if (contentType !== 'application/x-webpieces-jsonl') {
|
|
86
|
+
throw new core_util_1.ApiBadRequestError('Streaming endpoints require Content-Type application/x-webpieces-jsonl.');
|
|
234
87
|
}
|
|
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
88
|
}
|
|
281
89
|
toWebpiecesRequest(req) {
|
|
282
90
|
const headers = new Map();
|
|
283
|
-
for (const
|
|
91
|
+
for (const entry of Object.entries(req.headers)) {
|
|
92
|
+
const value = entry[1];
|
|
284
93
|
if (typeof value === 'string')
|
|
285
|
-
headers.set(
|
|
94
|
+
headers.set(entry[0].toLowerCase(), [value]);
|
|
286
95
|
else if (Array.isArray(value))
|
|
287
|
-
headers.set(
|
|
96
|
+
headers.set(entry[0].toLowerCase(), [...value]);
|
|
288
97
|
}
|
|
289
98
|
return new core_context_1.HttpRequest(req.method, req.path ?? this.route.path, headers);
|
|
290
99
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamExpressWrapper.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/StreamExpressWrapper.ts"],"names":[],"mappings":"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","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!.responseSchema, 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(this.route.streaming!.requestSchema, envelope.value, 'request');\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
|
+
{"version":3,"file":"StreamExpressWrapper.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/StreamExpressWrapper.ts"],"names":[],"mappings":"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","sourcesContent":["import { NextFunction, Request, Response } from 'express';\nimport {\n ApiBadRequestError,\n DtoValue,\n RouteMetadata,\n StreamDirection,\n StreamEventValidator,\n StreamTransportError,\n toError,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { ExpressWrapper } from './ExpressWrapper';\nimport { JsonlDuplexExpressCall, JsonlResponseWriter } from './JsonlDuplexExpressCall';\n\n/** Express adapter for the three typed application/x-webpieces-jsonl contract forms. */\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 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 HTTP boundary maps pre-open failures with ordinary HTTP errors\n try {\n const request = this.toWebpiecesRequest(req);\n RequestContext.setRequest(request);\n this.headers.fillFromRequest(request);\n this.requireContentType(req);\n if (this.route.streaming?.direction === StreamDirection.RESPONSE) {\n await this.executeResponseStream(req, res);\n return;\n }\n await new JsonlDuplexExpressCall(this.clientMethod, this.route, this.headers).execute(\n req,\n res,\n );\n } catch (err: unknown) {\n const error = toError(err);\n if (res.headersSent) res.destroy(error);\n else this.ordinaryErrors.handleError(res, error);\n }\n }\n\n private async executeResponseStream(req: Request, res: Response): Promise<void> {\n const initial = await this.readInitial(req);\n if (this.route.streaming?.initialRequestSchema) {\n this.validator.validate(this.route.streaming.initialRequestSchema, initial, 'request');\n }\n const outbound = new JsonlResponseWriter(res, this.route, this.validator);\n const response = (await this.clientMethod(initial, outbound.stream)) as DtoValue;\n await outbound.open(response, this.headers.buildResponseHeaders());\n }\n\n private async readInitial(req: Request): Promise<DtoValue> {\n const chunks: Buffer[] = [];\n let bytes = 0;\n for await (const raw of req) {\n const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n bytes += chunk.length;\n if (bytes > 1024 * 1024) {\n throw new StreamTransportError('Initial JSONL record exceeds 1048576 bytes.');\n }\n chunks.push(chunk);\n }\n const lines = Buffer.concat(chunks)\n .toString('utf8')\n .split(/\\r?\\n/)\n .filter((line: string): boolean => line !== '');\n if (lines.length !== 1) {\n throw new StreamTransportError(\n 'RESPONSE streaming requires exactly one initial request record.',\n );\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted request boundary\n try {\n return JSON.parse(lines[0]) as DtoValue;\n } catch (err: unknown) {\n const error = toError(err);\n throw new StreamTransportError(\n 'Malformed initial JSONL request.',\n undefined,\n undefined,\n error,\n );\n }\n }\n\n private requireContentType(req: Request): void {\n const contentType = req.get('content-type')?.split(';', 1)[0].trim().toLowerCase();\n if (contentType !== 'application/x-webpieces-jsonl') {\n throw new ApiBadRequestError(\n 'Streaming endpoints require Content-Type application/x-webpieces-jsonl.',\n );\n }\n }\n\n private toWebpiecesRequest(req: Request): HttpRequest {\n const headers = new Map<string, string[]>();\n for (const entry of Object.entries(req.headers)) {\n const value = entry[1];\n if (typeof value === 'string') headers.set(entry[0].toLowerCase(), [value]);\n else if (Array.isArray(value)) headers.set(entry[0].toLowerCase(), [...value]);\n }\n return new HttpRequest(req.method, req.path ?? this.route.path, headers);\n }\n}\n"]}
|