@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 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` включает вывод логов.
@@ -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
- * Invoke and continue until value received.
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 (async () => {
938
- let asyncResult = await result;
939
- if (isResponseSent(response)) {
940
- return response;
941
- }
942
- if (asyncResult != null) {
943
- return asyncResult;
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("A new route %s %v was created.", this.method, this.path);
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 was skipped for the %s request.",
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 was skipped for %v.", mediaType);
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("The query parameter %v had the value %v.", key, query[key]);
1390
+ debug("Found query parameter %v with value %v.", key, query[key]);
1357
1391
  });
1358
1392
  } else {
1359
1393
  debug(
1360
- "The request %s %v had no query parameters.",
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("The cookie %v had the value %v.", key, cookies[key]);
1420
+ debug("Found cookie %v with value %v.", key, cookies[key]);
1387
1421
  });
1388
1422
  } else {
1389
1423
  debug(
1390
- "The request %s %v had no cookies.",
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 with the request %s %v.",
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
- "The path parameter %v had the value %v.",
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 the request %s %v.",
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("The empty response was sent.");
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("The stream response was sent.");
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 = "The Buffer was sent as binary data.";
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)("The %v was sent as JSON.", typeof data);
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 = "The response data was sent as plain text.";
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
- "The %s error was sent for the request %s %v.",
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
- "The 404 error was sent for the request %s %v.",
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 was %v.", this._definition.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 the request %s %v.", request.method, requestPath);
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
  });
@@ -1,6 +1,5 @@
1
1
  import http from 'http';
2
- import {TrieRouter} from '../src/index.js';
3
- import {HttpMethod} from '../src/route.js';
2
+ import {TrieRouter, HttpMethod} from '../src/index.js';
4
3
 
5
4
  const router = new TrieRouter();
6
5
 
@@ -1,6 +1,5 @@
1
1
  import http from 'http';
2
- import {TrieRouter} from '../src/index.js';
3
- import {HttpMethod} from '../src/route.js';
2
+ import {TrieRouter, HttpMethod} from '../src/index.js';
4
3
 
5
4
  const router = new TrieRouter();
6
5
 
@@ -1,6 +1,5 @@
1
1
  import http from 'http';
2
- import {TrieRouter} from '../src/index.js';
3
- import {HttpMethod} from '../src/route.js';
2
+ import {TrieRouter, HttpMethod} from '../src/index.js';
4
3
 
5
4
  const router = new TrieRouter();
6
5
 
@@ -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
+ });
@@ -1,6 +1,5 @@
1
1
  import http from 'http';
2
- import {TrieRouter} from '../src/index.js';
3
- import {HttpMethod} from '../src/route.js';
2
+ import {TrieRouter, HttpMethod} from '../src/index.js';
4
3
 
5
4
  const router = new TrieRouter();
6
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e22m4u/js-trie-router",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "HTTP маршрутизатор для Node.js на основе префиксного дерева",
5
5
  "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
6
6
  "license": "MIT",
@@ -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 was %v.', this._definition.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
- * Invoke and continue until value received.
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 (async () => {
91
- // ожидание Promise, который был получен
92
- // на предыдущем шаге (в синхронном режиме)
93
- let asyncResult = await result;
94
- // если ответ уже отправлен,
95
- // то возвращается ServerResponse
96
- if (isResponseSent(response)) {
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
  }