@linebridge/engine-neo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +26 -0
- package/lib/SSEventStream.d.ts +29 -0
- package/lib/SSEventStream.js +85 -0
- package/lib/close.d.ts +11 -0
- package/lib/close.js +23 -0
- package/lib/engine.d.ts +105 -0
- package/lib/engine.js +221 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +1 -0
- package/lib/listen.d.ts +13 -0
- package/lib/listen.js +44 -0
- package/lib/on_request.d.ts +21 -0
- package/lib/on_request.js +97 -0
- package/lib/register.d.ts +17 -0
- package/lib/register.js +5 -0
- package/lib/register_middleware.d.ts +14 -0
- package/lib/register_middleware.js +42 -0
- package/lib/request.d.ts +168 -0
- package/lib/request.js +400 -0
- package/lib/request_iterator.d.ts +22 -0
- package/lib/request_iterator.js +61 -0
- package/lib/response.d.ts +306 -0
- package/lib/response.js +742 -0
- package/lib/route_register.d.ts +10 -0
- package/lib/route_register.js +67 -0
- package/lib/utils.d.ts +14 -0
- package/lib/utils.js +30 -0
- package/lib/ws/Websocket.d.ts +173 -0
- package/lib/ws/Websocket.js +381 -0
- package/package.json +32 -0
- package/uws-wrapper.node +0 -0
package/lib/response.js
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Full HTTP response wrapper for the Neo engine.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a uWS HttpResponse and provides Express-style methods for status codes,
|
|
5
|
+
* headers, cookies (with signed cookie support), streaming, file serving,
|
|
6
|
+
* Server-Sent Events, redirects, and JSON/HTML shortcuts.
|
|
7
|
+
*/
|
|
8
|
+
import * as cookie from "cookie";
|
|
9
|
+
import signature from "cookie-signature";
|
|
10
|
+
import { STATUS_CODES } from "http";
|
|
11
|
+
import mime_types from "mime-types";
|
|
12
|
+
import SSEventStream from "./SSEventStream.js";
|
|
13
|
+
const stringify = JSON.stringify;
|
|
14
|
+
/** Pre-built status-line cache so we don't concatenate strings on every response. */
|
|
15
|
+
const STATUS_CACHE = Object.create(null);
|
|
16
|
+
for (const code in STATUS_CODES) {
|
|
17
|
+
STATUS_CACHE[code] = `${code} ${STATUS_CODES[code]}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* HTTP Response class wrapping a uWS HttpResponse.
|
|
21
|
+
*
|
|
22
|
+
* Provides methods for status codes, headers, cookies, streaming, file serving,
|
|
23
|
+
* event listeners (abort, close, finish), and SSE.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam TServer - The server type this response belongs to.
|
|
26
|
+
*/
|
|
27
|
+
export default class Response {
|
|
28
|
+
constructor() { }
|
|
29
|
+
/** Active SSE stream, if the client requested it. */
|
|
30
|
+
_sse = null;
|
|
31
|
+
/** Per-response local storage for middleware communication. */
|
|
32
|
+
_locals;
|
|
33
|
+
/** The matched route. */
|
|
34
|
+
route;
|
|
35
|
+
/** Whether the response is in streaming mode. */
|
|
36
|
+
_streaming;
|
|
37
|
+
/** Tracks the furthest middleware index executed to prevent double-next(). */
|
|
38
|
+
_middleware_cursor;
|
|
39
|
+
/** The paired Request instance. */
|
|
40
|
+
_wrapped_request;
|
|
41
|
+
/** The uWS upgrade socket (WebSocket upgrade requests only). */
|
|
42
|
+
_upgrade_socket;
|
|
43
|
+
/** The raw uWS HttpResponse object. */
|
|
44
|
+
_raw_response;
|
|
45
|
+
/** HTTP status code (defaults to 200). */
|
|
46
|
+
_status_code;
|
|
47
|
+
/** Custom status message (overrides the default from STATUS_CODES). */
|
|
48
|
+
_status_message;
|
|
49
|
+
/** Response headers (key → value or key → string[]). */
|
|
50
|
+
_headers;
|
|
51
|
+
/** Set-Cookie headers (name → serialized cookie string). */
|
|
52
|
+
_cookies;
|
|
53
|
+
/** Whether the response has been sent. */
|
|
54
|
+
completed;
|
|
55
|
+
/** Whether headers have been written to the wire. */
|
|
56
|
+
initiated;
|
|
57
|
+
/** If true, the response body is wrapped in uWS `cork()` for batching. */
|
|
58
|
+
_cork;
|
|
59
|
+
/** Has `cork()` already been applied? Prevents double-corking. */
|
|
60
|
+
_corked;
|
|
61
|
+
/** The drain handler registered for backpressure-aware streaming. */
|
|
62
|
+
_drain_handler;
|
|
63
|
+
/** Event listeners (abort, close, finish). */
|
|
64
|
+
_events;
|
|
65
|
+
/**
|
|
66
|
+
* The engine that owns this response.
|
|
67
|
+
*/
|
|
68
|
+
get engine() {
|
|
69
|
+
return this.route?.engine ?? null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a Response instance and registers the uWS `onAborted` callback.
|
|
73
|
+
*
|
|
74
|
+
* The `onAborted` handler marks the response as completed, cleans up the
|
|
75
|
+
* pending request counter, stops body parsing on the paired request, and
|
|
76
|
+
* fires abort/close event listeners.
|
|
77
|
+
*/
|
|
78
|
+
static create(raw_response, route, request, socket) {
|
|
79
|
+
const res = new Response();
|
|
80
|
+
res._raw_response = raw_response;
|
|
81
|
+
res.route = route;
|
|
82
|
+
res._wrapped_request = request;
|
|
83
|
+
res._upgrade_socket = socket || null;
|
|
84
|
+
res._headers = {};
|
|
85
|
+
res._status_code = 200;
|
|
86
|
+
res._middleware_cursor = -1;
|
|
87
|
+
// handle uWS abort (client disconnects before response is sent)
|
|
88
|
+
raw_response.onAborted(() => {
|
|
89
|
+
if (res.completed)
|
|
90
|
+
return;
|
|
91
|
+
res.completed = true;
|
|
92
|
+
res.route?.server.engine._resolve_pending_request();
|
|
93
|
+
//res._wrapped_request.pause()
|
|
94
|
+
if (res._events?.abort) {
|
|
95
|
+
for (let i = 0; i < res._events.abort.length; i++) {
|
|
96
|
+
res._events.abort[i](res._wrapped_request, res);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (res._events?.close) {
|
|
100
|
+
for (let i = 0; i < res._events.close.length; i++) {
|
|
101
|
+
res._events.close[i](res._wrapped_request, res);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return res;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Registers an event listener on the response.
|
|
109
|
+
* Supported events: `"abort"`, `"close"`, `"finish"`.
|
|
110
|
+
*/
|
|
111
|
+
on(event, handler) {
|
|
112
|
+
if (!this._events) {
|
|
113
|
+
this._events = Object.create(null);
|
|
114
|
+
}
|
|
115
|
+
if (!this._events[event]) {
|
|
116
|
+
this._events[event] = [];
|
|
117
|
+
}
|
|
118
|
+
this._events[event].push(handler);
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Registers an event listener that fires only once.
|
|
123
|
+
*/
|
|
124
|
+
once(event, handler) {
|
|
125
|
+
const wrapper = (...args) => {
|
|
126
|
+
this.off(event, wrapper);
|
|
127
|
+
handler(...args);
|
|
128
|
+
};
|
|
129
|
+
return this.on(event, wrapper);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Removes an event listener.
|
|
133
|
+
*/
|
|
134
|
+
off(event, handler) {
|
|
135
|
+
const arr = this._events?.[event];
|
|
136
|
+
if (arr) {
|
|
137
|
+
const idx = arr.indexOf(handler);
|
|
138
|
+
if (idx !== -1) {
|
|
139
|
+
arr.splice(idx, 1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
listenerCount(event) {
|
|
145
|
+
return this._events?.[event]?.length ?? 0;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Tracks the current middleware cursor position.
|
|
149
|
+
*
|
|
150
|
+
* Throws if a middleware tries to advance backwards (i.e. double-next detection).
|
|
151
|
+
*
|
|
152
|
+
* @returns The new cursor position.
|
|
153
|
+
*/
|
|
154
|
+
_track_middleware_cursor(position) {
|
|
155
|
+
if (this._middleware_cursor === -1 ||
|
|
156
|
+
position > this._middleware_cursor) {
|
|
157
|
+
this._middleware_cursor = position;
|
|
158
|
+
return position;
|
|
159
|
+
}
|
|
160
|
+
throw new Error("ERR_DOUBLE_MIDDLEWARE_EXEUCTION_DETECTED: Please ensure you are not calling the next() iterator inside of an ASYNC middleware. You must only call next() ONCE per middleware inside of SYNCHRONOUS middlewares only!");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Wraps a handler in uWS `cork()` for batched writes.
|
|
164
|
+
* All writes inside the handler are sent in a single TCP segment.
|
|
165
|
+
*/
|
|
166
|
+
atomic(handler) {
|
|
167
|
+
if (!this.completed)
|
|
168
|
+
this._raw_response.cork(handler);
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Sets the HTTP status code. Optionally sets a custom status message.
|
|
173
|
+
*/
|
|
174
|
+
status(code, message) {
|
|
175
|
+
this._status_code = code;
|
|
176
|
+
if (message !== undefined)
|
|
177
|
+
this._status_message = message;
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Sets the `Content-Type` header from a file extension or MIME string.
|
|
182
|
+
* Prefix the value with a dot to indicate a file extension (e.g. `".html"`).
|
|
183
|
+
*/
|
|
184
|
+
type(mime_type) {
|
|
185
|
+
if (mime_type.charCodeAt(0) === 46) {
|
|
186
|
+
mime_type = mime_type.slice(1);
|
|
187
|
+
}
|
|
188
|
+
this._headers["content-type"] =
|
|
189
|
+
mime_types.contentType(mime_type) || "text/plain";
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Sets a response header. If the header already exists and `overwrite` is false,
|
|
194
|
+
* the values are accumulated as an array.
|
|
195
|
+
*/
|
|
196
|
+
header(name, value, overwrite) {
|
|
197
|
+
name = name.toLowerCase();
|
|
198
|
+
if (overwrite || this._headers[name] === undefined) {
|
|
199
|
+
this._headers[name] = value;
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
const existing = this._headers[name];
|
|
203
|
+
if (Array.isArray(existing)) {
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
for (let i = 0; i < value.length; i++) {
|
|
206
|
+
existing.push(value[i]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
existing.push(value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
this._headers[name] = [existing, value];
|
|
215
|
+
}
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Sets a `Set-Cookie` header.
|
|
220
|
+
*
|
|
221
|
+
* @param name - Cookie name.
|
|
222
|
+
* @param value - Cookie value. Pass `null` to delete the cookie.
|
|
223
|
+
* @param expiry - Expiry time in milliseconds from now.
|
|
224
|
+
* @param options - Cookie options (secure, sameSite, path, etc.).
|
|
225
|
+
* @param sign_cookie - If true and `options.secret` is set, signs the cookie value.
|
|
226
|
+
*/
|
|
227
|
+
cookie(name, value, expiry, options, sign_cookie = true) {
|
|
228
|
+
if (name && value === null) {
|
|
229
|
+
return this.cookie(name, "", null, { maxAge: 0 });
|
|
230
|
+
}
|
|
231
|
+
options = options
|
|
232
|
+
? Object.assign({}, options)
|
|
233
|
+
: { secure: true, sameSite: "none", path: "/" };
|
|
234
|
+
if (typeof expiry === "number") {
|
|
235
|
+
options.expires = options.expires || new Date(Date.now() + expiry);
|
|
236
|
+
options.maxAge = options.maxAge || (expiry / 1000) | 0;
|
|
237
|
+
}
|
|
238
|
+
if (sign_cookie && typeof options.secret === "string") {
|
|
239
|
+
options.encode = false;
|
|
240
|
+
value = signature.sign(value, options.secret);
|
|
241
|
+
}
|
|
242
|
+
if (this._cookies === null) {
|
|
243
|
+
this._cookies = Object.create(null);
|
|
244
|
+
}
|
|
245
|
+
this._cookies[name] = cookie.stringifySetCookie({
|
|
246
|
+
...options,
|
|
247
|
+
name: name,
|
|
248
|
+
value: value,
|
|
249
|
+
});
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Upgrades the HTTP connection to a WebSocket.
|
|
254
|
+
*
|
|
255
|
+
* @param context - User data to attach to the WebSocket context.
|
|
256
|
+
*/
|
|
257
|
+
upgrade(context) {
|
|
258
|
+
if (this.completed)
|
|
259
|
+
return;
|
|
260
|
+
if (this._upgrade_socket == null) {
|
|
261
|
+
throw new Error("Cannot upgrade a request that does not come from an upgrade handler.");
|
|
262
|
+
}
|
|
263
|
+
// if cork is requested, defer upgrade into a corked callback
|
|
264
|
+
if (this._cork && !this._corked) {
|
|
265
|
+
this._corked = true;
|
|
266
|
+
return this.atomic(() => this.upgrade(context));
|
|
267
|
+
}
|
|
268
|
+
const headers = this._wrapped_request.headers;
|
|
269
|
+
// perform the uWS upgrade with the stored socket handle
|
|
270
|
+
this._raw_response.upgrade({ context }, headers["sec-websocket-key"], headers["sec-websocket-protocol"], headers["sec-websocket-extensions"], this._upgrade_socket);
|
|
271
|
+
this.completed = true;
|
|
272
|
+
this.engine?._resolve_pending_request();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Writes the status line and all accumulated headers to the uWS response.
|
|
276
|
+
*
|
|
277
|
+
* Returns `false` if headers were already initiated (idempotent).
|
|
278
|
+
*/
|
|
279
|
+
_initiate_response() {
|
|
280
|
+
if (this.initiated)
|
|
281
|
+
return false;
|
|
282
|
+
this.initiated = true;
|
|
283
|
+
const raw = this._raw_response;
|
|
284
|
+
if (this._status_message) {
|
|
285
|
+
raw.writeStatus(`${this._status_code} ${this._status_message}`);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
raw.writeStatus(STATUS_CACHE[this._status_code] || `${this._status_code} OK`);
|
|
289
|
+
}
|
|
290
|
+
const headerKeys = Object.keys(this._headers);
|
|
291
|
+
for (let i = 0; i < headerKeys.length; i++) {
|
|
292
|
+
const name = headerKeys[i];
|
|
293
|
+
if (name === "content-length")
|
|
294
|
+
continue;
|
|
295
|
+
const values = this._headers[name];
|
|
296
|
+
if (Array.isArray(values)) {
|
|
297
|
+
for (let j = 0; j < values.length; j++) {
|
|
298
|
+
raw.writeHeader(name, values[j]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
raw.writeHeader(name, values);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (this._cookies) {
|
|
306
|
+
const cookieKeys = Object.keys(this._cookies);
|
|
307
|
+
for (let i = 0; i < cookieKeys.length; i++) {
|
|
308
|
+
raw.writeHeader("set-cookie", this._cookies[cookieKeys[i]]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Registers a drain handler for streaming responses.
|
|
315
|
+
*
|
|
316
|
+
* @param handler - Called when uWS signals the socket is writable.
|
|
317
|
+
* Must return `true` if the chunk was successfully written.
|
|
318
|
+
*/
|
|
319
|
+
drain(handler) {
|
|
320
|
+
const is_first_time = this._drain_handler === null;
|
|
321
|
+
this._drain_handler = handler;
|
|
322
|
+
if (is_first_time) {
|
|
323
|
+
this._raw_response.onWritable((offset) => {
|
|
324
|
+
const output = this._drain_handler(offset);
|
|
325
|
+
if (typeof output !== "boolean") {
|
|
326
|
+
throw new Error("Response.drain(handler) -> handler must return a boolean.");
|
|
327
|
+
}
|
|
328
|
+
return output;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Sends the response body and marks the response as completed.
|
|
334
|
+
*
|
|
335
|
+
* Supports corked (batched) and uncorked paths, streaming mode, and
|
|
336
|
+
* `endWithoutBody` when a custom `content-length` header is set.
|
|
337
|
+
*
|
|
338
|
+
* Fires `finish` and `close` event listeners.
|
|
339
|
+
*/
|
|
340
|
+
send(body, close_connection) {
|
|
341
|
+
if (this.completed)
|
|
342
|
+
return this;
|
|
343
|
+
if (this._cork && !this._corked) {
|
|
344
|
+
this._corked = true;
|
|
345
|
+
this._raw_response.cork(() => {
|
|
346
|
+
this._initiate_response();
|
|
347
|
+
if (body !== undefined || this._streaming) {
|
|
348
|
+
this._raw_response.end(body, close_connection);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const custom_length = this._headers["content-length"];
|
|
352
|
+
if (custom_length) {
|
|
353
|
+
const content_length = typeof custom_length === "string"
|
|
354
|
+
? custom_length
|
|
355
|
+
: custom_length[custom_length.length - 1];
|
|
356
|
+
this._raw_response.endWithoutBody(content_length, close_connection);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
this._raw_response.end(body, close_connection);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
this._initiate_response();
|
|
366
|
+
const raw = this._raw_response;
|
|
367
|
+
if (body !== undefined || this._streaming) {
|
|
368
|
+
raw.end(body, close_connection);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
const custom_length = this._headers["content-length"];
|
|
372
|
+
if (custom_length) {
|
|
373
|
+
const content_length = typeof custom_length === "string"
|
|
374
|
+
? custom_length
|
|
375
|
+
: custom_length[custom_length.length - 1];
|
|
376
|
+
raw.endWithoutBody(content_length, close_connection);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
raw.end(body, close_connection);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!this._streaming && this.listenerCount("finish") > 0) {
|
|
384
|
+
const handlers = this._events.finish;
|
|
385
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
386
|
+
handlers[i](this._wrapped_request, this);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
this.completed = true;
|
|
390
|
+
this.engine?._resolve_pending_request();
|
|
391
|
+
if (this.listenerCount("close") > 0) {
|
|
392
|
+
const handlers = this._events.close;
|
|
393
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
394
|
+
handlers[i](this._wrapped_request, this);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return this;
|
|
398
|
+
}
|
|
399
|
+
_uws_write_chunk(chunk, total_size) {
|
|
400
|
+
if (total_size) {
|
|
401
|
+
return this._raw_response.tryEnd(chunk, total_size);
|
|
402
|
+
}
|
|
403
|
+
return [this._raw_response.write(chunk), false];
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Writes a chunk to the response body.
|
|
407
|
+
* Initiates headers if not yet sent. Returns false under backpressure.
|
|
408
|
+
*/
|
|
409
|
+
write(chunk) {
|
|
410
|
+
if (this._cork && !this._corked) {
|
|
411
|
+
this._corked = true;
|
|
412
|
+
let ok = false;
|
|
413
|
+
this._raw_response.cork(() => {
|
|
414
|
+
this._initiate_response();
|
|
415
|
+
ok = this._raw_response.write(chunk);
|
|
416
|
+
});
|
|
417
|
+
return ok;
|
|
418
|
+
}
|
|
419
|
+
this._initiate_response();
|
|
420
|
+
return this._raw_response.write(chunk);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Streams a single chunk with backpressure support.
|
|
424
|
+
* If the chunk doesn't fit in the uWS send buffer, it registers a drain handler.
|
|
425
|
+
*/
|
|
426
|
+
_stream_chunk(chunk, total_size) {
|
|
427
|
+
if (this.completed)
|
|
428
|
+
return Promise.resolve();
|
|
429
|
+
return new Promise((resolve) => {
|
|
430
|
+
if (this._cork && !this._corked) {
|
|
431
|
+
this._corked = true;
|
|
432
|
+
this._raw_response.cork(() => this._initiate_response());
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
this._initiate_response();
|
|
436
|
+
}
|
|
437
|
+
if (this.completed)
|
|
438
|
+
return resolve();
|
|
439
|
+
const write_offset = this._raw_response.getWriteOffset();
|
|
440
|
+
const [sent] = this._uws_write_chunk(chunk, total_size);
|
|
441
|
+
if (sent)
|
|
442
|
+
return resolve();
|
|
443
|
+
this.drain((offset) => {
|
|
444
|
+
if (this.completed || !total_size) {
|
|
445
|
+
resolve();
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
const remaining = chunk.slice(offset - write_offset);
|
|
449
|
+
const [flushed] = this._uws_write_chunk(remaining, total_size);
|
|
450
|
+
if (flushed)
|
|
451
|
+
resolve();
|
|
452
|
+
return flushed;
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Streams a Readable to the client chunk by chunk.
|
|
458
|
+
*
|
|
459
|
+
* @param readable - A Node.js Readable stream.
|
|
460
|
+
* @param total_size - Total size hint for uWS `tryEnd`.
|
|
461
|
+
*/
|
|
462
|
+
async stream(readable, total_size) {
|
|
463
|
+
if (this.completed)
|
|
464
|
+
return;
|
|
465
|
+
const destroyReadable = () => !readable.destroyed && readable.destroy();
|
|
466
|
+
this.once("close", destroyReadable);
|
|
467
|
+
while (!this.completed &&
|
|
468
|
+
!readable.readableEnded &&
|
|
469
|
+
!readable.destroyed) {
|
|
470
|
+
let chunk = readable.read();
|
|
471
|
+
if (!chunk) {
|
|
472
|
+
await new Promise((resolve) => {
|
|
473
|
+
readable.once("end", resolve);
|
|
474
|
+
readable.once("readable", () => {
|
|
475
|
+
readable.removeListener("end", resolve);
|
|
476
|
+
resolve();
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
chunk = readable.read();
|
|
480
|
+
}
|
|
481
|
+
if (chunk)
|
|
482
|
+
await this._stream_chunk(chunk, total_size);
|
|
483
|
+
}
|
|
484
|
+
if (!this.completed) {
|
|
485
|
+
if (total_size)
|
|
486
|
+
this.engine?._resolve_pending_request();
|
|
487
|
+
else
|
|
488
|
+
this.send();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Immediately closes the response (hard abort).
|
|
493
|
+
*/
|
|
494
|
+
close() {
|
|
495
|
+
if (this.completed)
|
|
496
|
+
return;
|
|
497
|
+
this.completed = true;
|
|
498
|
+
this.engine?._resolve_pending_request();
|
|
499
|
+
//this._wrapped_request.pause()
|
|
500
|
+
this._raw_response.close();
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Sends a 302 redirect response.
|
|
504
|
+
*/
|
|
505
|
+
redirect(url) {
|
|
506
|
+
if (this.completed)
|
|
507
|
+
return false;
|
|
508
|
+
return this.status(302).header("location", url).send();
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Fast-path send used by the `_invokeHandler`.
|
|
512
|
+
* Skips cork/uncork logic and writes directly.
|
|
513
|
+
*/
|
|
514
|
+
_sendFast(body) {
|
|
515
|
+
this._corked = true;
|
|
516
|
+
this._initiate_response();
|
|
517
|
+
this._raw_response.end(body);
|
|
518
|
+
this.completed = true;
|
|
519
|
+
this.engine?._resolve_pending_request();
|
|
520
|
+
if (this._events?.finish) {
|
|
521
|
+
for (let i = 0; i < this._events.finish.length; i++) {
|
|
522
|
+
this._events.finish[i](this._wrapped_request, this);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (this._events?.close) {
|
|
526
|
+
for (let i = 0; i < this._events.close.length; i++) {
|
|
527
|
+
this._events.close[i](this._wrapped_request, this);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Sends a JSON response. Sets `Content-Type: application/json` and
|
|
533
|
+
* serializes the body with `JSON.stringify`.
|
|
534
|
+
*/
|
|
535
|
+
json(body) {
|
|
536
|
+
this._headers["content-type"] = "application/json";
|
|
537
|
+
return this.send(stringify(body));
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Sends an HTML response. Sets `Content-Type: text/html`.
|
|
541
|
+
*/
|
|
542
|
+
html(body) {
|
|
543
|
+
this._headers["content-type"] = "text/html";
|
|
544
|
+
return this.send(body);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Per-response local storage for middleware communication.
|
|
548
|
+
*/
|
|
549
|
+
get locals() {
|
|
550
|
+
if (!this._locals)
|
|
551
|
+
this._locals = Object.create(null);
|
|
552
|
+
return this._locals;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* The raw uWS HttpResponse.
|
|
556
|
+
*/
|
|
557
|
+
get raw() {
|
|
558
|
+
return this._raw_response;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Whether the response has been aborted by the client.
|
|
562
|
+
*/
|
|
563
|
+
get aborted() {
|
|
564
|
+
return this.completed;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* The uWS upgrade socket handle (WebSocket upgrade requests only).
|
|
568
|
+
*/
|
|
569
|
+
get upgrade_socket() {
|
|
570
|
+
return this._upgrade_socket;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Accessor for Server-Sent Events.
|
|
574
|
+
*
|
|
575
|
+
* Only available on GET requests. Creates a lazy SSEventStream on first access.
|
|
576
|
+
*/
|
|
577
|
+
get sse() {
|
|
578
|
+
if (this._wrapped_request.method === "GET") {
|
|
579
|
+
if (this._sse === null) {
|
|
580
|
+
this._sse = new SSEventStream();
|
|
581
|
+
this._sse._response = this;
|
|
582
|
+
}
|
|
583
|
+
return this._sse;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* The current uWS write offset. Returns `-1` if the response is completed.
|
|
588
|
+
*/
|
|
589
|
+
get write_offset() {
|
|
590
|
+
return this.completed ? -1 : this._raw_response.getWriteOffset();
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* The HTTP status code.
|
|
594
|
+
*/
|
|
595
|
+
get statusCode() {
|
|
596
|
+
return this._status_code;
|
|
597
|
+
}
|
|
598
|
+
set statusCode(value) {
|
|
599
|
+
this._status_code = value;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* The HTTP status message.
|
|
603
|
+
*/
|
|
604
|
+
get statusMessage() {
|
|
605
|
+
return this._status_message;
|
|
606
|
+
}
|
|
607
|
+
set statusMessage(value) {
|
|
608
|
+
this._status_message = value;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Whether response headers have been sent.
|
|
612
|
+
*/
|
|
613
|
+
get headersSent() {
|
|
614
|
+
return this.initiated;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Appends values to a header (alias for `header()`).
|
|
618
|
+
*/
|
|
619
|
+
append(name, values) {
|
|
620
|
+
return this.header(name, values);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Sets a header value (alias for `header()` with overwrite behaviour).
|
|
624
|
+
*/
|
|
625
|
+
setHeader(name, values) {
|
|
626
|
+
return this.header(name, values);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Writes multiple headers at once from an object.
|
|
630
|
+
*/
|
|
631
|
+
writeHeaders(headers) {
|
|
632
|
+
for (const key in headers)
|
|
633
|
+
this.header(key, headers[key]);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Alias for `writeHeaders()`.
|
|
637
|
+
*/
|
|
638
|
+
setHeaders(headers) {
|
|
639
|
+
this.writeHeaders(headers);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Writes multiple values for a single header.
|
|
643
|
+
*/
|
|
644
|
+
writeHeaderValues(name, values) {
|
|
645
|
+
for (let i = 0; i < values.length; i++)
|
|
646
|
+
this.header(name, values[i]);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Returns the value(s) of a header.
|
|
650
|
+
*/
|
|
651
|
+
getHeader(name) {
|
|
652
|
+
return this._headers[name];
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Removes a header.
|
|
656
|
+
*/
|
|
657
|
+
removeHeader(name) {
|
|
658
|
+
delete this._headers[name];
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Sets a cookie (shorthand for `cookie()`).
|
|
662
|
+
*/
|
|
663
|
+
setCookie(name, value, options) {
|
|
664
|
+
return this.cookie(name, value, null, options);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Checks if a cookie has been queued.
|
|
668
|
+
*/
|
|
669
|
+
hasCookie(name) {
|
|
670
|
+
return this._cookies !== null && this._cookies[name] !== undefined;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Removes a cookie by setting it to null.
|
|
674
|
+
*/
|
|
675
|
+
removeCookie(name) {
|
|
676
|
+
return this.cookie(name, null);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Alias for `removeCookie()`.
|
|
680
|
+
*/
|
|
681
|
+
clearCookie(name) {
|
|
682
|
+
return this.cookie(name, null);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Alias for `send()`.
|
|
686
|
+
*/
|
|
687
|
+
end(data) {
|
|
688
|
+
return this.send(data);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Returns the header value(s) for a given name.
|
|
692
|
+
*/
|
|
693
|
+
get(name) {
|
|
694
|
+
const values = this._headers[name];
|
|
695
|
+
if (values) {
|
|
696
|
+
return typeof values === "string" ? values : values[0];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Sets the `Link` header from an object mapping rel → URL.
|
|
701
|
+
*/
|
|
702
|
+
links(links) {
|
|
703
|
+
const chunks = [];
|
|
704
|
+
for (const rel in links) {
|
|
705
|
+
chunks.push(`<${links[rel]}>; rel="${rel}"`);
|
|
706
|
+
}
|
|
707
|
+
this.header("link", chunks.join(", "));
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Sets the `Location` header, used for redirects.
|
|
711
|
+
*/
|
|
712
|
+
location(path) {
|
|
713
|
+
this._headers["location"] = path;
|
|
714
|
+
return this;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Sends a status code response.
|
|
718
|
+
*/
|
|
719
|
+
sendStatus(status_code) {
|
|
720
|
+
this._status_code = status_code;
|
|
721
|
+
return this.send();
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Sets headers using either `(key, value)` or `({ key: value })`.
|
|
725
|
+
*/
|
|
726
|
+
set(field, value) {
|
|
727
|
+
if (typeof field === "object") {
|
|
728
|
+
for (const key in field)
|
|
729
|
+
this.header(key, field[key]);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
this.header(field, value);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Sets the `Vary` header.
|
|
737
|
+
*/
|
|
738
|
+
vary(name) {
|
|
739
|
+
this._headers["vary"] = name;
|
|
740
|
+
return this;
|
|
741
|
+
}
|
|
742
|
+
}
|