@e22m4u/js-trie-router 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -1
- package/dist/cjs/index.cjs +181 -40
- package/package.json +1 -1
- package/src/hooks/router-hook-invoker.d.ts +13 -0
- package/src/hooks/router-hook-invoker.js +185 -1
- package/src/hooks/router-hook-invoker.spec.js +305 -0
- package/src/hooks/router-hook-registry.d.ts +2 -1
- package/src/hooks/router-hook-registry.js +4 -2
- package/src/trie-router.js +128 -65
- package/src/trie-router.spec.js +357 -146
package/README.md
CHANGED
|
@@ -16,10 +16,10 @@ HTTP маршрутизатор для Node.js на основе
|
|
|
16
16
|
## Содержание
|
|
17
17
|
|
|
18
18
|
- [Установка](#установка)
|
|
19
|
-
- [Расширения](#расширения)
|
|
20
19
|
- [Использование](#использование)
|
|
21
20
|
- [Контекст запроса](#контекст-запроса)
|
|
22
21
|
- [Отправка ответа](#отправка-ответа)
|
|
22
|
+
- [Жизненный цикл](#жизненный-цикл)
|
|
23
23
|
- [Хуки маршрута](#хуки-маршрута)
|
|
24
24
|
- [Глобальные хуки](#глобальные-хуки)
|
|
25
25
|
- [Метаданные маршрута](#метаданные-маршрута)
|
|
@@ -178,6 +178,56 @@ router.defineRoute({
|
|
|
178
178
|
});
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
### Жизненный цикл
|
|
182
|
+
|
|
183
|
+
Для понимания того, как маршрутизатор обрабатывает входящий запрос, ниже
|
|
184
|
+
представлен порядок выполнения внутреннего конвейера и всех доступных хуков.
|
|
185
|
+
|
|
186
|
+
**На этапе запуска приложения**
|
|
187
|
+
|
|
188
|
+
1. [Глобальные хуки `onDefineRoute`](#ondefineroute).
|
|
189
|
+
\- Вызываются при регистрации маршрута.
|
|
190
|
+
|
|
191
|
+
**При получении входящего HTTP-запроса**
|
|
192
|
+
|
|
193
|
+
1. [Глобальные хуки `onRequest`](#onrequest)
|
|
194
|
+
\- Вызываются до поиска маршрута и создания контекста. Подходит для ранней
|
|
195
|
+
блокировки запроса или установки базовых заголовков.
|
|
196
|
+
|
|
197
|
+
2. Поиск маршрута
|
|
198
|
+
\- Маршрутизатор ищет совпадение пути. Если маршрут не найден,
|
|
199
|
+
отправляется ответ `404`, а дальнейшая обработка прекращается.
|
|
200
|
+
|
|
201
|
+
3. Создание `RequestContext` и парсинг
|
|
202
|
+
\- Создается экземпляр контекста, разбирается строка запроса, заголовки
|
|
203
|
+
и извлекается тело запроса.
|
|
204
|
+
|
|
205
|
+
4. [Глобальные хуки `preHandler`](#prehandler-глобальный)
|
|
206
|
+
\- Вызываются последовательно. Если хук возвращает значение
|
|
207
|
+
или отправляет ответ, цикл прерывается.
|
|
208
|
+
|
|
209
|
+
5. [Хуки маршрута `preHandler`](#prehandler-для-маршрута)
|
|
210
|
+
\- Вызываются последовательно для конкретного маршрута. Правила
|
|
211
|
+
прерывания такие же, как у одноименных глобальных хуков.
|
|
212
|
+
|
|
213
|
+
6. Основной обработчик (функция `handler`)
|
|
214
|
+
\- Вызывается для формирования ответа сервера.
|
|
215
|
+
|
|
216
|
+
7. [Хуки маршрута `postHandler`](#posthandler-для-маршрута)
|
|
217
|
+
\- Вызываются последовательно. Получают результат работы обработчика
|
|
218
|
+
(или из `preHandler`) и могут трансформировать его перед отправкой.
|
|
219
|
+
|
|
220
|
+
8. [Глобальные хуки `postHandler`](#posthandler-глобальный)
|
|
221
|
+
\- Завершают цепочку трансформации ответа.
|
|
222
|
+
|
|
223
|
+
9. Отправка ответа.
|
|
224
|
+
\- Маршрутизатор автоматически форматирует и отправляет итоговые данные
|
|
225
|
+
клиенту (если ответ не был отправлен ранее вручную).
|
|
226
|
+
|
|
227
|
+
Процесс обработки запроса обернут в глобальный `try/catch` блок. Любая ошибка,
|
|
228
|
+
выброшенная на любом этапе (в хуках, при разборе тела или в самом обработчике),
|
|
229
|
+
будет перехвачена и передана в `ErrorSender` для формирования ответа с ошибкой.
|
|
230
|
+
|
|
181
231
|
### Хуки маршрута
|
|
182
232
|
|
|
183
233
|
Определение маршрута методом `defineRoute` позволяет задать хуки
|
|
@@ -367,6 +417,7 @@ router.defineRoute({
|
|
|
367
417
|
которые выполняются на различных этапах жизненного цикла.
|
|
368
418
|
|
|
369
419
|
- [`onDefineRoute`](#ondefineroute) выполняется перед регистрацией маршрута;
|
|
420
|
+
- [`onRequest`](#onrequest) выполняется при получении входящего HTTP-запроса;
|
|
370
421
|
- [`preHandler`](#prehandler-глобальный) выполняется перед вызовом обработчика каждого маршрута;
|
|
371
422
|
- [`postHandler`](#posthandler-глобальный) выполняется после вызова обработчика каждого маршрута;
|
|
372
423
|
|
|
@@ -413,6 +464,51 @@ router.addHook(RouterHookType.ON_DEFINE_ROUTE, (routeDef, container) => {
|
|
|
413
464
|
// router.defineRoute(...)
|
|
414
465
|
```
|
|
415
466
|
|
|
467
|
+
#### onRequest
|
|
468
|
+
|
|
469
|
+
Глобальный хук `onRequest` выполняется самым первым при получении входящего
|
|
470
|
+
запроса. В этот момент маршрутизатор еще не начал поиск подходящего маршрута,
|
|
471
|
+
не разобрал тело запроса и не создавал `RequestContext` (контекст запроса).
|
|
472
|
+
|
|
473
|
+
Хук принимает три аргумента:
|
|
474
|
+
|
|
475
|
+
- `request: IncomingMessage` нативный экземпляр запроса;
|
|
476
|
+
- `response: ServerResponse` нативный экземпляр ответа;
|
|
477
|
+
- `container: ServiceContainer` сервис-контейнер приложения;
|
|
478
|
+
|
|
479
|
+
Это идеальное место для установки общих CORS-заголовков, раннего логирования
|
|
480
|
+
или блокировки нежелательных запросов (например, по IP).
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
router.addHook(RouterHookType.ON_REQUEST, (req, res, container) => {
|
|
484
|
+
// логирование входящего запроса до начала любой обработки
|
|
485
|
+
console.log(`[Incoming]: ${req.method} ${req.url}`);
|
|
486
|
+
// установка глобальных заголовков
|
|
487
|
+
res.setHeader('X-Powered-By', 'TrieRouter');
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Если хук отправляет ответ клиенту (например, вызывает `res.end()`) или явно
|
|
492
|
+
возвращает логическое значение `true`, маршрутизатор немедленно прерывает
|
|
493
|
+
обработку запроса. Поиск маршрута, чтение тела и вызов остальных хуков
|
|
494
|
+
выполнены не будут.
|
|
495
|
+
|
|
496
|
+
```js
|
|
497
|
+
router.addHook(RouterHookType.ON_REQUEST, (req, res) => {
|
|
498
|
+
const clientIp = req.socket.remoteAddress;
|
|
499
|
+
// пример блокировки запроса на самом раннем этапе
|
|
500
|
+
if (clientIp === '192.168.0.100') {
|
|
501
|
+
res.statusCode = 403;
|
|
502
|
+
res.end('Access denied');
|
|
503
|
+
return true; // прерывает дальнейшее выполнение
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Если хук `onRequest` возвращает значение, оно обязано быть логическим типом
|
|
509
|
+
или `undefined`. Также допускается `Promise`, разрешающийся этими значениями.
|
|
510
|
+
Попытка вернуть строку или объект приведет к выбросу ошибки.
|
|
511
|
+
|
|
416
512
|
#### preHandler (глобальный)
|
|
417
513
|
|
|
418
514
|
Глобальный хук `preHandler` вызывается перед каждым обработчиком маршрута,
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -964,9 +964,10 @@ var import_js_format12 = require("@e22m4u/js-format");
|
|
|
964
964
|
// src/hooks/router-hook-registry.js
|
|
965
965
|
var import_js_format11 = require("@e22m4u/js-format");
|
|
966
966
|
var RouterHookType = {
|
|
967
|
+
ON_DEFINE_ROUTE: "onDefineRoute",
|
|
968
|
+
ON_REQUEST: "onRequest",
|
|
967
969
|
PRE_HANDLER: "preHandler",
|
|
968
|
-
POST_HANDLER: "postHandler"
|
|
969
|
-
ON_DEFINE_ROUTE: "onDefineRoute"
|
|
970
|
+
POST_HANDLER: "postHandler"
|
|
970
971
|
};
|
|
971
972
|
var ROUTER_HOOK_TYPES = Object.values(RouterHookType);
|
|
972
973
|
var _RouterHookRegistry = class _RouterHookRegistry {
|
|
@@ -1057,6 +1058,110 @@ var RouterHookRegistry = _RouterHookRegistry;
|
|
|
1057
1058
|
|
|
1058
1059
|
// src/hooks/router-hook-invoker.js
|
|
1059
1060
|
var _RouterHookInvoker = class _RouterHookInvoker extends DebuggableService {
|
|
1061
|
+
/**
|
|
1062
|
+
* Invoke on-request hooks.
|
|
1063
|
+
*
|
|
1064
|
+
* @param {IncomingMessage} request
|
|
1065
|
+
* @param {ServerResponse} response
|
|
1066
|
+
* @returns {Promise<boolean|undefined>|boolean|undefined}
|
|
1067
|
+
*/
|
|
1068
|
+
invokeOnRequestHooks(request, response) {
|
|
1069
|
+
if (!request || typeof request !== "object" || Array.isArray(request) || !isReadableStream(request)) {
|
|
1070
|
+
throw new import_js_format12.InvalidArgumentError(
|
|
1071
|
+
'Parameter "request" must be an instance of IncomingMessage, but %v was given.',
|
|
1072
|
+
request
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (!response || typeof response !== "object" || Array.isArray(response) || !isWritableStream(response)) {
|
|
1076
|
+
throw new import_js_format12.InvalidArgumentError(
|
|
1077
|
+
'Parameter "response" must be an instance of ServerResponse, but %v was given.',
|
|
1078
|
+
response
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (isResponseSent(response)) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const hooks = this.getService(RouterHookRegistry).getHooks(
|
|
1085
|
+
RouterHookType.ON_REQUEST
|
|
1086
|
+
);
|
|
1087
|
+
let isInterrupted = void 0;
|
|
1088
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
1089
|
+
const hook = hooks[i];
|
|
1090
|
+
const result = hook(request, response, this.container);
|
|
1091
|
+
if (isResponseSent(response)) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (result !== void 0) {
|
|
1095
|
+
if (isPromise(result)) {
|
|
1096
|
+
return this._continueOnRequestHooksInvocationAsync(
|
|
1097
|
+
hooks,
|
|
1098
|
+
i + 1,
|
|
1099
|
+
result,
|
|
1100
|
+
request,
|
|
1101
|
+
response
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
if (result === true) {
|
|
1105
|
+
isInterrupted = result;
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
if (result !== false) {
|
|
1109
|
+
throw new import_js_format12.InvalidArgumentError(
|
|
1110
|
+
'Hook "onRequest" must return undefined or a Boolean, but %v was given.',
|
|
1111
|
+
result
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return isInterrupted;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Continue on-request hooks invocation async.
|
|
1120
|
+
*
|
|
1121
|
+
* @param {Function[]} hooks
|
|
1122
|
+
* @param {number} startIndex
|
|
1123
|
+
* @param {Promise} initialPromise
|
|
1124
|
+
* @param {IncomingMessage} request
|
|
1125
|
+
* @param {ServerResponse} response
|
|
1126
|
+
* @returns {Promise<boolean|undefined>}
|
|
1127
|
+
*/
|
|
1128
|
+
async _continueOnRequestHooksInvocationAsync(hooks, startIndex, initialPromise, request, response) {
|
|
1129
|
+
let result = await initialPromise;
|
|
1130
|
+
if (isResponseSent(response)) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (result !== void 0) {
|
|
1134
|
+
if (result === true) {
|
|
1135
|
+
return result;
|
|
1136
|
+
}
|
|
1137
|
+
if (result !== false) {
|
|
1138
|
+
throw new import_js_format12.InvalidArgumentError(
|
|
1139
|
+
'Hook "onRequest" must return undefined or a Boolean, but %v was given.',
|
|
1140
|
+
result
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
let isInterrupted = void 0;
|
|
1145
|
+
for (let i = startIndex; i < hooks.length; i++) {
|
|
1146
|
+
result = await hooks[i](request, response, this.container);
|
|
1147
|
+
if (isResponseSent(response)) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (result !== void 0) {
|
|
1151
|
+
if (result === true) {
|
|
1152
|
+
isInterrupted = result;
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
if (result !== false) {
|
|
1156
|
+
throw new import_js_format12.InvalidArgumentError(
|
|
1157
|
+
'Hook "onRequest" must return undefined or a Boolean, but %v was given.',
|
|
1158
|
+
result
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return isInterrupted;
|
|
1164
|
+
}
|
|
1060
1165
|
/**
|
|
1061
1166
|
* Последовательно вызывает глобальные хуки и хуки маршрута типа "preHandler",
|
|
1062
1167
|
* пока один из них не вернет отличное от undefined значение или не отправит
|
|
@@ -2318,40 +2423,66 @@ var _TrieRouter = class _TrieRouter extends DebuggableService {
|
|
|
2318
2423
|
async _handleRequest(request, response) {
|
|
2319
2424
|
const debug = this.getDebuggerFor(this._handleRequest);
|
|
2320
2425
|
const requestPath = getRequestPathname(request);
|
|
2321
|
-
const routeRegistry = this.getService(RouteRegistry);
|
|
2322
2426
|
debug("Handling an incoming request %s %v.", request.method, requestPath);
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2427
|
+
try {
|
|
2428
|
+
if (isResponseSent(response)) {
|
|
2429
|
+
debug("Response has been sent before handling.");
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const hookInvoker = this.getService(RouterHookInvoker);
|
|
2433
|
+
const onRequestHooks = this.getService(RouterHookRegistry).getHooks(
|
|
2434
|
+
RouterHookType.ON_REQUEST
|
|
2435
|
+
);
|
|
2436
|
+
if (onRequestHooks.length) {
|
|
2437
|
+
debug('Invoking "onRequest" hooks, %v hook(s) found.');
|
|
2438
|
+
let shouldIgnoreRequest = hookInvoker.invokeOnRequestHooks(
|
|
2439
|
+
request,
|
|
2440
|
+
response
|
|
2441
|
+
);
|
|
2442
|
+
if (isPromise(shouldIgnoreRequest)) {
|
|
2443
|
+
shouldIgnoreRequest = await shouldIgnoreRequest;
|
|
2444
|
+
}
|
|
2445
|
+
if (isResponseSent(response)) {
|
|
2446
|
+
debug('Response has been sent by "onRequest" hook.');
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
if (shouldIgnoreRequest === true) {
|
|
2450
|
+
debug('Response handling was interrupted by "onRequest" hook.');
|
|
2336
2451
|
return;
|
|
2337
2452
|
}
|
|
2338
2453
|
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
request.method
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2454
|
+
const resolved = this.getService(RouteRegistry).matchRouteByRequest(request);
|
|
2455
|
+
if (!resolved) {
|
|
2456
|
+
if (request.method.toUpperCase() === HttpMethod.OPTIONS) {
|
|
2457
|
+
const allowedMethods = this.getService(RouteRegistry).getAllowedMethodsForRequestPath(
|
|
2458
|
+
requestPath
|
|
2459
|
+
);
|
|
2460
|
+
if (allowedMethods.length > 0) {
|
|
2461
|
+
debug("Auto-handling OPTIONS request.");
|
|
2462
|
+
if (!allowedMethods.includes("OPTIONS")) {
|
|
2463
|
+
allowedMethods.push("OPTIONS");
|
|
2464
|
+
}
|
|
2465
|
+
const allowHeader = allowedMethods.join(", ");
|
|
2466
|
+
response.statusCode = 204;
|
|
2467
|
+
response.setHeader("Allow", allowHeader);
|
|
2468
|
+
response.end();
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
debug(
|
|
2473
|
+
"No route found for the request %s %v.",
|
|
2474
|
+
request.method,
|
|
2475
|
+
requestPath
|
|
2476
|
+
);
|
|
2477
|
+
this.getService(ErrorSender).send404(request, response);
|
|
2478
|
+
} else {
|
|
2479
|
+
const { route, params } = resolved;
|
|
2480
|
+
const container = new import_js_service4.ServiceContainer(this.container);
|
|
2481
|
+
const context = new RequestContext(container, request, response, route);
|
|
2482
|
+
container.set(RequestContext, context);
|
|
2483
|
+
container.set(import_http4.IncomingMessage, request);
|
|
2484
|
+
container.set(import_http4.ServerResponse, response);
|
|
2485
|
+
context.params = params;
|
|
2355
2486
|
const reqDataOrPromise = this.getService(RequestParser).parse(request);
|
|
2356
2487
|
if (isPromise(reqDataOrPromise)) {
|
|
2357
2488
|
const reqData = await reqDataOrPromise;
|
|
@@ -2359,8 +2490,7 @@ var _TrieRouter = class _TrieRouter extends DebuggableService {
|
|
|
2359
2490
|
} else {
|
|
2360
2491
|
Object.assign(context, reqDataOrPromise);
|
|
2361
2492
|
}
|
|
2362
|
-
|
|
2363
|
-
data = hookInvoker.invokePreHandlerHooks(context);
|
|
2493
|
+
let data = hookInvoker.invokePreHandlerHooks(context);
|
|
2364
2494
|
if (isPromise(data)) {
|
|
2365
2495
|
data = await data;
|
|
2366
2496
|
}
|
|
@@ -2371,6 +2501,10 @@ var _TrieRouter = class _TrieRouter extends DebuggableService {
|
|
|
2371
2501
|
data = await data;
|
|
2372
2502
|
}
|
|
2373
2503
|
}
|
|
2504
|
+
if (isResponseSent(response)) {
|
|
2505
|
+
debug("Response has been sent by the route handler.");
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2374
2508
|
let postHandlerData = hookInvoker.invokePostHandlerHooks(
|
|
2375
2509
|
context,
|
|
2376
2510
|
data
|
|
@@ -2378,17 +2512,24 @@ var _TrieRouter = class _TrieRouter extends DebuggableService {
|
|
|
2378
2512
|
if (isPromise(postHandlerData)) {
|
|
2379
2513
|
postHandlerData = await postHandlerData;
|
|
2380
2514
|
}
|
|
2515
|
+
if (isResponseSent(response)) {
|
|
2516
|
+
debug('Response has been sent by "postHandler" hook.');
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2381
2519
|
if (postHandlerData !== void 0) {
|
|
2382
2520
|
data = postHandlerData;
|
|
2383
2521
|
}
|
|
2522
|
+
} else {
|
|
2523
|
+
debug('Response has been sent by "preHandler" hook.');
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (!isResponseSent(response)) {
|
|
2527
|
+
this.getService(DataSender).send(response, data);
|
|
2384
2528
|
}
|
|
2385
|
-
} catch (error) {
|
|
2386
|
-
this.getService(ErrorSender).send(request, response, error);
|
|
2387
|
-
return;
|
|
2388
|
-
}
|
|
2389
|
-
if (!isResponseSent(response)) {
|
|
2390
|
-
this.getService(DataSender).send(response, data);
|
|
2391
2529
|
}
|
|
2530
|
+
} catch (error) {
|
|
2531
|
+
this.getService(ErrorSender).send(request, response, error);
|
|
2532
|
+
return;
|
|
2392
2533
|
}
|
|
2393
2534
|
}
|
|
2394
2535
|
/**
|
package/package.json
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import {ValueOrPromise} from '../types.js';
|
|
2
2
|
import {RequestContext} from '../request-context.js';
|
|
3
|
+
import {IncomingMessage, ServerResponse} from 'http';
|
|
3
4
|
import {DebuggableService} from '../debuggable-service.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Router hook invoker.
|
|
7
8
|
*/
|
|
8
9
|
export declare class RouterHookInvoker extends DebuggableService {
|
|
10
|
+
/**
|
|
11
|
+
* Invoke on-request hooks.
|
|
12
|
+
*
|
|
13
|
+
* @param {IncomingMessage} request
|
|
14
|
+
* @param {ServerResponse} response
|
|
15
|
+
* @returns {Promise<boolean|undefined>|boolean|undefined}
|
|
16
|
+
*/
|
|
17
|
+
invokeOnRequestHooks(
|
|
18
|
+
request: IncomingMessage,
|
|
19
|
+
response: ServerResponse,
|
|
20
|
+
): Promise<boolean | undefined> | boolean | undefined;
|
|
21
|
+
|
|
9
22
|
/**
|
|
10
23
|
* Invoke pre-handler hooks.
|
|
11
24
|
*
|
|
@@ -1,13 +1,197 @@
|
|
|
1
1
|
import {RequestContext} from '../request-context.js';
|
|
2
|
+
import {IncomingMessage, ServerResponse} from 'http';
|
|
2
3
|
import {InvalidArgumentError} from '@e22m4u/js-format';
|
|
3
4
|
import {DebuggableService} from '../debuggable-service.js';
|
|
4
|
-
import {isPromise, isResponseSent} from '../utils/index.js';
|
|
5
5
|
import {RouterHookRegistry, RouterHookType} from './router-hook-registry.js';
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
isPromise,
|
|
9
|
+
isResponseSent,
|
|
10
|
+
isReadableStream,
|
|
11
|
+
isWritableStream,
|
|
12
|
+
} from '../utils/index.js';
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Router hook invoker.
|
|
9
16
|
*/
|
|
10
17
|
export class RouterHookInvoker extends DebuggableService {
|
|
18
|
+
/**
|
|
19
|
+
* Invoke on-request hooks.
|
|
20
|
+
*
|
|
21
|
+
* @param {IncomingMessage} request
|
|
22
|
+
* @param {ServerResponse} response
|
|
23
|
+
* @returns {Promise<boolean|undefined>|boolean|undefined}
|
|
24
|
+
*/
|
|
25
|
+
invokeOnRequestHooks(request, response) {
|
|
26
|
+
if (
|
|
27
|
+
!request ||
|
|
28
|
+
typeof request !== 'object' ||
|
|
29
|
+
Array.isArray(request) ||
|
|
30
|
+
!isReadableStream(request)
|
|
31
|
+
) {
|
|
32
|
+
throw new InvalidArgumentError(
|
|
33
|
+
'Parameter "request" must be an instance of IncomingMessage, ' +
|
|
34
|
+
'but %v was given.',
|
|
35
|
+
request,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
!response ||
|
|
40
|
+
typeof response !== 'object' ||
|
|
41
|
+
Array.isArray(response) ||
|
|
42
|
+
!isWritableStream(response)
|
|
43
|
+
) {
|
|
44
|
+
throw new InvalidArgumentError(
|
|
45
|
+
'Parameter "response" must be an instance of ServerResponse, ' +
|
|
46
|
+
'but %v was given.',
|
|
47
|
+
response,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
// если ответ уже отправлен,
|
|
51
|
+
// то вызов хуков прерывается
|
|
52
|
+
if (isResponseSent(response)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// итерация по хукам выполняется по индексу,
|
|
56
|
+
// чтобы знать, с какого места продолжать
|
|
57
|
+
// в асинхронном режиме
|
|
58
|
+
const hooks = this.getService(RouterHookRegistry).getHooks(
|
|
59
|
+
RouterHookType.ON_REQUEST,
|
|
60
|
+
);
|
|
61
|
+
let isInterrupted = undefined;
|
|
62
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
63
|
+
const hook = hooks[i];
|
|
64
|
+
// вызов хука выполняется
|
|
65
|
+
// в синхронном режиме
|
|
66
|
+
const result = hook(request, response, this.container);
|
|
67
|
+
// если ответ уже отправлен,
|
|
68
|
+
// то вызов хуков прерывается
|
|
69
|
+
if (isResponseSent(response)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// если синхронный вызов хука вернул значение отличное
|
|
73
|
+
// от undefined, то требуется проверить данное значение
|
|
74
|
+
// для коррекции режима вызова оставшихся хуков
|
|
75
|
+
if (result !== undefined) {
|
|
76
|
+
// если синхронный вызов хука вернул Promise, то дальнейшее
|
|
77
|
+
// выполнение переключается в асинхронный режим, начиная
|
|
78
|
+
// с индекса следующего хука
|
|
79
|
+
if (isPromise(result)) {
|
|
80
|
+
return this._continueOnRequestHooksInvocationAsync(
|
|
81
|
+
hooks,
|
|
82
|
+
i + 1,
|
|
83
|
+
result,
|
|
84
|
+
request,
|
|
85
|
+
response,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
// если синхронный хук вернул значение true,
|
|
89
|
+
// то выполняется установка флага прерывания
|
|
90
|
+
// обработки запроса и выход из цикла
|
|
91
|
+
if (result === true) {
|
|
92
|
+
isInterrupted = result;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
// если синхронный хук вернул значение, отличное
|
|
96
|
+
// от логического, то выбрасывается ошибка
|
|
97
|
+
if (result !== false) {
|
|
98
|
+
throw new InvalidArgumentError(
|
|
99
|
+
'Hook "onRequest" must return undefined or a Boolean, ' +
|
|
100
|
+
'but %v was given.',
|
|
101
|
+
result,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// если все хуки были синхронными, то возвращается
|
|
107
|
+
// значение флага прерывания обработки запроса
|
|
108
|
+
// в результат вызова
|
|
109
|
+
return isInterrupted;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Continue on-request hooks invocation async.
|
|
114
|
+
*
|
|
115
|
+
* @param {Function[]} hooks
|
|
116
|
+
* @param {number} startIndex
|
|
117
|
+
* @param {Promise} initialPromise
|
|
118
|
+
* @param {IncomingMessage} request
|
|
119
|
+
* @param {ServerResponse} response
|
|
120
|
+
* @returns {Promise<boolean|undefined>}
|
|
121
|
+
*/
|
|
122
|
+
async _continueOnRequestHooksInvocationAsync(
|
|
123
|
+
hooks,
|
|
124
|
+
startIndex,
|
|
125
|
+
initialPromise,
|
|
126
|
+
request,
|
|
127
|
+
response,
|
|
128
|
+
) {
|
|
129
|
+
// ожидание Promise, который был получен
|
|
130
|
+
// на предыдущем шаге (в синхронном режиме)
|
|
131
|
+
let result = await initialPromise;
|
|
132
|
+
// если ответ уже отправлен,
|
|
133
|
+
// то вызов хуков прерывается
|
|
134
|
+
if (isResponseSent(response)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// если Promise разрешился значением, отличным
|
|
138
|
+
// от undefined, то в зависимости от значения,
|
|
139
|
+
// цикл вызова хуков прерывается
|
|
140
|
+
if (result !== undefined) {
|
|
141
|
+
// если Promise разрешился значением true,
|
|
142
|
+
// то данное значение немедленно возвращается
|
|
143
|
+
if (result === true) {
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
// если Promise вернул значение, отличное
|
|
147
|
+
// от логического, то выбрасывается ошибка
|
|
148
|
+
if (result !== false) {
|
|
149
|
+
throw new InvalidArgumentError(
|
|
150
|
+
'Hook "onRequest" must return undefined or a Boolean, ' +
|
|
151
|
+
'but %v was given.',
|
|
152
|
+
result,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// продолжение вызова хуков начиная
|
|
157
|
+
// со следующего индекса (асинхронно)
|
|
158
|
+
let isInterrupted = undefined;
|
|
159
|
+
for (let i = startIndex; i < hooks.length; i++) {
|
|
160
|
+
// с этого момента все синхронные
|
|
161
|
+
// хуки выполняются как асинхронные
|
|
162
|
+
result = await hooks[i](request, response, this.container);
|
|
163
|
+
// если ответ уже отправлен,
|
|
164
|
+
// то вызов хуков прерывается
|
|
165
|
+
if (isResponseSent(response)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// если вызов хука вернул значение отличное
|
|
169
|
+
// от undefined, то выполняется проверка
|
|
170
|
+
// необходимости выхода из цикла
|
|
171
|
+
if (result !== undefined) {
|
|
172
|
+
// если хук вернул значение true, то выполняется
|
|
173
|
+
// установка флага прерывания обработки запроса
|
|
174
|
+
// и выход из цикла
|
|
175
|
+
if (result === true) {
|
|
176
|
+
isInterrupted = result;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
// если хук вернул значение, отличное
|
|
180
|
+
// от логического, то выбрасывается ошибка
|
|
181
|
+
if (result !== false) {
|
|
182
|
+
throw new InvalidArgumentError(
|
|
183
|
+
'Hook "onRequest" must return undefined or a Boolean, ' +
|
|
184
|
+
'but %v was given.',
|
|
185
|
+
result,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// передача флага прерывания обработки
|
|
191
|
+
// запроса в результат вызова
|
|
192
|
+
return isInterrupted;
|
|
193
|
+
}
|
|
194
|
+
|
|
11
195
|
/**
|
|
12
196
|
* Последовательно вызывает глобальные хуки и хуки маршрута типа "preHandler",
|
|
13
197
|
* пока один из них не вернет отличное от undefined значение или не отправит
|