-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathRhinoCreateServer.ts
270 lines (246 loc) · 11.3 KB
/
RhinoCreateServer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import { HTTPServer } from "./deps.ts";
import {
DecoratedClass,
ErrorClass,
ErrorData,
HookClass,
Result,
RhinoErrorHandler,
RhinoRequest,
RhinoResponse,
RhinoRouter,
StatusCode,
_Result,
_RhinoEndpoint,
_RhinoHook,
_RhinoServer,
} from "./mod.ts";
export class CreateServer {
// constructor parameters
public PORT: number;
public HOSTNAME: string;
// class parameters
private SERVER: HTTPServer.Server;
private PRE_HOOKS: HookClass[] = [];
private AFTER_HOOKS: HookClass[] = [];
private ERROR_HANDLERS: ErrorClass[] = [];
/**
* Creates a new server that listens to the provided optional
* hostname through the provided port.
* @param init The server's initialization parameters
*/
constructor(readonly serverClass: DecoratedClass) {
const s = new serverClass() as _RhinoServer;
this.PORT = s.port;
this.HOSTNAME = s.hostname || "0.0.0.0";
// Divides all the hooks to be "pre" or "after" hooks
(s.hooks ?? []).filter((hook) => {
if (hook.prototype && hook.prototype.hookParams.type === "PRE") {
this.PRE_HOOKS.push(hook);
} else if (hook.prototype && hook.prototype.hookParams.type === "AFTER") {
this.AFTER_HOOKS.push(hook);
}
});
// Defines the error handlers
this.ERROR_HANDLERS = s.errorHandlers ?? [];
// Creates a server on the port specified in PORT
if (s.TLS) {
// Creates a TLS server if the TLS option is provided
this.SERVER = HTTPServer.serveTLS({
port: this.PORT,
hostname: this.HOSTNAME,
certFile: s.TLS.certFile,
keyFile: s.TLS.keyFile,
});
} else {
// Creates a standard server without TLS
this.SERVER = HTTPServer.serve({ port: this.PORT, hostname: this.HOSTNAME });
}
// Starts listening for requests
this.listen(s.router).then(() => s.onListening(s));
}
/**
* Starts listening for requests to the server under the path {hostname}:{port}/{root}/REQUEST
* @param callback The callback function to execute once the server starts listening for requests
*/
public listen(router: RhinoRouter): Promise<{}> {
return new Promise(async (resolve, reject) => {
try {
// resolves the promise
resolve(this.SERVER.listener);
// Listens to all the requests made to the server
for await (const req of this.SERVER) this.processRequests(req, router);
} catch (err) {
reject(err);
}
});
}
/**
* Processes the requests made to the sever. This is
* what is know in Rhino as the request-response middleware pipeline.
* @param req The request object sent by the client
*/
private async processRequests(req: HTTPServer.ServerRequest, router: RhinoRouter) {
// The general request object
const genRequest = new RhinoRequest(req);
const genResponse = new RhinoResponse(req);
// Holds any error data sent by any
// of the middlewares
let errorData: any;
// Execute all the "pre" hooks
for await (const err of this.execPreHooks(genRequest, genResponse)) {
// If the headers were sent to the client by one of the hooks, or the err constant holds
// a value (sent by calling the error callback in the hook's class), we break the loop.
if (genResponse.headersSent || err) {
if (err) errorData = err;
break;
}
}
// Matches the URL with an endpoint, and executes the endpoint's callback
if (!errorData && !genResponse.headersSent) {
for await (const err of this.execEndpoint(router, genRequest, genResponse)) {
if (genResponse.headersSent || err) {
if (err) errorData = err;
break;
}
}
}
// If a response is yet to be sent, we execute all the "after" hooks
if (!errorData && !genResponse.headersSent) {
for await (const err of this.execAfterHooks(genRequest, genResponse)) {
// If the headers were sent to the client by one of the hooks, or the err constant holds
// a value (sent by calling the error callback in the hook's class), we break the loop.
if (genResponse.headersSent || err) {
if (err) errorData = err;
break;
}
}
}
// Error Handling loop
if (errorData && !genResponse.headersSent) {
// If the passed error code is a valid status code, we set it automatically
if (Object.values(StatusCode).includes(errorData.code)) genResponse.status(errorData.code);
for await (const err of this.execErrorHandlers(errorData, genRequest, genResponse)) {
// If the headers were sent to the client by one of the error handlers, or the err constant holds
// a value (sent by calling the error callback in the hook's class), we break the loop.
if (genResponse.headersSent || err) {
if (err) errorData = err;
break;
}
}
}
}
/**
* Executes all hooks labeled as "PRE" (one-by-one; in order of declaration).
* @param req The generated RhinoRequest for this request
* @param res The generated RhinoResponse for this request
*/
private *execPreHooks(req: RhinoRequest, res: RhinoResponse) {
for (let i = 0; i < this.PRE_HOOKS.length; i++) {
const hook = this.PRE_HOOKS[i];
yield new Promise<Result>((resolve) => {
const hookInstance = new hook(req, res, resolve, resolve) as _RhinoHook;
// If the hook's path can be matched to a route, then we
// execute the hook before executing the endpoint handler.
// Also, if the hook's path is exactly equal to "**", then
// urlMatch will return true, so the hook will also execute.
if (!res.headersSent && req.URLObject.pathMatch(hookInstance.path || "**")) {
hookInstance.executeHook();
// we resolve the promise after the handler function
// has been executed so that the loop can reach the end,
// even if the handler does not explicitly call next/error
resolve(_Result.OK);
}
});
}
}
/**
* Executes the first found endpoint for the request. If no endpoints
* were found that could handle the request, the program proceeds to execute
* the hooks labeled as "AFTER"
* @param router The application's router
* @param req The generated RhinoRequest for the request
* @param res The generated RhinoResponse for the request
*/
private *execEndpoint(router: RhinoRouter, req: RhinoRequest, res: RhinoResponse) {
// Finds an endpoint that can handle the request
const foundEndpoints = router.matchEndpoint(req) ?? [];
for (let i = 0; i < foundEndpoints.length; i++) {
const endpoint = foundEndpoints[i];
// Once an endpoint it found, we attach the endpoint-specific
// properties for the request
req.params = req.URLObject.getParams(endpoint.fullPath);
req.routePath = endpoint.routePath;
req.fullPath = endpoint.fullPath;
yield new Promise<Result>((resolve) => {
if (endpoint.handler.prototype.endpointParams.canActivate(req)) {
// Then, we execute the handler for the path
const ep = new endpoint.handler(req, res, resolve, resolve) as _RhinoEndpoint;
ep.onEndpointCall();
// we resolve the promise after the handler function
// has been executed so that the loop can reach the end,
// even if the handler does not explicitly call next/error
resolve(_Result.OK);
} else {
resolve({
code: StatusCode.Forbidden,
data: {
origin: "canActivate",
message: `The endpoint ${endpoint.fullPath} rejected the request`,
},
});
}
});
}
}
/**
* Executes all hooks labeled as "AFTER" (one-by-one; in order of declaration).
* @param req The generated RhinoRequest for this request
* @param res The generated RhinoResponse for this request
*/
private *execAfterHooks(req: RhinoRequest, res: RhinoResponse) {
for (let i = 0; i < this.AFTER_HOOKS.length; i++) {
const hook = this.AFTER_HOOKS[i];
yield new Promise<Result>((resolve) => {
const hookInstance = new hook(req, res, resolve, resolve) as _RhinoHook;
// If the hook's path can be matched to a route, the we
// execute the hook after executing the endpoint handler.
// Also, if the hook's path is exactly equal to "**", then
// urlMatch will return true, so the hook will also execute.
if (!res.headersSent && req.URLObject.pathMatch(hookInstance.path || "**")) {
hookInstance.executeHook();
// we resolve the promise after the handler function
// has been executed so that the loop can reach the end,
// even if the handler does not explicitly call next/error
resolve(_Result.OK);
}
});
}
}
/**
* Executes an error handler based on the error code provided by the
* class which threw the error
* @param error The error data thrown by one of the middlewares
* @param req The generated RhinoRequest for this request
* @param res The generated RhinoResponse for this request
*/
private *execErrorHandlers(error: ErrorData, req: RhinoRequest, res: RhinoResponse) {
for (let i = 0; i < this.ERROR_HANDLERS.length; i++) {
const errHandler = this.ERROR_HANDLERS[i];
yield new Promise<Result>((resolve) => {
const hookInstance = new errHandler(error, req, res, resolve, resolve) as RhinoErrorHandler;
// If the hook's path can be matched to a route, the we
// execute the hook after executing the endpoint handler.
// Also, if the hook's path is exactly equal to "**", then
// urlMatch will return true, so the hook will also execute.
if (error.code === hookInstance.errorCode) {
hookInstance.executeError();
// we resolve the promise after the handler function
// has been executed so that the loop can reach the end,
// even if the handler does not explicitly call next/error
resolve(_Result.OK);
}
});
}
}
}