@e22m4u/js-trie-router 0.5.6 → 0.5.8
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 +67 -0
- package/dist/cjs/index.cjs +88 -67
- package/examples/cookies-parsing-example.js +1 -2
- package/examples/params-parsing-example.js +1 -2
- package/examples/query-parsing-example.js +1 -2
- package/examples/router-branch-example.js +36 -0
- package/examples/uptime-example.js +1 -2
- package/package.json +1 -1
- package/src/branch/router-branch.js +1 -1
- package/src/hooks/hook-invoker.js +69 -36
- package/src/parsers/body-parser.js +7 -5
- package/src/parsers/cookies-parser.js +2 -2
- package/src/parsers/query-parser.js +2 -2
- package/src/parsers/request-parser.js +1 -1
- package/src/route/route.js +9 -9
- package/src/route/route.spec.js +316 -297
- package/src/route-registry.js +5 -13
- package/src/senders/data-sender.js +7 -10
- package/src/senders/error-sender.js +2 -2
- package/src/trie-router.js +4 -8
- package/src/utils/index.d.ts +1 -0
- package/src/utils/index.js +1 -0
- package/src/utils/to-pascal-case.d.ts +6 -0
- package/src/utils/to-pascal-case.js +26 -0
- package/src/utils/to-pascal-case.spec.js +15 -0
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ HTTP маршрутизатор для Node.js на основе
|
|
|
26
26
|
- [Метаданные маршрута](#метаданные-маршрута)
|
|
27
27
|
- [Состояние запроса](#состояние-запроса)
|
|
28
28
|
- [Ветвление маршрутов](#ветвление-маршрутов)
|
|
29
|
+
- [Обработка ошибок](#обработка-ошибок)
|
|
29
30
|
- [Отладка](#отладка)
|
|
30
31
|
- [Тестирование](#тестирование)
|
|
31
32
|
- [Лицензия](#лицензия)
|
|
@@ -410,6 +411,72 @@ v1Branch.defineRoute({
|
|
|
410
411
|
// GET /api/v1/status
|
|
411
412
|
```
|
|
412
413
|
|
|
414
|
+
## Обработка ошибок
|
|
415
|
+
|
|
416
|
+
Маршрутизатор автоматически перехватывает любые ошибки, выброшенные из хуков
|
|
417
|
+
или обработчика маршрута. По умолчанию, любая ошибка приводит к ответу сервера
|
|
418
|
+
со статусом *500 Internal Server Error*.
|
|
419
|
+
|
|
420
|
+
Для более гибкого управления HTTP-статусами рекомендуется использовать
|
|
421
|
+
библиотеку [http-errors](https://www.npmjs.com/package/http-errors).
|
|
422
|
+
Стандартный обработчик ошибок спроектирован для работы с данной библиотекой
|
|
423
|
+
и автоматически извлекает `statusCode`, `message` и другие свойства ошибки.
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
import HttpErrors from 'http-errors';
|
|
427
|
+
|
|
428
|
+
router.defineRoute({
|
|
429
|
+
method: HttpMethod.GET,
|
|
430
|
+
path: '/users/:id',
|
|
431
|
+
handler(ctx) {
|
|
432
|
+
const {id} = ctx.params;
|
|
433
|
+
const user = findUserById(id); // логика поиска пользователя
|
|
434
|
+
if (!user) {
|
|
435
|
+
// выброс ошибки 404 Not Found
|
|
436
|
+
// маршрутизатор перехватит ее и отправит JSON-ответ
|
|
437
|
+
throw new HttpErrors.NotFound('Пользователь не найден');
|
|
438
|
+
}
|
|
439
|
+
if (!hasAccess(ctx.state.currentUser, user)) {
|
|
440
|
+
// ошибка 403 Forbidden с дополнительными данными
|
|
441
|
+
// свойства "code" и "details" будут добавлены к ответу
|
|
442
|
+
const error = new HttpErrors.Forbidden('Доступ запрещен');
|
|
443
|
+
error.code = 'ACCESS_DENIED';
|
|
444
|
+
error.details = {reason: 'Недостаточно прав'};
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
return user;
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Структура ответа будет зависеть от данных, содержащихся в объекте ошибки.
|
|
453
|
+
Например, первая ошибка из примера выше `HttpErrors.NotFound` приведет
|
|
454
|
+
к ответу со статусом `404` и телом, содержащим указанное сообщение.
|
|
455
|
+
|
|
456
|
+
```json
|
|
457
|
+
{
|
|
458
|
+
"error": {
|
|
459
|
+
"message": "Пользователь не найден"
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Если объект ошибки содержит дополнительные поля (например, `details`
|
|
465
|
+
или `code`), они автоматически включаются в ответ. Что позволяет
|
|
466
|
+
передавать клиенту более детальную информацию.
|
|
467
|
+
|
|
468
|
+
```json
|
|
469
|
+
{
|
|
470
|
+
"error": {
|
|
471
|
+
"code": "ACCESS_DENIED",
|
|
472
|
+
"message": "Доступ запрещен",
|
|
473
|
+
"details": {
|
|
474
|
+
"reason": "Недостаточно прав"
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
413
480
|
## Отладка
|
|
414
481
|
|
|
415
482
|
Установка переменной `DEBUG` включает вывод логов.
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -70,6 +70,7 @@ __export(index_exports, {
|
|
|
70
70
|
parseCookieString: () => parseCookieString,
|
|
71
71
|
parseJsonBody: () => parseJsonBody,
|
|
72
72
|
toCamelCase: () => toCamelCase,
|
|
73
|
+
toPascalCase: () => toPascalCase,
|
|
73
74
|
validateRouteDefinition: () => validateRouteDefinition
|
|
74
75
|
});
|
|
75
76
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -200,6 +201,15 @@ function toCamelCase(input) {
|
|
|
200
201
|
}
|
|
201
202
|
__name(toCamelCase, "toCamelCase");
|
|
202
203
|
|
|
204
|
+
// src/utils/to-pascal-case.js
|
|
205
|
+
function toPascalCase(input) {
|
|
206
|
+
if (!input) {
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
return input.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([0-9])([a-zA-Z])/g, "$1 $2").replace(/[-_]+|[^\p{L}\p{N}]/gu, " ").toLowerCase().replace(new RegExp("(?:^|\\s)(\\p{L})", "gu"), (_, letter) => letter.toUpperCase()).replace(/\s+/g, "");
|
|
210
|
+
}
|
|
211
|
+
__name(toPascalCase, "toPascalCase");
|
|
212
|
+
|
|
203
213
|
// src/utils/normalize-path.js
|
|
204
214
|
function normalizePath(value, noStartingSlash = false) {
|
|
205
215
|
if (typeof value !== "string") {
|
|
@@ -885,7 +895,11 @@ var HookRegistry = _HookRegistry;
|
|
|
885
895
|
// src/hooks/hook-invoker.js
|
|
886
896
|
var _HookInvoker = class _HookInvoker extends DebuggableService {
|
|
887
897
|
/**
|
|
888
|
-
*
|
|
898
|
+
* Последовательно вызывает глобальные хуки и хуки маршрута указанного
|
|
899
|
+
* типа, пока один из них не вернет отличное от undefined и null значение
|
|
900
|
+
* или не отправит HTTP-ответ. Метод выполняет хуки в синхронном режиме
|
|
901
|
+
* для улучшения производительности. Если один из хуков возвращает Promise,
|
|
902
|
+
* выполнение оставшейся части цепочки переключается в асинхронный режим.
|
|
889
903
|
*
|
|
890
904
|
* @param {Route} route
|
|
891
905
|
* @param {string} hookType
|
|
@@ -934,31 +948,53 @@ var _HookInvoker = class _HookInvoker extends DebuggableService {
|
|
|
934
948
|
}
|
|
935
949
|
if (result != null) {
|
|
936
950
|
if (isPromise(result)) {
|
|
937
|
-
return (
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
for (let j = i + 1; j < hooks.length; j++) {
|
|
946
|
-
asyncResult = await hooks[j](...args);
|
|
947
|
-
if (isResponseSent(response)) {
|
|
948
|
-
return response;
|
|
949
|
-
}
|
|
950
|
-
if (asyncResult != null) {
|
|
951
|
-
return asyncResult;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
return;
|
|
955
|
-
})();
|
|
951
|
+
return this._continueHooksInvocationAsync(
|
|
952
|
+
hooks,
|
|
953
|
+
i + 1,
|
|
954
|
+
result,
|
|
955
|
+
response,
|
|
956
|
+
args
|
|
957
|
+
);
|
|
956
958
|
}
|
|
957
959
|
return result;
|
|
958
960
|
}
|
|
959
961
|
}
|
|
960
962
|
return;
|
|
961
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* Асинхронно продолжает выполнение цепочки хуков, начиная с указанного
|
|
966
|
+
* индекса. Данный метод вызывается, когда хук в основном синхронном цикле
|
|
967
|
+
* возвращает Promise. Метод ожидает разрешения начального Promise, а затем
|
|
968
|
+
* последовательно выполняет оставшиеся хуки в асинхронном режиме, следуя
|
|
969
|
+
* той же логике прерывания (при получении значения или отправке ответа),
|
|
970
|
+
* что и основной метод.
|
|
971
|
+
*
|
|
972
|
+
* @param {Function[]} hooks
|
|
973
|
+
* @param {number} startIndex
|
|
974
|
+
* @param {Promise} initialPromise
|
|
975
|
+
* @param {import('http').ServerResponse} response
|
|
976
|
+
* @param {*} args
|
|
977
|
+
* @returns {Promise<*>}
|
|
978
|
+
*/
|
|
979
|
+
async _continueHooksInvocationAsync(hooks, startIndex, initialPromise, response, args) {
|
|
980
|
+
let result = await initialPromise;
|
|
981
|
+
if (isResponseSent(response)) {
|
|
982
|
+
return response;
|
|
983
|
+
}
|
|
984
|
+
if (result != null) {
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
for (let i = startIndex; i < hooks.length; i++) {
|
|
988
|
+
result = await hooks[i](...args);
|
|
989
|
+
if (isResponseSent(response)) {
|
|
990
|
+
return response;
|
|
991
|
+
}
|
|
992
|
+
if (result != null) {
|
|
993
|
+
return result;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
962
998
|
};
|
|
963
999
|
__name(_HookInvoker, "HookInvoker");
|
|
964
1000
|
var HookInvoker = _HookInvoker;
|
|
@@ -1043,6 +1079,7 @@ var HttpMethod = {
|
|
|
1043
1079
|
PATCH: "PATCH",
|
|
1044
1080
|
DELETE: "DELETE"
|
|
1045
1081
|
};
|
|
1082
|
+
var DEFAULT_META = Object.freeze({});
|
|
1046
1083
|
var _Route = class _Route extends import_js_debug.Debuggable {
|
|
1047
1084
|
/**
|
|
1048
1085
|
* Definition.
|
|
@@ -1094,7 +1131,7 @@ var _Route = class _Route extends import_js_debug.Debuggable {
|
|
|
1094
1131
|
* @returns {object}
|
|
1095
1132
|
*/
|
|
1096
1133
|
get meta() {
|
|
1097
|
-
return this._definition.meta;
|
|
1134
|
+
return this._definition.meta || DEFAULT_META;
|
|
1098
1135
|
}
|
|
1099
1136
|
/**
|
|
1100
1137
|
* Getter of the handler.
|
|
@@ -1118,7 +1155,6 @@ var _Route = class _Route extends import_js_debug.Debuggable {
|
|
|
1118
1155
|
validateRouteDefinition(routeDef);
|
|
1119
1156
|
this._definition = cloneDeep(routeDef);
|
|
1120
1157
|
this._definition.method = this._definition.method.toUpperCase();
|
|
1121
|
-
this._definition.meta = this._definition.meta || {};
|
|
1122
1158
|
if (routeDef.preHandler !== void 0) {
|
|
1123
1159
|
const preHandlerHooks = [routeDef.preHandler].flat().filter(Boolean);
|
|
1124
1160
|
preHandlerHooks.forEach((hook) => {
|
|
@@ -1131,7 +1167,7 @@ var _Route = class _Route extends import_js_debug.Debuggable {
|
|
|
1131
1167
|
this._hookRegistry.addHook(RouterHookType.POST_HANDLER, hook);
|
|
1132
1168
|
});
|
|
1133
1169
|
}
|
|
1134
|
-
this.ctorDebug("
|
|
1170
|
+
this.ctorDebug("Route %s %v created.", this.method, this.path);
|
|
1135
1171
|
}
|
|
1136
1172
|
/**
|
|
1137
1173
|
* Handle request.
|
|
@@ -1142,11 +1178,7 @@ var _Route = class _Route extends import_js_debug.Debuggable {
|
|
|
1142
1178
|
handle(context) {
|
|
1143
1179
|
const debug = this.getDebuggerFor(this.handle);
|
|
1144
1180
|
const requestPath = getRequestPathname(context.request);
|
|
1145
|
-
debug(
|
|
1146
|
-
"Invoking the Route handler for the request %s %v.",
|
|
1147
|
-
this.method,
|
|
1148
|
-
requestPath
|
|
1149
|
-
);
|
|
1181
|
+
debug("Invoking route handler for %s %v.", this.method, requestPath);
|
|
1150
1182
|
return this.handler(context);
|
|
1151
1183
|
}
|
|
1152
1184
|
};
|
|
@@ -1280,7 +1312,7 @@ var _BodyParser = class _BodyParser extends DebuggableService {
|
|
|
1280
1312
|
const debug = this.getDebuggerFor(this.parse);
|
|
1281
1313
|
if (!METHODS_WITH_BODY.includes(request.method.toUpperCase())) {
|
|
1282
1314
|
debug(
|
|
1283
|
-
"Body parsing
|
|
1315
|
+
"Body parsing skipped for %s method.",
|
|
1284
1316
|
request.method.toUpperCase()
|
|
1285
1317
|
);
|
|
1286
1318
|
return;
|
|
@@ -1290,9 +1322,7 @@ var _BodyParser = class _BodyParser extends DebuggableService {
|
|
|
1290
1322
|
"$1"
|
|
1291
1323
|
);
|
|
1292
1324
|
if (!contentType) {
|
|
1293
|
-
debug(
|
|
1294
|
-
"Body parsing was skipped because the request had no content type."
|
|
1295
|
-
);
|
|
1325
|
+
debug("Body parsing skipped because no content type provided.");
|
|
1296
1326
|
return;
|
|
1297
1327
|
}
|
|
1298
1328
|
const { mediaType } = parseContentType(contentType);
|
|
@@ -1305,7 +1335,7 @@ var _BodyParser = class _BodyParser extends DebuggableService {
|
|
|
1305
1335
|
const parser = this._parsers[mediaType];
|
|
1306
1336
|
if (!parser) {
|
|
1307
1337
|
if (UNPARSABLE_MEDIA_TYPES.includes(mediaType)) {
|
|
1308
|
-
debug("Body parsing
|
|
1338
|
+
debug("Body parsing skipped for media type %v.", mediaType);
|
|
1309
1339
|
return;
|
|
1310
1340
|
}
|
|
1311
1341
|
throw createError(
|
|
@@ -1315,10 +1345,14 @@ var _BodyParser = class _BodyParser extends DebuggableService {
|
|
|
1315
1345
|
);
|
|
1316
1346
|
}
|
|
1317
1347
|
const bodyBytesLimit = this.getService(RouterOptions).requestBodyBytesLimit;
|
|
1348
|
+
debug("Fetching request body.");
|
|
1349
|
+
debug("Body limit %v bytes.", bodyBytesLimit);
|
|
1318
1350
|
return fetchRequestBody(request, bodyBytesLimit).then((rawBody) => {
|
|
1319
1351
|
if (rawBody != null) {
|
|
1352
|
+
debug("Read %v bytes.", Buffer.byteLength(rawBody, "utf8"));
|
|
1320
1353
|
return parser(rawBody);
|
|
1321
1354
|
}
|
|
1355
|
+
debug("No request body content.");
|
|
1322
1356
|
return rawBody;
|
|
1323
1357
|
});
|
|
1324
1358
|
}
|
|
@@ -1353,11 +1387,11 @@ var _QueryParser = class _QueryParser extends DebuggableService {
|
|
|
1353
1387
|
const queryKeys = Object.keys(query);
|
|
1354
1388
|
if (queryKeys.length) {
|
|
1355
1389
|
queryKeys.forEach((key) => {
|
|
1356
|
-
debug("
|
|
1390
|
+
debug("Found query parameter %v with value %v.", key, query[key]);
|
|
1357
1391
|
});
|
|
1358
1392
|
} else {
|
|
1359
1393
|
debug(
|
|
1360
|
-
"
|
|
1394
|
+
"Request %s %v had no query parameters.",
|
|
1361
1395
|
request.method,
|
|
1362
1396
|
getRequestPathname(request)
|
|
1363
1397
|
);
|
|
@@ -1383,11 +1417,11 @@ var _CookiesParser = class _CookiesParser extends DebuggableService {
|
|
|
1383
1417
|
const cookiesKeys = Object.keys(cookies);
|
|
1384
1418
|
if (cookiesKeys.length) {
|
|
1385
1419
|
cookiesKeys.forEach((key) => {
|
|
1386
|
-
debug("
|
|
1420
|
+
debug("Found cookie %v with value %v.", key, cookies[key]);
|
|
1387
1421
|
});
|
|
1388
1422
|
} else {
|
|
1389
1423
|
debug(
|
|
1390
|
-
"
|
|
1424
|
+
"Request %s %v had no cookies.",
|
|
1391
1425
|
request.method,
|
|
1392
1426
|
getRequestPathname(request)
|
|
1393
1427
|
);
|
|
@@ -1473,11 +1507,7 @@ var _RouteRegistry = class _RouteRegistry extends DebuggableService {
|
|
|
1473
1507
|
const route = new Route(routeDef);
|
|
1474
1508
|
const triePath = `${route.method}/${route.path}`;
|
|
1475
1509
|
this._trie.add(triePath, route);
|
|
1476
|
-
debug(
|
|
1477
|
-
"The route %s %v was registered.",
|
|
1478
|
-
route.method.toUpperCase(),
|
|
1479
|
-
route.path
|
|
1480
|
-
);
|
|
1510
|
+
debug("Route %s %v registered.", route.method.toUpperCase(), route.path);
|
|
1481
1511
|
return route;
|
|
1482
1512
|
}
|
|
1483
1513
|
/**
|
|
@@ -1490,7 +1520,7 @@ var _RouteRegistry = class _RouteRegistry extends DebuggableService {
|
|
|
1490
1520
|
const debug = this.getDebuggerFor(this.matchRouteByRequest);
|
|
1491
1521
|
const requestPath = getRequestPathname(request);
|
|
1492
1522
|
debug(
|
|
1493
|
-
"Matching routes
|
|
1523
|
+
"Matching routes for %s %v.",
|
|
1494
1524
|
request.method.toUpperCase(),
|
|
1495
1525
|
requestPath
|
|
1496
1526
|
);
|
|
@@ -1499,16 +1529,12 @@ var _RouteRegistry = class _RouteRegistry extends DebuggableService {
|
|
|
1499
1529
|
const resolved = this._trie.match(triePath);
|
|
1500
1530
|
if (resolved) {
|
|
1501
1531
|
const route = resolved.value;
|
|
1502
|
-
debug(
|
|
1503
|
-
"The route %s %v was matched.",
|
|
1504
|
-
route.method.toUpperCase(),
|
|
1505
|
-
route.path
|
|
1506
|
-
);
|
|
1532
|
+
debug("Matched route %s %v.", route.method.toUpperCase(), route.path);
|
|
1507
1533
|
const paramNames = Object.keys(resolved.params);
|
|
1508
1534
|
if (paramNames.length) {
|
|
1509
1535
|
paramNames.forEach((name) => {
|
|
1510
1536
|
debug(
|
|
1511
|
-
"
|
|
1537
|
+
"Found path parameter %v with value %v.",
|
|
1512
1538
|
name,
|
|
1513
1539
|
resolved.params[name]
|
|
1514
1540
|
);
|
|
@@ -1519,7 +1545,7 @@ var _RouteRegistry = class _RouteRegistry extends DebuggableService {
|
|
|
1519
1545
|
return { route, params: resolved.params };
|
|
1520
1546
|
}
|
|
1521
1547
|
debug(
|
|
1522
|
-
"No matched route for
|
|
1548
|
+
"No matched route for %s %v.",
|
|
1523
1549
|
request.method.toUpperCase(),
|
|
1524
1550
|
requestPath
|
|
1525
1551
|
);
|
|
@@ -1726,21 +1752,19 @@ var _DataSender = class _DataSender extends DebuggableService {
|
|
|
1726
1752
|
send(response, data) {
|
|
1727
1753
|
const debug = this.getDebuggerFor(this.send);
|
|
1728
1754
|
if (data === response || response.headersSent) {
|
|
1729
|
-
debug(
|
|
1730
|
-
"Response sending was skipped because its headers where sent already."
|
|
1731
|
-
);
|
|
1755
|
+
debug("Response skipped because headers already sent.");
|
|
1732
1756
|
return;
|
|
1733
1757
|
}
|
|
1734
1758
|
if (data == null) {
|
|
1735
1759
|
response.statusCode = 204;
|
|
1736
1760
|
response.end();
|
|
1737
|
-
debug("
|
|
1761
|
+
debug("Empty response sent.");
|
|
1738
1762
|
return;
|
|
1739
1763
|
}
|
|
1740
1764
|
if (isReadableStream(data)) {
|
|
1741
1765
|
response.setHeader("Content-Type", "application/octet-stream");
|
|
1742
1766
|
data.pipe(response);
|
|
1743
|
-
debug("
|
|
1767
|
+
debug("Stream response sent.");
|
|
1744
1768
|
return;
|
|
1745
1769
|
}
|
|
1746
1770
|
let debugMsg;
|
|
@@ -1750,16 +1774,16 @@ var _DataSender = class _DataSender extends DebuggableService {
|
|
|
1750
1774
|
case "number":
|
|
1751
1775
|
if (Buffer.isBuffer(data)) {
|
|
1752
1776
|
response.setHeader("content-type", "application/octet-stream");
|
|
1753
|
-
debugMsg = "
|
|
1777
|
+
debugMsg = "Buffer sent as binary data.";
|
|
1754
1778
|
} else {
|
|
1755
1779
|
response.setHeader("content-type", "application/json");
|
|
1756
|
-
debugMsg = (0, import_js_format18.format)("
|
|
1780
|
+
debugMsg = (0, import_js_format18.format)("%v sent as JSON.", toPascalCase(typeof data));
|
|
1757
1781
|
data = JSON.stringify(data);
|
|
1758
1782
|
}
|
|
1759
1783
|
break;
|
|
1760
1784
|
default:
|
|
1761
1785
|
response.setHeader("content-type", "text/plain");
|
|
1762
|
-
debugMsg = "
|
|
1786
|
+
debugMsg = "Response data sent as plain text.";
|
|
1763
1787
|
data = String(data);
|
|
1764
1788
|
break;
|
|
1765
1789
|
}
|
|
@@ -1827,7 +1851,7 @@ var _ErrorSender = class _ErrorSender extends DebuggableService {
|
|
|
1827
1851
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
1828
1852
|
response.end(JSON.stringify(body, null, 2), "utf-8");
|
|
1829
1853
|
debug(
|
|
1830
|
-
"
|
|
1854
|
+
"%s error sent for %s %v request.",
|
|
1831
1855
|
statusCode,
|
|
1832
1856
|
request.method,
|
|
1833
1857
|
getRequestPathname(request)
|
|
@@ -1846,7 +1870,7 @@ var _ErrorSender = class _ErrorSender extends DebuggableService {
|
|
|
1846
1870
|
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
1847
1871
|
response.end("404 Not Found", "utf-8");
|
|
1848
1872
|
debug(
|
|
1849
|
-
"
|
|
1873
|
+
"404 error sent for %s %v.",
|
|
1850
1874
|
request.method,
|
|
1851
1875
|
getRequestPathname(request)
|
|
1852
1876
|
);
|
|
@@ -2045,7 +2069,7 @@ var _RouterBranch = class _RouterBranch extends DebuggableService {
|
|
|
2045
2069
|
this._definition = cloneDeep(branchDef);
|
|
2046
2070
|
}
|
|
2047
2071
|
this.ctorDebug("Branch %v created.", normalizePath(branchDef.path, true));
|
|
2048
|
-
this.ctorDebug("Branch path
|
|
2072
|
+
this.ctorDebug("Branch path set to %v.", this._definition.path);
|
|
2049
2073
|
}
|
|
2050
2074
|
/**
|
|
2051
2075
|
* Define route.
|
|
@@ -2162,14 +2186,10 @@ var _TrieRouter = class _TrieRouter extends DebuggableService {
|
|
|
2162
2186
|
async _handleRequest(request, response) {
|
|
2163
2187
|
const debug = this.getDebuggerFor(this._handleRequest);
|
|
2164
2188
|
const requestPath = getRequestPathname(request);
|
|
2165
|
-
debug(
|
|
2166
|
-
"Preparing to handle an incoming request %s %v.",
|
|
2167
|
-
request.method,
|
|
2168
|
-
requestPath
|
|
2169
|
-
);
|
|
2189
|
+
debug("Handling incoming request %s %v.", request.method, requestPath);
|
|
2170
2190
|
const resolved = this.getService(RouteRegistry).matchRouteByRequest(request);
|
|
2171
2191
|
if (!resolved) {
|
|
2172
|
-
debug("No route for
|
|
2192
|
+
debug("No route found for %s %v.", request.method, requestPath);
|
|
2173
2193
|
this.getService(ErrorSender).send404(request, response);
|
|
2174
2194
|
} else {
|
|
2175
2195
|
const { route, params } = resolved;
|
|
@@ -2322,5 +2342,6 @@ var TrieRouter = _TrieRouter;
|
|
|
2322
2342
|
parseCookieString,
|
|
2323
2343
|
parseJsonBody,
|
|
2324
2344
|
toCamelCase,
|
|
2345
|
+
toPascalCase,
|
|
2325
2346
|
validateRouteDefinition
|
|
2326
2347
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import {TrieRouter, HttpMethod} from '../src/index.js';
|
|
3
|
+
|
|
4
|
+
const router = new TrieRouter();
|
|
5
|
+
|
|
6
|
+
// создание ветки маршрутизатора с адресом "api",
|
|
7
|
+
// указанный путь будет использован как префикс
|
|
8
|
+
// для маршрутов данной ветки
|
|
9
|
+
const apiBranch = router.createBranch({path: 'api'});
|
|
10
|
+
|
|
11
|
+
// определение маршрута в рамках ветки "api",
|
|
12
|
+
// маршрут будет доступен по адресу "/api/status"
|
|
13
|
+
apiBranch.defineRoute({
|
|
14
|
+
method: HttpMethod.GET,
|
|
15
|
+
path: '/status',
|
|
16
|
+
handler: () => 'API is working',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// создание экземпляра HTTP сервера
|
|
20
|
+
// и подключение обработчика запросов
|
|
21
|
+
const server = new http.Server();
|
|
22
|
+
server.on('request', router.requestListener);
|
|
23
|
+
|
|
24
|
+
// прослушивание входящих запросов
|
|
25
|
+
// на указанный адрес и порт
|
|
26
|
+
const port = 3000;
|
|
27
|
+
const host = '0.0.0.0';
|
|
28
|
+
server.listen(port, host, function () {
|
|
29
|
+
const cyan = '\x1b[36m%s\x1b[0m';
|
|
30
|
+
console.log(cyan, 'Server listening on port:', port);
|
|
31
|
+
console.log(
|
|
32
|
+
cyan,
|
|
33
|
+
'Open in browser:',
|
|
34
|
+
`http://${host}:${port}/api/status`,
|
|
35
|
+
);
|
|
36
|
+
});
|
package/package.json
CHANGED
|
@@ -121,7 +121,7 @@ export class RouterBranch extends DebuggableService {
|
|
|
121
121
|
this._definition = cloneDeep(branchDef);
|
|
122
122
|
}
|
|
123
123
|
this.ctorDebug('Branch %v created.', normalizePath(branchDef.path, true));
|
|
124
|
-
this.ctorDebug('Branch path
|
|
124
|
+
this.ctorDebug('Branch path set to %v.', this._definition.path);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
@@ -9,7 +9,11 @@ import {HookRegistry, RouterHookType} from './hook-registry.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export class HookInvoker extends DebuggableService {
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Последовательно вызывает глобальные хуки и хуки маршрута указанного
|
|
13
|
+
* типа, пока один из них не вернет отличное от undefined и null значение
|
|
14
|
+
* или не отправит HTTP-ответ. Метод выполняет хуки в синхронном режиме
|
|
15
|
+
* для улучшения производительности. Если один из хуков возвращает Promise,
|
|
16
|
+
* выполнение оставшейся части цепочки переключается в асинхронный режим.
|
|
13
17
|
*
|
|
14
18
|
* @param {Route} route
|
|
15
19
|
* @param {string} hookType
|
|
@@ -87,41 +91,13 @@ export class HookInvoker extends DebuggableService {
|
|
|
87
91
|
// выполнение переключается в асинхронный режим, начиная
|
|
88
92
|
// с индекса следующего хука
|
|
89
93
|
if (isPromise(result)) {
|
|
90
|
-
return (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return response;
|
|
98
|
-
}
|
|
99
|
-
// если Promise разрешился значением отличным
|
|
100
|
-
// от undefined и null, то данное значение
|
|
101
|
-
// возвращается в качестве результата
|
|
102
|
-
if (asyncResult != null) {
|
|
103
|
-
return asyncResult;
|
|
104
|
-
}
|
|
105
|
-
// продолжение вызова хуков начиная
|
|
106
|
-
// со следующего индекса (асинхронно)
|
|
107
|
-
for (let j = i + 1; j < hooks.length; j++) {
|
|
108
|
-
// с этого момента все синхронные
|
|
109
|
-
// хуки выполняются как асинхронные
|
|
110
|
-
asyncResult = await hooks[j](...args);
|
|
111
|
-
// если ответ уже отправлен,
|
|
112
|
-
// то возвращается ServerResponse
|
|
113
|
-
if (isResponseSent(response)) {
|
|
114
|
-
return response;
|
|
115
|
-
}
|
|
116
|
-
// если хук вернул значение отличное
|
|
117
|
-
// от undefined и null, то данное значение
|
|
118
|
-
// возвращается в качестве результата
|
|
119
|
-
if (asyncResult != null) {
|
|
120
|
-
return asyncResult;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return;
|
|
124
|
-
})();
|
|
94
|
+
return this._continueHooksInvocationAsync(
|
|
95
|
+
hooks,
|
|
96
|
+
i + 1,
|
|
97
|
+
result,
|
|
98
|
+
response,
|
|
99
|
+
args,
|
|
100
|
+
);
|
|
125
101
|
}
|
|
126
102
|
// если синхронный хук вернул значение отличное
|
|
127
103
|
// от undefined и null, то данное значение
|
|
@@ -133,4 +109,61 @@ export class HookInvoker extends DebuggableService {
|
|
|
133
109
|
// и не вернули значения
|
|
134
110
|
return;
|
|
135
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Асинхронно продолжает выполнение цепочки хуков, начиная с указанного
|
|
115
|
+
* индекса. Данный метод вызывается, когда хук в основном синхронном цикле
|
|
116
|
+
* возвращает Promise. Метод ожидает разрешения начального Promise, а затем
|
|
117
|
+
* последовательно выполняет оставшиеся хуки в асинхронном режиме, следуя
|
|
118
|
+
* той же логике прерывания (при получении значения или отправке ответа),
|
|
119
|
+
* что и основной метод.
|
|
120
|
+
*
|
|
121
|
+
* @param {Function[]} hooks
|
|
122
|
+
* @param {number} startIndex
|
|
123
|
+
* @param {Promise} initialPromise
|
|
124
|
+
* @param {import('http').ServerResponse} response
|
|
125
|
+
* @param {*} args
|
|
126
|
+
* @returns {Promise<*>}
|
|
127
|
+
*/
|
|
128
|
+
async _continueHooksInvocationAsync(
|
|
129
|
+
hooks,
|
|
130
|
+
startIndex,
|
|
131
|
+
initialPromise,
|
|
132
|
+
response,
|
|
133
|
+
args,
|
|
134
|
+
) {
|
|
135
|
+
// ожидание Promise, который был получен
|
|
136
|
+
// на предыдущем шаге (в синхронном режиме)
|
|
137
|
+
let result = await initialPromise;
|
|
138
|
+
// если ответ уже отправлен,
|
|
139
|
+
// то возвращается ServerResponse
|
|
140
|
+
if (isResponseSent(response)) {
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
143
|
+
// если Promise разрешился значением отличным
|
|
144
|
+
// от undefined и null, то данное значение
|
|
145
|
+
// возвращается в качестве результата
|
|
146
|
+
if (result != null) {
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
// продолжение вызова хуков начиная
|
|
150
|
+
// со следующего индекса (асинхронно)
|
|
151
|
+
for (let i = startIndex; i < hooks.length; i++) {
|
|
152
|
+
// с этого момента все синхронные
|
|
153
|
+
// хуки выполняются как асинхронные
|
|
154
|
+
result = await hooks[i](...args);
|
|
155
|
+
// если ответ уже отправлен,
|
|
156
|
+
// то возвращается ServerResponse
|
|
157
|
+
if (isResponseSent(response)) {
|
|
158
|
+
return response;
|
|
159
|
+
}
|
|
160
|
+
// если хук вернул значение отличное
|
|
161
|
+
// от undefined и null, то данное значение
|
|
162
|
+
// возвращается в качестве результата
|
|
163
|
+
if (result != null) {
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
136
169
|
}
|