@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/engine.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Neo engine - the uWebSockets.js engine adaptor for Linebridge.
|
|
3
|
+
*
|
|
4
|
+
* This engine creates a uWS app (plain or SSL), optionally attaches a WebSocket
|
|
5
|
+
* server via RTEngine, and manages the HTTP request/response lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* It is the default and only built-in engine; other engines can be added by
|
|
8
|
+
* implementing the EngineAdaptor interface and registering them in `engines/index.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import fs_promises from "node:fs/promises";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import toBoolean from "linebridge/utils/toBoolean";
|
|
14
|
+
import RTEngine from "linebridge/classes/RtEngine/index";
|
|
15
|
+
import { EngineAdaptor } from "linebridge/classes/EngineAdaptor/index";
|
|
16
|
+
import { Route } from "linebridge/classes/Route/index";
|
|
17
|
+
import WebsocketPolyfill from "./ws/Websocket.js";
|
|
18
|
+
import route_register from "./route_register.js";
|
|
19
|
+
import middleware_register from "./register_middleware.js";
|
|
20
|
+
import listen from "./listen.js";
|
|
21
|
+
import close from "./close.js";
|
|
22
|
+
import on_request from "./on_request.js";
|
|
23
|
+
import request_iterator from "./request_iterator.js";
|
|
24
|
+
let uWebsockets;
|
|
25
|
+
try {
|
|
26
|
+
// Try loading from the root (when executing from build/out/lib/index.js)
|
|
27
|
+
uWebsockets = createRequire(import.meta.url)("../uws-wrapper.node");
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
try {
|
|
31
|
+
// Try loading from build/Release (when executing from src/index.ts in monorepo)
|
|
32
|
+
uWebsockets = createRequire(import.meta.url)("../build/Release/uws-wrapper.node");
|
|
33
|
+
}
|
|
34
|
+
catch (e2) {
|
|
35
|
+
// Try loading from build/out (when executing from src/index.ts in monorepo if already packaged)
|
|
36
|
+
uWebsockets = createRequire(import.meta.url)("../build/out/uws-wrapper.node");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The Neo engine is the uWebSockets.js adaptor.
|
|
41
|
+
*
|
|
42
|
+
* It handles app construction (plain or SSL), WebSocket setup, route registration,
|
|
43
|
+
* middleware registration, and the request/response pipeline.
|
|
44
|
+
*/
|
|
45
|
+
export class NeoEngine extends EngineAdaptor {
|
|
46
|
+
/** Number of in-flight HTTP requests. Tracked so the server knows when it is idle. */
|
|
47
|
+
pending_requests_count = 0;
|
|
48
|
+
/** Callback fired when the pending request count reaches zero. */
|
|
49
|
+
pending_requests_zero_handler = null;
|
|
50
|
+
/** Base headers to include in all responses. */
|
|
51
|
+
base_headers = {};
|
|
52
|
+
/** Set of registered {method, path} objects to avoid duplicate registrations. */
|
|
53
|
+
registers = new Set();
|
|
54
|
+
/** Global middleware stack applied to every request before route handlers run. */
|
|
55
|
+
middlewares = [];
|
|
56
|
+
/** Listening port, defaults to 3000. */
|
|
57
|
+
port = 3000;
|
|
58
|
+
/** Listening host, defaults to "0.0.0.0". */
|
|
59
|
+
host = "0.0.0.0";
|
|
60
|
+
/** Engine-level options merged with uWS constructor options. */
|
|
61
|
+
options = {
|
|
62
|
+
is_ssl: false,
|
|
63
|
+
auto_close: true,
|
|
64
|
+
trust_proxy: false,
|
|
65
|
+
max_body_buffer: 16 * 1024,
|
|
66
|
+
max_body_length: 9 * 1024 * 1024,
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Initializes the engine: creates the uWS app, configures SSL if needed,
|
|
70
|
+
* attaches WebSocket support, and registers the default catch-all route.
|
|
71
|
+
*/
|
|
72
|
+
initialize = async () => {
|
|
73
|
+
// raise uWS limits to handle apps with many headers (e.g. large cookie payloads)
|
|
74
|
+
//@ts-ignore
|
|
75
|
+
process.env["UWS_HTTP_MAX_HEADERS_COUNT"] = 512;
|
|
76
|
+
//@ts-ignore
|
|
77
|
+
process.env["UWS_HTTP_MAX_HEADERS_SIZE"] = 650000;
|
|
78
|
+
// unless explicitly kept, hide the internal uWS Server header
|
|
79
|
+
if (!process.env["KEEP_UWS_HEADER"]) {
|
|
80
|
+
try {
|
|
81
|
+
//@ts-ignore
|
|
82
|
+
uWebsockets._cfg("999999990007");
|
|
83
|
+
}
|
|
84
|
+
catch (_e) {
|
|
85
|
+
// womp womp
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// set port and host from server params, falling back to defaults
|
|
89
|
+
this.port = this.server.params.listenPort ?? this.port;
|
|
90
|
+
this.host = this.server.params.listenIp ?? this.host;
|
|
91
|
+
// validate and enable SSL when key/cert paths are provided
|
|
92
|
+
if (this.server.ssl) {
|
|
93
|
+
if (this.server.ssl.key && this.server.ssl.cert) {
|
|
94
|
+
try {
|
|
95
|
+
await Promise.all([
|
|
96
|
+
fs_promises.access(this.server.ssl.key),
|
|
97
|
+
fs_promises.access(this.server.ssl.cert),
|
|
98
|
+
]);
|
|
99
|
+
this.options.is_ssl = true;
|
|
100
|
+
this.options.key_file_name = this.server.ssl.key;
|
|
101
|
+
this.options.cert_file_name = this.server.ssl.cert;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
throw new Error("Cannot access SSL key or cert: " + err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// initialize the uWebsockets app depending on SSL mode
|
|
109
|
+
if (this.options.is_ssl) {
|
|
110
|
+
this.uws = uWebsockets.SSLApp({
|
|
111
|
+
...this.options,
|
|
112
|
+
key_file_name: this.server.ssl.key,
|
|
113
|
+
cert_file_name: this.server.ssl.cert,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.uws = uWebsockets.App(this.options);
|
|
118
|
+
}
|
|
119
|
+
// if socket mode is enabled, use a unix socket path
|
|
120
|
+
if (toBoolean(process.env.LB_SOCKET_MODE)) {
|
|
121
|
+
this.socket_path = `/tmp/lb_node_${this.server.params.refName}.sock`;
|
|
122
|
+
// if the socket path already exists, remove it
|
|
123
|
+
if (fs.existsSync(this.socket_path)) {
|
|
124
|
+
fs.unlinkSync(this.socket_path);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// if websockets are enabled, set up the websocket server
|
|
128
|
+
if (typeof this.server.params.websockets === "object" &&
|
|
129
|
+
this.server.params.websockets.enabled === true) {
|
|
130
|
+
const websocket_attached_path = this.server.params.websockets.path ??
|
|
131
|
+
`/${this.server.params.refName}`;
|
|
132
|
+
this.ws = new RTEngine(this.server, {
|
|
133
|
+
path: websocket_attached_path,
|
|
134
|
+
onUpgrade: this.server.handleWsUpgrade,
|
|
135
|
+
onConnection: this.server.handleWsConnection,
|
|
136
|
+
onDisconnect: this.server.handleWsDisconnect,
|
|
137
|
+
});
|
|
138
|
+
// attach the engine adaptor to RTEngine
|
|
139
|
+
this.ws.engine = this;
|
|
140
|
+
// attach to uWebsockets with adaptors that bridge uWS events into RTEngine
|
|
141
|
+
this.uws.ws(websocket_attached_path, {
|
|
142
|
+
// uWS passes (res, req, context) but RTEngine expects (req, res)
|
|
143
|
+
upgrade: (res, req, _context) => this.ws.handleUpgrade(req, res),
|
|
144
|
+
// wrap raw uWS websocket with the polyfill so RTEngine gets EventEmitter + helpers
|
|
145
|
+
open: (ws) => {
|
|
146
|
+
const poly = new WebsocketPolyfill(ws);
|
|
147
|
+
ws.poly = poly;
|
|
148
|
+
//@ts-ignore polyfill context is Object vs RtEngineSocket's typed shape, but runtime is correct
|
|
149
|
+
this.ws.handleConnection(poly);
|
|
150
|
+
},
|
|
151
|
+
// forward uWS message events to the polyfill so RTEngine listeners fire
|
|
152
|
+
message: (ws, message, isBinary) => {
|
|
153
|
+
// uWS passes raw ArrayBuffer, but RTEngine expects a string for JSON.parse
|
|
154
|
+
const parsed = isBinary
|
|
155
|
+
? message
|
|
156
|
+
: Buffer.from(message).toString();
|
|
157
|
+
ws.poly?.emit("message", parsed, isBinary);
|
|
158
|
+
},
|
|
159
|
+
// forward uWS close events to the polyfill so RTEngine listeners fire
|
|
160
|
+
close: (ws, code, message) => {
|
|
161
|
+
ws.poly?._destroy();
|
|
162
|
+
ws.poly?.emit("close", code, message);
|
|
163
|
+
delete ws.poly;
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
// @ts-ignore
|
|
167
|
+
global.websockets = this.ws;
|
|
168
|
+
}
|
|
169
|
+
// create the default route
|
|
170
|
+
const defaultRoute = new Route();
|
|
171
|
+
defaultRoute.method = "any";
|
|
172
|
+
defaultRoute.path = "/*";
|
|
173
|
+
defaultRoute.fn = this._defaultResponse;
|
|
174
|
+
this.register(defaultRoute);
|
|
175
|
+
};
|
|
176
|
+
/** Starts the server. Bound to the engine instance. */
|
|
177
|
+
listen = listen.bind(this);
|
|
178
|
+
/** Gracefully shuts down the server. Bound to the engine instance. */
|
|
179
|
+
close = close.bind(this);
|
|
180
|
+
/** Registers a route (HTTP method + path + handler). */
|
|
181
|
+
register = route_register.bind(this);
|
|
182
|
+
/** Registers a global middleware that runs before every route handler. */
|
|
183
|
+
register_middleware = middleware_register.bind(this);
|
|
184
|
+
/**
|
|
185
|
+
* Publish a message to a topic in MQTT syntax to all WebSocket connections on this Server instance.
|
|
186
|
+
* You cannot publish using wildcards, only fully specified topics.
|
|
187
|
+
*/
|
|
188
|
+
publish(topic, message, is_binary, compress) {
|
|
189
|
+
return this.uws?.publish(topic, message, is_binary, compress);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Returns the number of subscribers to a topic across all WebSocket connections on this Server instance.
|
|
193
|
+
*/
|
|
194
|
+
num_of_subscribers(topic) {
|
|
195
|
+
return this.uws?.numSubscribers(topic);
|
|
196
|
+
}
|
|
197
|
+
/** Incoming HTTP request handler. Bound to the engine instance. */
|
|
198
|
+
on_request = on_request.bind(this);
|
|
199
|
+
/** Middleware/route iteration loop. Bound to the engine instance. */
|
|
200
|
+
request_iterator = request_iterator.bind(this);
|
|
201
|
+
/**
|
|
202
|
+
* Decrements the pending request counter and fires the zero-handler
|
|
203
|
+
* when no requests remain in flight.
|
|
204
|
+
*/
|
|
205
|
+
_resolve_pending_request() {
|
|
206
|
+
if (this.pending_requests_count < 1)
|
|
207
|
+
return;
|
|
208
|
+
this.pending_requests_count--;
|
|
209
|
+
if (this.pending_requests_count === 0 &&
|
|
210
|
+
this.pending_requests_zero_handler) {
|
|
211
|
+
this.pending_requests_zero_handler();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Default handler for unmatched routes. Responds with a 404 JSON body.
|
|
216
|
+
*/
|
|
217
|
+
_defaultResponse(req, res) {
|
|
218
|
+
res.status(404).json({ error: "Not found" });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export default NeoEngine;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NeoEngine } from "./engine.js";
|
|
2
|
+
import type Request from "./request.js";
|
|
3
|
+
import type Response from "./response.js";
|
|
4
|
+
declare module "linebridge/engines" {
|
|
5
|
+
interface EnginesRegistry {
|
|
6
|
+
neo: typeof NeoEngine;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
declare module "linebridge/types" {
|
|
10
|
+
interface EnginesRequests<T = any> {
|
|
11
|
+
neo: Request<T>;
|
|
12
|
+
}
|
|
13
|
+
interface EnginesResponses<T = any> {
|
|
14
|
+
neo: Response<T>;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/listen.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Binds the uWS app to a host:port or unix socket and returns a promise
|
|
3
|
+
* that resolves once the server is listening.
|
|
4
|
+
*
|
|
5
|
+
* Optionally registers process-exit handlers so the server auto-closes on termination.
|
|
6
|
+
*/
|
|
7
|
+
import type Engine from "./engine.js";
|
|
8
|
+
/**
|
|
9
|
+
* Starts the engine's uWS app on the configured host/port or unix socket.
|
|
10
|
+
*
|
|
11
|
+
* @returns A promise that resolves when the server is listening, or rejects with an error.
|
|
12
|
+
*/
|
|
13
|
+
export default function (this: Engine): Promise<void>;
|
package/lib/listen.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Binds the uWS app to a host:port or unix socket and returns a promise
|
|
3
|
+
* that resolves once the server is listening.
|
|
4
|
+
*
|
|
5
|
+
* Optionally registers process-exit handlers so the server auto-closes on termination.
|
|
6
|
+
*/
|
|
7
|
+
/** OS signals and events that trigger an automatic server close when auto_close is enabled. */
|
|
8
|
+
const EXIT_EVENTS = ["exit", "SIGINT", "SIGUSR1", "SIGUSR2", "SIGTERM"];
|
|
9
|
+
/**
|
|
10
|
+
* Starts the engine's uWS app on the configured host/port or unix socket.
|
|
11
|
+
*
|
|
12
|
+
* @returns A promise that resolves when the server is listening, or rejects with an error.
|
|
13
|
+
*/
|
|
14
|
+
export default async function () {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (!this.uws) {
|
|
17
|
+
return reject(new Error("Engine is not initialized"));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Callback invoked by uWS once the server is bound (or fails to bind).
|
|
21
|
+
*/
|
|
22
|
+
const on_listen_socket = (listen_socket) => {
|
|
23
|
+
if (listen_socket) {
|
|
24
|
+
// store the socket handle so we can close it later
|
|
25
|
+
this.listen_socket = listen_socket;
|
|
26
|
+
// optionally register process-exit listeners for graceful shutdown
|
|
27
|
+
if (this.options.auto_close) {
|
|
28
|
+
EXIT_EVENTS.forEach((type) => process.once(type, () => this.close()));
|
|
29
|
+
}
|
|
30
|
+
resolve();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
reject("Server.listen(): No Socket Received From uWebsockets.js likely due to an invalid host or busy port.");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
// use a unix socket when socket_path is set, otherwise bind to tcp
|
|
37
|
+
if (this.socket_path) {
|
|
38
|
+
this.uws.listen_unix(on_listen_socket, this.socket_path);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
this.uws.listen(this.host, this.port, on_listen_socket);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The main HTTP request entry point called by uWS when a request matches a route.
|
|
3
|
+
*
|
|
4
|
+
* It creates Request and Response wrappers, runs the middleware + handler pipeline,
|
|
5
|
+
* and includes a "fast path" optimisation for routes with zero or one middleware.
|
|
6
|
+
*/
|
|
7
|
+
import type { us_socket_context_t, HttpRequest, HttpResponse } from "uWebSockets.js";
|
|
8
|
+
import type { Route } from "linebridge/classes/Route/index";
|
|
9
|
+
import type Engine from "./engine.js";
|
|
10
|
+
/**
|
|
11
|
+
* Entry point for every incoming HTTP request.
|
|
12
|
+
*
|
|
13
|
+
* Wraps the raw uWS request/response objects into framework-level Request/Response
|
|
14
|
+
* instances, then dispatches through middleware and the route handler.
|
|
15
|
+
*
|
|
16
|
+
* @param native_req - Raw uWS HttpRequest.
|
|
17
|
+
* @param native_res - Raw uWS HttpResponse.
|
|
18
|
+
* @param route - The matched route object.
|
|
19
|
+
* @param socket - Upgrade socket context (only set for WebSocket upgrade requests).
|
|
20
|
+
*/
|
|
21
|
+
export default function (this: Engine, native_req: HttpRequest, native_res: HttpResponse, route: Route<any>, socket?: us_socket_context_t | null): any;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The main HTTP request entry point called by uWS when a request matches a route.
|
|
3
|
+
*
|
|
4
|
+
* It creates Request and Response wrappers, runs the middleware + handler pipeline,
|
|
5
|
+
* and includes a "fast path" optimisation for routes with zero or one middleware.
|
|
6
|
+
*/
|
|
7
|
+
import Request from "./request.js";
|
|
8
|
+
import Response from "./response.js";
|
|
9
|
+
/** HTTP methods that never carry a request body. */
|
|
10
|
+
const BODYLESS_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
|
11
|
+
/** No-op next() placeholder used when there is no real middleware chain. */
|
|
12
|
+
const EMPTY_NEXT = () => { };
|
|
13
|
+
/**
|
|
14
|
+
* Entry point for every incoming HTTP request.
|
|
15
|
+
*
|
|
16
|
+
* Wraps the raw uWS request/response objects into framework-level Request/Response
|
|
17
|
+
* instances, then dispatches through middleware and the route handler.
|
|
18
|
+
*
|
|
19
|
+
* @param native_req - Raw uWS HttpRequest.
|
|
20
|
+
* @param native_res - Raw uWS HttpResponse.
|
|
21
|
+
* @param route - The matched route object.
|
|
22
|
+
* @param socket - Upgrade socket context (only set for WebSocket upgrade requests).
|
|
23
|
+
*/
|
|
24
|
+
export default function (native_req, native_res, route, socket) {
|
|
25
|
+
try {
|
|
26
|
+
const request = Request.create(route, native_req, native_res);
|
|
27
|
+
const response = Response.create(native_res, route, request, socket);
|
|
28
|
+
// if a zero-handler is pending (server is draining), reject new requests
|
|
29
|
+
if (this.pending_requests_zero_handler)
|
|
30
|
+
return response.close();
|
|
31
|
+
if (!route.handler) {
|
|
32
|
+
console.warn(`Route [${route.path}] is registered, but does not have a valid handler.`);
|
|
33
|
+
this.defaultResponse(request, response);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
this.pending_requests_count++;
|
|
37
|
+
// Start body parsing immediately if needed
|
|
38
|
+
if (!BODYLESS_METHODS.has(request.method)) {
|
|
39
|
+
request._body_parser_run(response, this.options.max_body_length);
|
|
40
|
+
}
|
|
41
|
+
// fast path: no middleware or at most one global middleware
|
|
42
|
+
if (this.middlewares.length <= 1 && route.middlewares.length === 0) {
|
|
43
|
+
return _fastPath.call(this, request, response, route);
|
|
44
|
+
}
|
|
45
|
+
// merge global and route-level middlewares
|
|
46
|
+
const allMiddlewares = route.middlewares.length > 0
|
|
47
|
+
? [...this.middlewares, ...route.middlewares]
|
|
48
|
+
: this.middlewares;
|
|
49
|
+
return this.request_iterator(request, response, route, allMiddlewares);
|
|
50
|
+
}
|
|
51
|
+
catch (exception) {
|
|
52
|
+
console.error("Internal fatal error:", exception);
|
|
53
|
+
// try to respond with a 500 even in catastrophic failure
|
|
54
|
+
native_res.writeStatus("500");
|
|
55
|
+
native_res.end(JSON.stringify({ fatal: true, error: exception.message }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Optimised path for routes that have zero or one global middleware.
|
|
60
|
+
* Skips the full iterator overhead.
|
|
61
|
+
*/
|
|
62
|
+
function _fastPath(request, response, route) {
|
|
63
|
+
const runMiddlewares = () => {
|
|
64
|
+
if (this.middlewares.length === 1) {
|
|
65
|
+
const mw = this.middlewares[0];
|
|
66
|
+
const mwResult = mw.fn(request, response, EMPTY_NEXT, mw.ctx ?? {});
|
|
67
|
+
// wait for async middleware to finish before calling the handler
|
|
68
|
+
if (mwResult instanceof Promise) {
|
|
69
|
+
return mwResult.then(() => {
|
|
70
|
+
if (response.completed)
|
|
71
|
+
return;
|
|
72
|
+
return _executeHandler.call(this, request, response, route);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (response.completed)
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
return _executeHandler.call(this, request, response, route);
|
|
79
|
+
};
|
|
80
|
+
if (!BODYLESS_METHODS.has(request.method)) {
|
|
81
|
+
return request.parseBody().then(() => {
|
|
82
|
+
if (response.completed)
|
|
83
|
+
return;
|
|
84
|
+
return runMiddlewares();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return runMiddlewares();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parses the request body (if needed) then invokes the route handler
|
|
91
|
+
* through the standard Handler.execute path, which uses res.send()
|
|
92
|
+
* and properly corks writes to uWS.
|
|
93
|
+
*/
|
|
94
|
+
function _executeHandler(request, response, route) {
|
|
95
|
+
response._cork = true;
|
|
96
|
+
return route.handler.execute(request, response);
|
|
97
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import NeoEngine from "./engine.js";
|
|
2
|
+
import type NeoRequest from "./request.js";
|
|
3
|
+
import type NeoResponse from "./response.js";
|
|
4
|
+
export * from "./engine.js";
|
|
5
|
+
declare module "linebridge/engines" {
|
|
6
|
+
interface EnginesRegistry {
|
|
7
|
+
neo: typeof NeoEngine;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
declare module "linebridge/types" {
|
|
11
|
+
interface EnginesRequests<T> {
|
|
12
|
+
neo: NeoRequest<T>;
|
|
13
|
+
}
|
|
14
|
+
interface EnginesResponses<T> {
|
|
15
|
+
neo: NeoResponse<T>;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/lib/register.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Registers a global middleware that runs before every route handler.
|
|
3
|
+
*
|
|
4
|
+
* Middlewares can be passed as a plain async function or as a Handler wrapper.
|
|
5
|
+
* If a plain function is passed, it is automatically wrapped in a Handler.
|
|
6
|
+
*/
|
|
7
|
+
import type Engine from "./engine.js";
|
|
8
|
+
/**
|
|
9
|
+
* Adds a middleware to the engine's global middleware stack.
|
|
10
|
+
*
|
|
11
|
+
* @param middleware - Either a raw `(req, res, next) => void` function
|
|
12
|
+
* or a pre-built Handler instance.
|
|
13
|
+
*/
|
|
14
|
+
export default function (this: Engine, middleware: any): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Registers a global middleware that runs before every route handler.
|
|
3
|
+
*
|
|
4
|
+
* Middlewares can be passed as a plain async function or as a Handler wrapper.
|
|
5
|
+
* If a plain function is passed, it is automatically wrapped in a Handler.
|
|
6
|
+
*/
|
|
7
|
+
import { Handler, HandlerKind } from "linebridge/classes/Handler/index";
|
|
8
|
+
/**
|
|
9
|
+
* Adds a middleware to the engine's global middleware stack.
|
|
10
|
+
*
|
|
11
|
+
* @param middleware - Either a raw `(req, res, next) => void` function
|
|
12
|
+
* or a pre-built Handler instance.
|
|
13
|
+
*/
|
|
14
|
+
export default function (middleware) {
|
|
15
|
+
let middlewareCtx;
|
|
16
|
+
if (middleware &&
|
|
17
|
+
typeof middleware === "object" &&
|
|
18
|
+
!(middleware instanceof Handler) &&
|
|
19
|
+
typeof middleware.fn === "function") {
|
|
20
|
+
if (Array.isArray(middleware.useContexts) &&
|
|
21
|
+
middleware.useContexts.length > 0) {
|
|
22
|
+
middlewareCtx = {};
|
|
23
|
+
const allContexts = Object.assign({}, this.server.contexts, this.server.base_contexts);
|
|
24
|
+
for (const ctxKey of middleware.useContexts) {
|
|
25
|
+
if (ctxKey in allContexts) {
|
|
26
|
+
middlewareCtx[ctxKey] = allContexts[ctxKey];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
middleware = middleware.fn;
|
|
31
|
+
}
|
|
32
|
+
// auto-wrap plain functions so they work with the Handler lifecycle
|
|
33
|
+
if (!(middleware instanceof Handler)) {
|
|
34
|
+
middleware = new Handler({
|
|
35
|
+
kind: HandlerKind.middleware,
|
|
36
|
+
engine: this,
|
|
37
|
+
fn: middleware,
|
|
38
|
+
ctx: middlewareCtx,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
this.middlewares.push(middleware);
|
|
42
|
+
}
|
package/lib/request.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Full HTTP request wrapper for the Neo engine.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a raw uWS HttpRequest and provides high-level accessors for headers,
|
|
5
|
+
* body parsing (JSON, text, urlencoded, buffer), cookies, IP resolution,
|
|
6
|
+
* and path/query parameters.
|
|
7
|
+
*/
|
|
8
|
+
import type { HttpRequest, HttpResponse } from "uWebSockets.js";
|
|
9
|
+
import type { Request as BaseHttpRequest } from "linebridge/classes/Handler/http";
|
|
10
|
+
import type { EngineAdaptor } from "linebridge/classes/EngineAdaptor/index";
|
|
11
|
+
import type { Route } from "linebridge/classes/Route/index";
|
|
12
|
+
import type { Server } from "linebridge/server";
|
|
13
|
+
import type Response from "./response.js";
|
|
14
|
+
/**
|
|
15
|
+
* HTTP Request class wrapping a uWS HttpRequest.
|
|
16
|
+
*
|
|
17
|
+
* Provides parsed access to the request path, query string, headers, cookies,
|
|
18
|
+
* and body. Body parsing is lazy and supports JSON, text, urlencoded, and buffer.
|
|
19
|
+
*
|
|
20
|
+
* @typeParam TServer - The server type this request belongs to.
|
|
21
|
+
*/
|
|
22
|
+
export default class Request<TServer = Server<any>> implements BaseHttpRequest {
|
|
23
|
+
constructor();
|
|
24
|
+
/** Per-request local storage for middleware communication. */
|
|
25
|
+
_locals: any;
|
|
26
|
+
/** Whether the response stream is paused. */
|
|
27
|
+
_paused: boolean;
|
|
28
|
+
/** HTTP method string, normalized (e.g. "GET", "DELETE"). */
|
|
29
|
+
_method: string;
|
|
30
|
+
/** Full URL including query string. */
|
|
31
|
+
_url: string;
|
|
32
|
+
/** URL path portion without query string. */
|
|
33
|
+
_path: string;
|
|
34
|
+
/** Raw query string (without leading `?`). */
|
|
35
|
+
_query_str: string;
|
|
36
|
+
/** Resolved remote IP address. */
|
|
37
|
+
_remote_ip: string;
|
|
38
|
+
/** Resolved proxy IP address. */
|
|
39
|
+
_remote_proxy_ip: string;
|
|
40
|
+
/** Parsed cookies (lazy). */
|
|
41
|
+
_cookies: any;
|
|
42
|
+
/** URL path parameters extracted by uWS (e.g. `/user/:id`). */
|
|
43
|
+
_path_parameters: any;
|
|
44
|
+
/** Parsed query string parameters (lazy). */
|
|
45
|
+
_query_parameters: any;
|
|
46
|
+
/** The raw uWS HttpRequest object. */
|
|
47
|
+
_raw_request: HttpRequest;
|
|
48
|
+
/** The raw uWS HttpResponse object (needed for `onData` and `getRemoteAddress`). */
|
|
49
|
+
_raw_response: HttpResponse;
|
|
50
|
+
/** Request headers extracted synchronously. */
|
|
51
|
+
_headers: Record<string, string>;
|
|
52
|
+
/** Whether all body chunks have been received. */
|
|
53
|
+
_received: boolean;
|
|
54
|
+
/** Total bytes received so far for the body. */
|
|
55
|
+
_body_received_bytes: number;
|
|
56
|
+
/** Has the onData listener been registered for body parsing? */
|
|
57
|
+
_body_parser_on_data_registered: boolean;
|
|
58
|
+
/** Parsed body (populated lazily by `.json()`, `.text()`, etc.). */
|
|
59
|
+
_body: any;
|
|
60
|
+
/** The type that `.parseBody()` resolved to. */
|
|
61
|
+
_body_type: string | null;
|
|
62
|
+
/** Raw body buffer, populated after body is fully received. */
|
|
63
|
+
_body_raw: Buffer | null;
|
|
64
|
+
/** Promise that resolves once all body data is received and parsed. */
|
|
65
|
+
_body_promise: Promise<any> | null;
|
|
66
|
+
/** Callback invoked when body data is fully received. */
|
|
67
|
+
_onDone: (() => void) | null;
|
|
68
|
+
/** Context object for sharing data between middlewares and the handler. */
|
|
69
|
+
ctx: Record<string, any>;
|
|
70
|
+
/** The matched route for this request. */
|
|
71
|
+
route: Route<TServer> | null;
|
|
72
|
+
/**
|
|
73
|
+
* Creates a Request instance from raw uWS objects.
|
|
74
|
+
*/
|
|
75
|
+
static create<TServer extends Server<any>>(route: Route<TServer>, raw_request: HttpRequest, raw_response: HttpResponse): Request<TServer>;
|
|
76
|
+
/**
|
|
77
|
+
* The engine that owns this request.
|
|
78
|
+
*/
|
|
79
|
+
get engine(): EngineAdaptor | null;
|
|
80
|
+
/**
|
|
81
|
+
* The raw uWS HttpRequest.
|
|
82
|
+
*/
|
|
83
|
+
get raw(): HttpRequest;
|
|
84
|
+
/**
|
|
85
|
+
* Lazily-parsed headers as a plain object.
|
|
86
|
+
* Header names are lowercased by uWS.
|
|
87
|
+
*/
|
|
88
|
+
get headers(): Record<string, string>;
|
|
89
|
+
/**
|
|
90
|
+
* Pauses the underlying response stream.
|
|
91
|
+
* Used during body parsing to apply backpressure.
|
|
92
|
+
*/
|
|
93
|
+
pause(): this;
|
|
94
|
+
/**
|
|
95
|
+
* Resumes the underlying response stream.
|
|
96
|
+
*/
|
|
97
|
+
resume(): this;
|
|
98
|
+
/**
|
|
99
|
+
* Signs a string with a secret using the same algorithm as `cookie-signature`.
|
|
100
|
+
*/
|
|
101
|
+
sign(string: string, secret: string): string;
|
|
102
|
+
/**
|
|
103
|
+
* Unsigns a signed value. Returns the original string or `undefined` on failure.
|
|
104
|
+
*/
|
|
105
|
+
unsign(signed_value: string, secret: string): string | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Starts the body parser by registering a uWS `onData` callback.
|
|
108
|
+
*
|
|
109
|
+
* This is called before any body-consuming method (`.json()`, `.text()`, etc.)
|
|
110
|
+
* to begin buffering incoming body chunks.
|
|
111
|
+
*/
|
|
112
|
+
_body_parser_run(response: Response<TServer>, limit_bytes: number): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Returns the entire request body as a Buffer.
|
|
115
|
+
*/
|
|
116
|
+
buffer(): Promise<Buffer>;
|
|
117
|
+
/**
|
|
118
|
+
* Returns the entire request body as a string.
|
|
119
|
+
*/
|
|
120
|
+
text(): Promise<string>;
|
|
121
|
+
/**
|
|
122
|
+
* Parses the request body as JSON.
|
|
123
|
+
*/
|
|
124
|
+
json(default_value?: {}): Promise<any>;
|
|
125
|
+
/**
|
|
126
|
+
* Parses the request body as URL-encoded form data.
|
|
127
|
+
*/
|
|
128
|
+
urlencoded(): Promise<any>;
|
|
129
|
+
get locals(): any;
|
|
130
|
+
/** Whether the request stream is paused. */
|
|
131
|
+
get paused(): boolean;
|
|
132
|
+
/** Normalized HTTP method (uppercase). */
|
|
133
|
+
get method(): string;
|
|
134
|
+
/** Full URL (path + query string). */
|
|
135
|
+
get url(): string;
|
|
136
|
+
/** URL path (no query string). */
|
|
137
|
+
get path(): string;
|
|
138
|
+
/** Parsed query string parameters. */
|
|
139
|
+
get query(): any;
|
|
140
|
+
/** URL path parameters extracted (e.g. `/user/:id` → `{ id: "42" }`). */
|
|
141
|
+
get params(): any;
|
|
142
|
+
/**
|
|
143
|
+
* Parsed cookies from the `Cookie` header.
|
|
144
|
+
* Parsed lazily on first access.
|
|
145
|
+
*/
|
|
146
|
+
get cookies(): any;
|
|
147
|
+
/** The parsed body. Populated after calling `.parseBody()`, `.json()`, `.text()`, etc. */
|
|
148
|
+
get body(): any;
|
|
149
|
+
/**
|
|
150
|
+
* Automatically parses the request body based on `Content-Type`.
|
|
151
|
+
*/
|
|
152
|
+
parseBody(): Promise<any>;
|
|
153
|
+
/**
|
|
154
|
+
* Decodes the raw body buffer based on Content-Type.
|
|
155
|
+
*/
|
|
156
|
+
_finalize_body(): any;
|
|
157
|
+
/**
|
|
158
|
+
* Resolved remote IP address.
|
|
159
|
+
*
|
|
160
|
+
* When `trust_proxy` is enabled, it reads from `x-forwarded-for`.
|
|
161
|
+
* Otherwise, it uses the direct remote address from uWS.
|
|
162
|
+
*/
|
|
163
|
+
get ip(): string;
|
|
164
|
+
/**
|
|
165
|
+
* The proxy IP as reported by uWS.
|
|
166
|
+
*/
|
|
167
|
+
get proxy_ip(): string;
|
|
168
|
+
}
|