Просмотр исходного кода

refactor: improve hooks invocation performance

e22m4u 3 недель назад
Родитель
Сommit
67fa11f26e
3 измененных файлов с 144 добавлено и 77 удалено
  1. 49 34
      dist/cjs/index.cjs
  2. 68 41
      src/hooks/hook-invoker.js
  3. 27 2
      src/hooks/hook-invoker.spec.js

+ 49 - 34
dist/cjs/index.cjs

@@ -76,6 +76,25 @@ var import_js_debug = require("@e22m4u/js-debug");
 // src/hooks/hook-invoker.js
 var import_js_format13 = require("@e22m4u/js-format");
 
+// src/debuggable-service.js
+var import_js_service = require("@e22m4u/js-service");
+var MODULE_DEBUG_NAMESPACE = "jsTrieRouter";
+var _DebuggableService = class _DebuggableService extends import_js_service.DebuggableService {
+  /**
+   * Constructor.
+   *
+   * @param {ServiceContainer} container
+   */
+  constructor(container = void 0) {
+    super(container, {
+      namespace: MODULE_DEBUG_NAMESPACE,
+      noEnvironmentNamespace: true
+    });
+  }
+};
+__name(_DebuggableService, "DebuggableService");
+var DebuggableService = _DebuggableService;
+
 // src/utils/clone-deep.js
 function cloneDeep(value) {
   if (value == null || typeof value !== "object") {
@@ -739,25 +758,6 @@ var _HookRegistry = class _HookRegistry {
 __name(_HookRegistry, "HookRegistry");
 var HookRegistry = _HookRegistry;
 
-// src/debuggable-service.js
-var import_js_service = require("@e22m4u/js-service");
-var MODULE_DEBUG_NAMESPACE = "jsTrieRouter";
-var _DebuggableService = class _DebuggableService extends import_js_service.DebuggableService {
-  /**
-   * Constructor.
-   *
-   * @param {ServiceContainer} container
-   */
-  constructor(container = void 0) {
-    super(container, {
-      namespace: MODULE_DEBUG_NAMESPACE,
-      noEnvironmentNamespace: true
-    });
-  }
-};
-__name(_DebuggableService, "DebuggableService");
-var DebuggableService = _DebuggableService;
-
 // src/hooks/hook-invoker.js
 var _HookInvoker = class _HookInvoker extends DebuggableService {
   /**
@@ -788,31 +788,46 @@ var _HookInvoker = class _HookInvoker extends DebuggableService {
         response
       );
     }
+    if (isResponseSent(response)) {
+      return response;
+    }
     const hooks = [
       ...this.getService(HookRegistry).getHooks(hookType),
       ...route.hookRegistry.getHooks(hookType)
     ];
     let result = void 0;
-    for (const hook of hooks) {
+    for (let i = 0; i < hooks.length; i++) {
+      const hook = hooks[i];
+      result = hook(...args);
       if (isResponseSent(response)) {
-        result = response;
-        break;
+        return response;
       }
-      if (result == null) {
-        result = hook(...args);
-      } else if (isPromise(result)) {
-        result = result.then((prevVal) => {
-          if (isResponseSent(response)) {
+      if (result != null) {
+        if (isPromise(result)) {
+          return (async () => {
+            let asyncResult = await result;
+            if (isResponseSent(response)) {
+              return response;
+            }
+            if (asyncResult != null) {
+              return asyncResult;
+            }
+            for (let j = i + 1; j < hooks.length; j++) {
+              asyncResult = await hooks[j](...args);
+              if (isResponseSent(response)) {
+                return response;
+              }
+              if (asyncResult != null) {
+                return asyncResult;
+              }
+            }
             return;
-          }
-          if (prevVal != null) return prevVal;
-          return hook(...args);
-        });
-      } else {
-        break;
+          })();
+        }
+        return result;
       }
     }
-    return result;
+    return;
   }
 };
 __name(_HookInvoker, "HookInvoker");

+ 68 - 41
src/hooks/hook-invoker.js

@@ -1,10 +1,8 @@
 import {Route} from '../route.js';
 import {Errorf} from '@e22m4u/js-format';
-import {isPromise} from '../utils/index.js';
-import {HookRegistry} from './hook-registry.js';
-import {isResponseSent} from '../utils/index.js';
-import {RouterHookType} from './hook-registry.js';
 import {DebuggableService} from '../debuggable-service.js';
+import {isPromise, isResponseSent} from '../utils/index.js';
+import {HookRegistry, RouterHookType} from './hook-registry.js';
 
 /**
  * Hook invoker.
@@ -49,6 +47,11 @@ export class HookInvoker extends DebuggableService {
         response,
       );
     }
+    // если ответ уже отправлен,
+    // то возвращается ServerResponse
+    if (isResponseSent(response)) {
+      return response;
+    }
     // так как хуки роута выполняются
     // после глобальных, то объединяем
     // их в данной последовательности
@@ -56,48 +59,72 @@ export class HookInvoker extends DebuggableService {
       ...this.getService(HookRegistry).getHooks(hookType),
       ...route.hookRegistry.getHooks(hookType),
     ];
-    // последовательный вызов хуков будет прерван,
-    // если один из них вернет значение (или Promise)
-    // отличное от "undefined" и "null"
     let result = undefined;
-    for (const hook of hooks) {
-      // если ответ уже был отправлен,
-      // то завершаем обход
+    // итерация по хукам выполняется по индексу,
+    // чтобы знать, с какого места продолжать
+    // в асинхронном режиме
+    for (let i = 0; i < hooks.length; i++) {
+      const hook = hooks[i];
+      // вызов хука выполняется
+      // в синхронном режиме
+      result = hook(...args);
+      // если ответ уже отправлен,
+      // то возвращается ServerResponse
       if (isResponseSent(response)) {
-        result = response;
-        break;
-      }
-      // если выполняется первый хук, или предыдущий
-      // хук вернул пустое значение, то выполняем
-      // следующий, записывая возвращаемое
-      // значение в результат
-      if (result == null) {
-        result = hook(...args);
+        return response;
       }
-      // если какой-то из предыдущих хуков вернул
-      // Promise, то последующие значения будут
-      // оборачиваться именно им
-      else if (isPromise(result)) {
-        result = result.then(prevVal => {
-          // если ответ уже был отправлен,
-          // то останавливаем выполнение
-          if (isResponseSent(response)) {
+      // если синхронный вызов хука вернул значение отличное
+      // от undefined и null, то требуется проверить данное
+      // значение для коррекции режима вызова оставшихся хуков
+      if (result != null) {
+        // если синхронный вызов хука вернул Promise, то дальнейшее
+        // выполнение переключается в асинхронный режим, начиная
+        // с индекса следующего хука
+        if (isPromise(result)) {
+          return (async () => {
+            // ожидание Promise, который был получен
+            // на предыдущем шаге (в синхронном режиме)
+            let asyncResult = await result;
+            // если ответ уже отправлен,
+            // то возвращается ServerResponse
+            if (isResponseSent(response)) {
+              return response;
+            }
+            // если Promise разрешился значением отличным
+            // от undefined и null, то данное значение
+            // возвращается в качестве результата
+            if (asyncResult != null) {
+              return asyncResult;
+            }
+            // продолжение вызова хуков начиная
+            // со следующего индекса (асинхронно)
+            for (let j = i + 1; j < hooks.length; j++) {
+              // с этого момента все синхронные
+              // хуки выполняются как асинхронные
+              asyncResult = await hooks[j](...args);
+              // если ответ уже отправлен,
+              // то возвращается ServerResponse
+              if (isResponseSent(response)) {
+                return response;
+              }
+              // если хук вернул значение отличное
+              // от undefined и null, то данное значение
+              // возвращается в качестве результата
+              if (asyncResult != null) {
+                return asyncResult;
+              }
+            }
             return;
-          }
-          // если предыдущий Promise вернул значение
-          // отличное от "undefined" и "null",
-          // то завершаем обход
-          if (prevVal != null) return prevVal;
-          return hook(...args);
-        });
-      }
-      // если предыдущий хук вернул значение
-      // отличное от "undefined" и "null",
-      // то завершаем обход
-      else {
-        break;
+          })();
+        }
+        // если синхронный хук вернул значение отличное
+        // от undefined и null, то данное значение
+        // возвращается в качестве результата
+        return result;
       }
     }
-    return result;
+    // все хуки были синхронными
+    // и не вернули значения
+    return;
   }
 }

+ 27 - 2
src/hooks/hook-invoker.spec.js

@@ -234,7 +234,32 @@ describe('HookInvoker', function () {
       ]);
     });
 
-    it('stops global hooks invocation and returns the given response if it was sent', function () {
+    it('returns the given response and should not call hooks if the response is already sent', function () {
+      const s = new HookInvoker();
+      const res = createResponseMock();
+      res._headersSent = true;
+      s.getService(HookRegistry).addHook(RouterHookType.PRE_HANDLER, () => {
+        throw new Error('Should not be called');
+      });
+      const route = new Route({
+        method: HttpMethod.GET,
+        path: '/',
+        preHandler: [
+          () => {
+            throw new Error('Should not be called');
+          },
+        ],
+        handler: () => undefined,
+      });
+      const result = s.invokeAndContinueUntilValueReceived(
+        route,
+        RouterHookType.PRE_HANDLER,
+        res,
+      );
+      expect(result).to.be.eq(res);
+    });
+
+    it('stops global hooks invocation and returns the given response if it is already sent', function () {
       const s = new HookInvoker();
       const order = [];
       const res = createResponseMock();
@@ -270,7 +295,7 @@ describe('HookInvoker', function () {
       expect(order).to.be.eql(['globalHook1', 'globalHook2']);
     });
 
-    it('stops route hooks invocation and returns the given response if it was sent', function () {
+    it('stops route hooks invocation and returns the given response if it is already sent', function () {
       const s = new HookInvoker();
       const order = [];
       const res = createResponseMock();