@fuzionx/framework 0.1.44 → 0.1.46

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.
@@ -11,7 +11,7 @@ import path from 'node:path';
11
11
  import { promises as fs } from 'node:fs';
12
12
  import cluster from 'node:cluster';
13
13
  import Config from './Config.js';
14
- import Context from './Context.js';
14
+ import Context, { createContext, createContextFromReq } from './Context.js';
15
15
  import Router from '../http/Router.js';
16
16
  import ModelRegistry from '../database/ModelRegistry.js';
17
17
  import ErrorHandler from '../http/ErrorHandler.js';
@@ -461,12 +461,15 @@ export default class Application {
461
461
  // WS proxy 연결 (WsHandler에서 this.app.ws 사용)
462
462
  if (!this.ws) this.ws = this._coreApp.ws;
463
463
 
464
- this._registerBridgeRoutes(this._coreApp);
464
+ // ⚡ Raw dispatch 구축: Bridge에 직접 라우트 등록 + handlerId 디스패치 테이블
465
+ // Core의 createReq/createRes/runMiddlewareChain 파이프라인 완전 우회
466
+ this._buildRawDispatch();
465
467
 
466
468
  // WsHandler → Bridge WS 이벤트 연결
467
469
  this._registerWsHandlers(this._coreApp);
468
470
 
469
- this._coreApp.listen(port, callback);
471
+ // Raw handler로 서버 시작 (Core _handleRequest 우회)
472
+ this._coreApp.listen(port, callback, (rawReq) => this._handleRawRequest(rawReq));
470
473
  // Bridge가 자체 graceful shutdown 처리 → Framework 중복 등록 불필요
471
474
  } else {
472
475
  // Bridge 미설치 (테스트 환경)
@@ -622,6 +625,170 @@ export default class Application {
622
625
  }
623
626
  }
624
627
 
628
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
629
+ // Raw Dispatch — Core 파이프라인 우회 (최적화 v3)
630
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
631
+
632
+ /**
633
+ * Bridge에 직접 라우트 등록 + handlerId 기반 디스패치 테이블 구축
634
+ *
635
+ * ⚡ 최적화 핵심:
636
+ * - Core의 Router/createReq/createRes/runMiddlewareChain 전체 우회
637
+ * - Bridge에 직접 registerRoute → handlerId 획득
638
+ * - handlerId → { resolvedHandler, middlewareFns, appName } 직접 매핑
639
+ * - _handleRawRequest에서 O(1) 배열 인덱스로 디스패치
640
+ *
641
+ * @private
642
+ */
643
+ _buildRawDispatch() {
644
+ this._initControllers();
645
+
646
+ // handlerId는 1부터 순차 발급 → 배열 인덱스로 O(1) 조회 (Map 대신)
647
+ this._rawDispatch = [];
648
+ // Express :param → Rust {param} 변환
649
+ const toRustPath = (p) => p.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
650
+ let nextId = 1;
651
+
652
+ // 멀티앱 Host 디스패치용: "METHOD:path" → Map<appName, entry>
653
+ const routeGroups = new Map();
654
+
655
+ for (const [appName, appEntry] of this._appRegistry) {
656
+ for (const route of appEntry.router.getRoutes()) {
657
+ const key = `${route.method}:${route.path}`;
658
+ if (!routeGroups.has(key)) routeGroups.set(key, new Map());
659
+
660
+ // 미들웨어 체인 사전 구성 (부트 시 1회)
661
+ const middlewareFns = [...this._globalMiddleware];
662
+ if (route.middleware?.length) {
663
+ for (const mw of route.middleware) {
664
+ if (typeof mw === 'function') {
665
+ middlewareFns.push(mw);
666
+ } else if (typeof mw === 'string') {
667
+ const fn = this.resolveMiddleware(mw, appName);
668
+ if (fn) middlewareFns.push(fn);
669
+ }
670
+ }
671
+ }
672
+
673
+ // ⚡ 핸들러 사전 바인딩 (런타임 Map 조회 0)
674
+ let resolvedHandler;
675
+ if (route.handler?.__handler__) {
676
+ const instance = appEntry.controllerCache.get(route.handler.controller);
677
+ if (instance && typeof instance[route.handler.method] === 'function') {
678
+ resolvedHandler = instance[route.handler.method].bind(instance);
679
+ }
680
+ } else if (typeof route.handler === 'function') {
681
+ resolvedHandler = route.handler;
682
+ }
683
+
684
+ routeGroups.get(key).set(appName, { middlewareFns, resolvedHandler });
685
+ }
686
+ }
687
+
688
+ // Bridge에 직접 라우트 등록 + 디스패치 테이블 구축
689
+ for (const [key, appMap] of routeGroups) {
690
+ const colonIdx = key.indexOf(':');
691
+ const method = key.slice(0, colonIdx);
692
+ const routePath = key.slice(colonIdx + 1);
693
+ const rustPath = toRustPath(routePath);
694
+ const handlerId = nextId++;
695
+
696
+ // Rust Bridge에 직접 등록 (Core Router 우회)
697
+ this._bridge.registerRoute(method, rustPath, handlerId);
698
+
699
+ // 단일 앱 최적화: Host 디스패치 불필요
700
+ const isSingleApp = appMap.size === 1;
701
+ const singleEntry = isSingleApp ? appMap.values().next().value : null;
702
+ const singleAppName = isSingleApp ? appMap.keys().next().value : null;
703
+
704
+ // 배열 인덱스로 O(1) 조회 (Map.get 대신)
705
+ this._rawDispatch[handlerId] = {
706
+ appMap,
707
+ isSingleApp,
708
+ singleEntry,
709
+ singleAppName,
710
+ };
711
+ }
712
+
713
+ // 라우트 등록 완료 → Rust 라우터 빌드
714
+ this._bridge.buildRoutes();
715
+ }
716
+
717
+ /**
718
+ * Raw request 핸들러 — Core 파이프라인 완전 우회
719
+ *
720
+ * ⚡ 실행 경로:
721
+ * Rust → rawReq → createContext(rawReq) → resolvedHandler(ctx) → _toFusionResponse()
722
+ *
723
+ * Core 경유 시 제거되는 비용:
724
+ * - createReq: 14 프로퍼티 할당 (제거)
725
+ * - createRes: Object.keys + delete + 4 할당 (제거)
726
+ * - runMiddlewareChain: 클로저 생성 + 호출 (제거)
727
+ * - ctx → res scalar 복사 (제거)
728
+ *
729
+ * @param {object} rawReq - Bridge에서 직접 전달받는 원시 요청
730
+ * @returns {object|{async:true}} Fusion 응답 객체 또는 async 마커
731
+ * @private
732
+ */
733
+ _handleRawRequest(rawReq) {
734
+ const dispatch = this._rawDispatch[rawReq.handlerId];
735
+ if (!dispatch) {
736
+ // handlerId 미등록 → 404
737
+ return { status: 404, body: '{"error":"Not Found"}', headers: { 'Content-Type': 'application/json' } };
738
+ }
739
+
740
+ // ── 앱 결정 (단일 앱: O(0), 멀티앱: Host 기반) ──
741
+ let appName, entry;
742
+ if (dispatch.isSingleApp) {
743
+ appName = dispatch.singleAppName;
744
+ entry = dispatch.singleEntry;
745
+ } else {
746
+ const host = rawReq.headers?.host || '';
747
+ appName = this._resolveApp(host, rawReq.headers);
748
+ entry = dispatch.appMap.get(appName);
749
+ if (!entry) entry = dispatch.appMap.get(this._getDefaultAppName());
750
+ if (!entry) entry = dispatch.appMap.values().next().value;
751
+ }
752
+
753
+ const { middlewareFns, resolvedHandler } = entry;
754
+
755
+ // ── rawReq → Context 직접 생성 (Core req/res 우회, 할당 최소화) ──
756
+ // ⚡ async 지원 시 클로저 포인터 공유 문제가 발생하므로 매 요청마다 새 컨텍스트 생성 (할당 비용 극소)
757
+ const ctx = createContext(rawReq, this, false);
758
+ ctx.appName = appName;
759
+
760
+ // ── Sync-first 실행 ──
761
+ let chainResult;
762
+ try {
763
+ if (middlewareFns.length === 0) {
764
+ // ⚡ 미들웨어 없음 → 핸들러 직접 호출
765
+ chainResult = resolvedHandler(ctx);
766
+ } else {
767
+ chainResult = this._executeSyncChain(middlewareFns, ctx, resolvedHandler);
768
+ }
769
+ } catch (err) {
770
+ this._handleContextError(err, ctx);
771
+ return ctx._toFusionResponse();
772
+ }
773
+
774
+ // ── sync 완료: 직접 Fusion 응답 반환 (중간 복사 0) ──
775
+ if (!chainResult || typeof chainResult.then !== 'function') {
776
+ return ctx._toFusionResponse();
777
+ }
778
+
779
+ // ── async 경로: Promise 대기 후 원래 Context로 응답 전송 ──
780
+ chainResult
781
+ .then(() => {
782
+ this._sendContextResponse(rawReq.requestId, ctx);
783
+ })
784
+ .catch((err) => {
785
+ this._handleContextError(err, ctx);
786
+ this._sendContextResponse(rawReq.requestId, ctx);
787
+ });
788
+
789
+ return { async: true };
790
+ }
791
+
625
792
  /**
626
793
  * 프레임워크 라우트 → Bridge 라우트 변환 (Host 기반 앱 디스패치)
627
794
  *
@@ -634,7 +801,7 @@ export default class Application {
634
801
  _registerBridgeRoutes(coreApp) {
635
802
  this._initControllers();
636
803
 
637
- // 디스패치 테이블: "METHOD:path" → Map<appName, { route, middlewareFns }>
804
+ // 디스패치 테이블: "METHOD:path" → Map<appName, { route, middlewareFns, resolvedHandler }>
638
805
  const dispatch = new Map();
639
806
 
640
807
  for (const [appName, appEntry] of this._appRegistry) {
@@ -642,12 +809,10 @@ export default class Application {
642
809
  const key = `${route.method}:${route.path}`;
643
810
  if (!dispatch.has(key)) dispatch.set(key, new Map());
644
811
 
645
- // 미들웨어 체인 사전 구성
812
+ // 미들웨어 체인 사전 구성 (배열 구성은 부트 시 1회만 실행)
646
813
  const middlewareFns = [...this._globalMiddleware];
647
814
  if (route.middleware?.length) {
648
815
  for (const mw of route.middleware) {
649
- // 함수면 직접 사용 (built-in: auth(), cors() 등)
650
- // 문자열이면 resolveMiddleware로 클래스 기반 조회
651
816
  if (typeof mw === 'function') {
652
817
  middlewareFns.push(mw);
653
818
  } else if (typeof mw === 'string') {
@@ -657,7 +822,20 @@ export default class Application {
657
822
  }
658
823
  }
659
824
 
660
- dispatch.get(key).set(appName, { route, middlewareFns });
825
+ // 핸들러 사전 바인딩 — 런타임 Map 조회 2회 → 직접 함수 호출 (부트 시 1회)
826
+ let resolvedHandler;
827
+ if (route.handler?.__handler__) {
828
+ // 컨트롤러 메서드: appRegistry + controllerCache Map 조회를 부트 시 해결
829
+ const instance = appEntry.controllerCache.get(route.handler.controller);
830
+ if (instance && typeof instance[route.handler.method] === 'function') {
831
+ resolvedHandler = instance[route.handler.method].bind(instance);
832
+ }
833
+ } else if (typeof route.handler === 'function') {
834
+ // 일반 함수 핸들러: 그대로 사용
835
+ resolvedHandler = route.handler;
836
+ }
837
+
838
+ dispatch.get(key).set(appName, { route, middlewareFns, resolvedHandler });
661
839
  }
662
840
  }
663
841
 
@@ -725,88 +903,94 @@ export default class Application {
725
903
  }
726
904
 
727
905
  /**
728
- * Host 기반 디스패치 핸들러 생성
906
+ * Host 기반 디스패치 핸들러 생성 (최적화 v2)
907
+ *
908
+ * ⚡ 최적화 핵심:
909
+ * - createContextFromReq: 중간 객체 리터럴 제거 (Core req 직접 참조)
910
+ * - 헤더 공유: ctx._headers = res._headers → _copyToRes 헤더 루프 제거
911
+ * - 사전 바인딩: resolvedHandler → 런타임 Map 조회 0회
912
+ * - 빈 미들웨어: middlewareFns.length === 0 → next() 함수 생성 스킵
729
913
  *
730
- * 동일 path에 대해 Host 헤더로 앱을 결정하고
731
- * 해당 앱의 route + middleware 체인을 실행.
914
+ * sync/async 분기:
915
+ * - 미들웨어와 핸들러가 모두 sync scalar 3개 복사 → Rust 즉시 write
916
+ * - async 감지 → { async: true } + sendAsyncResponse
732
917
  *
733
- * @param {Map<string, {route, middlewareFns}>} appMap - appName → {route, middlewareFns}
918
+ * @param {Map<string, {route, middlewareFns, resolvedHandler}>} appMap
734
919
  * @returns {Function} Bridge 핸들러
735
920
  * @private
736
921
  */
737
922
  _createDispatchHandler(appMap) {
923
+ // 단일 앱 최적화: appMap에 1개만 있으면 Host 라우팅 스킵
924
+ const isSingleApp = appMap.size === 1;
925
+ const singleEntry = isSingleApp ? appMap.values().next().value : null;
926
+ const singleAppName = isSingleApp ? appMap.keys().next().value : null;
927
+
738
928
  return (req, res) => {
739
- // Host 헤더에서 앱 결정 (reverse proxy: X-Forwarded-Host 우선)
740
- const host = req.headers?.host || req.headers?.Host || '';
741
- const appName = this._resolveApp(host, req.headers);
742
-
743
- // 해당 앱의 route+middleware 찾기 (없으면 기본 앱)
744
- let entry = appMap.get(appName);
745
- if (!entry) {
746
- // 매칭 앱에 이 경로가 없으면 기본 앱 시도
747
- const defaultApp = this._getDefaultAppName();
748
- entry = appMap.get(defaultApp);
749
- }
750
- if (!entry) {
751
- // 어떤 앱에도 없으면 첫 번째 앱
752
- entry = appMap.values().next().value;
929
+ // ── 앱 결정 (단일 앱이면 O(0), 멀티앱이면 Host 기반) ──
930
+ let appName, entry;
931
+ if (isSingleApp) {
932
+ appName = singleAppName;
933
+ entry = singleEntry;
934
+ } else {
935
+ const host = req.headers?.host || '';
936
+ appName = this._resolveApp(host, req.headers);
937
+ entry = appMap.get(appName);
938
+ if (!entry) entry = appMap.get(this._getDefaultAppName());
939
+ if (!entry) entry = appMap.values().next().value;
753
940
  }
754
941
 
755
- const { route, middlewareFns } = entry;
756
-
757
- // rawReq → Context
758
- const ctx = new Context({
759
- method: req.method,
760
- url: req.url,
761
- path: req.path,
762
- query: req.query,
763
- params: req.params,
764
- headers: req.headers,
765
- body: req.body || req.json,
766
- remoteIp: req.ip,
767
- handlerId: req.handlerId,
768
- requestId: req.requestId,
769
- sessionId: req.sessionId,
770
- session: req.session?._data || {},
771
- files: req.files || null,
772
- formFields: req.formFields || null,
773
- }, this);
942
+ const { middlewareFns, resolvedHandler } = entry;
774
943
 
944
+ // ── Core req/res에서 직접 Context 생성 (중간 객체 할당 0) ──
945
+ const ctx = createContextFromReq(req, res, this, true);
775
946
  ctx.appName = appName;
776
947
 
777
- // async 체인 실행 → sendAsyncResponse
778
- const promise = this._executeChain(middlewareFns, route, ctx);
779
-
780
- promise.then(() => {
781
- const response = ctx.toResponse();
782
- const contentType = response.headers?.['Content-Type'] || 'application/json';
783
- const headerParts = [];
784
- if (response.headers) {
785
- for (const [k, v] of Object.entries(response.headers)) {
786
- if (k !== 'Content-Type') headerParts.push(`${k}: ${v}`);
787
- }
788
- }
789
- const extraHeaders = headerParts.length > 0 ? headerParts.join('\r\n') + '\r\n' : '';
790
- try {
791
- this._coreApp._bridge.sendAsyncResponse(
792
- req.requestId, response.status,
793
- response.body || '', contentType, extraHeaders,
794
- );
795
- } catch (e) {
796
- console.error('[fuzionx] sendAsyncResponse failed:', e.message);
948
+ // ── Sync-first 실행 ──
949
+ let chainResult;
950
+ try {
951
+ if (middlewareFns.length === 0) {
952
+ // 미들웨어 없음 → next() 함수 생성 스킵, 핸들러 직접 호출
953
+ chainResult = resolvedHandler(ctx);
954
+ } else {
955
+ chainResult = this._executeSyncChain(middlewareFns, ctx, resolvedHandler);
797
956
  }
798
- }).catch((err) => {
799
- console.error('[fuzionx] Handler error:', err.message || err);
800
- try {
801
- this._coreApp._bridge.sendAsyncResponse(
802
- req.requestId, 500,
803
- JSON.stringify({ error: err.message || 'Internal Server Error' }),
804
- 'application/json', '',
805
- );
806
- } catch (e) {
807
- console.error('[fuzionx] sendAsyncResponse error failed:', e.message);
957
+ } catch (err) {
958
+ this._handleContextError(err, ctx);
959
+ // 헤더는 이미 공유 → scalar만 복사
960
+ res._statusCode = ctx._statusCode;
961
+ res._body = ctx._body;
962
+ res._sent = ctx._sent;
963
+ return;
964
+ }
965
+
966
+ // ── sync 완료: scalar 3개만 복사 (헤더는 이미 공유) ──
967
+ if (!chainResult || typeof chainResult.then !== 'function') {
968
+ res._statusCode = ctx._statusCode;
969
+ res._body = ctx._body;
970
+ res._sent = ctx._sent;
971
+ // Set-Cookie 병합 (공유 헤더에 직접 기록)
972
+ if (ctx._setCookies.length > 0) {
973
+ ctx._headers['Set-Cookie'] = ctx._setCookies;
808
974
  }
809
- });
975
+ return; // Core가 res._toFusionResponse() 호출
976
+ }
977
+
978
+ // ── async 경로: 새 Context 인스턴스로 Promise 처리 ──
979
+ const asyncCtx = createContextFromReq(req, null, this, false);
980
+ asyncCtx.appName = appName;
981
+ asyncCtx._statusCode = ctx._statusCode;
982
+ asyncCtx._body = ctx._body;
983
+ asyncCtx._sent = ctx._sent;
984
+ asyncCtx.user = ctx.user;
985
+
986
+ chainResult
987
+ .then(() => {
988
+ this._sendContextResponse(req.requestId, asyncCtx);
989
+ })
990
+ .catch((err) => {
991
+ this._handleContextError(err, asyncCtx);
992
+ this._sendContextResponse(req.requestId, asyncCtx);
993
+ });
810
994
 
811
995
  return { async: true };
812
996
  };
@@ -836,8 +1020,7 @@ export default class Application {
836
1020
  }
837
1021
 
838
1022
  return (req, res) => {
839
- // rawReq Context 변환
840
- const ctx = new Context({
1023
+ const ctx = createContext({
841
1024
  method: req.method,
842
1025
  url: req.url,
843
1026
  path: req.path,
@@ -852,43 +1035,18 @@ export default class Application {
852
1035
  session: req.session?._data || {},
853
1036
  files: req.files || null,
854
1037
  formFields: req.formFields || null,
855
- }, this);
1038
+ }, this, false);
856
1039
 
857
- // 앱 이름 주입
858
1040
  ctx.appName = appName;
859
1041
 
860
- // Framework → async 체인 실행 후 직접 sendAsyncResponse
1042
+ // async 체인 실행 후 sendAsyncResponse
861
1043
  const promise = this._executeChain(middlewareFns, route, ctx);
862
1044
 
863
1045
  promise.then(() => {
864
- const response = ctx.toResponse();
865
- const contentType = response.headers?.['Content-Type'] || 'application/json';
866
- const headerParts = [];
867
- if (response.headers) {
868
- for (const [k, v] of Object.entries(response.headers)) {
869
- if (k !== 'Content-Type') headerParts.push(`${k}: ${v}`);
870
- }
871
- }
872
- const extraHeaders = headerParts.length > 0 ? headerParts.join('\r\n') + '\r\n' : '';
873
- try {
874
- this._coreApp._bridge.sendAsyncResponse(
875
- req.requestId, response.status,
876
- response.body || '', contentType, extraHeaders,
877
- );
878
- } catch (e) {
879
- console.error('[fuzionx] sendAsyncResponse failed:', e.message);
880
- }
1046
+ this._sendContextResponse(req.requestId, ctx);
881
1047
  }).catch((err) => {
882
- console.error('[fuzionx] Handler error:', err.message || err);
883
- try {
884
- this._coreApp._bridge.sendAsyncResponse(
885
- req.requestId, 500,
886
- JSON.stringify({ error: err.message || 'Internal Server Error' }),
887
- 'application/json', '',
888
- );
889
- } catch (e) {
890
- console.error('[fuzionx] sendAsyncResponse error failed:', e.message);
891
- }
1048
+ this._handleContextError(err, ctx);
1049
+ this._sendContextResponse(req.requestId, ctx);
892
1050
  });
893
1051
 
894
1052
  return { async: true };
@@ -896,64 +1054,193 @@ export default class Application {
896
1054
  }
897
1055
 
898
1056
  /**
899
- * 미들웨어 체인 + 핸들러 실행
1057
+ * Sync-first 미들웨어 체인 + 핸들러 실행 (최적화 v2)
1058
+ *
1059
+ * flat loop으로 실행. 모든 미들웨어와 핸들러가 sync이면 Promise 생성 0.
1060
+ * async 미들웨어/핸들러 감지 시 Promise를 반환하여 호출자에서 async 처리.
1061
+ *
1062
+ * @param {Function[]} middlewareFns - 미들웨어 함수 배열
1063
+ * @param {object} ctx - Context 인스턴스
1064
+ * @param {Function} resolvedHandler - 사전 바인딩된 핸들러 함수
1065
+ * @returns {undefined|Promise} sync 완료 시 undefined, async 감지 시 Promise
900
1066
  * @private
901
1067
  */
902
- async _executeChain(middlewareFns, route, ctx) {
1068
+ _executeSyncChain(middlewareFns, ctx, resolvedHandler) {
903
1069
  let idx = 0;
1070
+ const len = middlewareFns.length;
904
1071
 
905
- const next = async () => {
1072
+ /**
1073
+ * next() — sync 우선 flat 호출
1074
+ * 미들웨어가 Promise를 반환하면 체인 나머지를 async로 이어감
1075
+ */
1076
+ function next() {
906
1077
  if (ctx._sent) return;
907
- if (idx < middlewareFns.length) {
1078
+
1079
+ // 미들웨어 체인 순회
1080
+ if (idx < len) {
908
1081
  const fn = middlewareFns[idx++];
909
- await fn(ctx, next);
910
- return;
1082
+ const result = fn(ctx, next);
1083
+ // async 미들웨어 감지 → Promise 전파
1084
+ if (result && typeof result.then === 'function') {
1085
+ return result.catch((e) => {
1086
+ if (!ctx._sent) throw e;
1087
+ });
1088
+ }
1089
+ return result;
911
1090
  }
912
1091
 
913
- // 미들웨어 완료 → 핸들러 실행
914
- await this._executeHandler(route.handler, ctx);
915
- };
1092
+ // 미들웨어 완료 → 사전 바인딩된 핸들러 직접 호출 (Map 조회 0)
1093
+ return resolvedHandler(ctx);
1094
+ }
916
1095
 
917
- try {
918
- await next();
919
- } catch (err) {
920
- // 커스텀 에러 핸들러 체크 (useError로 등록)
921
- let handled = false;
922
- for (const { path: ePath, handler } of this._errorHandlers) {
923
- if (!ePath || ctx.path?.startsWith(ePath)) {
924
- try {
925
- await handler(err, ctx);
926
- handled = true;
927
- break;
928
- } catch { /* 커스텀 핸들러 실패 시 기본으로 */ }
929
- }
930
- }
931
- if (!handled) this._errorHandler.handle(err, ctx);
1096
+ return next();
1097
+ }
1098
+
1099
+ /**
1100
+ * Framework ctx 결과를 Core res 객체에 복사
1101
+ *
1102
+ * sync 경로에서 Core의 res._toFusionResponse() 올바른 응답을 생성하도록:
1103
+ * - _statusCode, _body, _headers, _sent를 Core res에 기록
1104
+ * - Set-Cookie 헤더 병합
1105
+ *
1106
+ * @param {object} ctx - Framework Context 인스턴스
1107
+ * @param {object} res - Core res 객체
1108
+ * @private
1109
+ */
1110
+ _copyToRes(ctx, res) {
1111
+ res._statusCode = ctx._statusCode;
1112
+ res._body = ctx._body;
1113
+ res._sent = ctx._sent;
1114
+ // 헤더 복사: ctx._headers → res._headers
1115
+ const ctxHeaders = ctx._headers;
1116
+ const resHeaders = res._headers;
1117
+ const keys = Object.keys(ctxHeaders);
1118
+ for (let i = 0; i < keys.length; i++) {
1119
+ resHeaders[keys[i]] = ctxHeaders[keys[i]];
1120
+ }
1121
+ // Set-Cookie 헤더 병합
1122
+ if (ctx._setCookies && ctx._setCookies.length > 0) {
1123
+ resHeaders['Set-Cookie'] = ctx._setCookies;
932
1124
  }
933
- // 응답은 _createBridgeHandler의 .then()에서 sendAsyncResponse로 전송
934
1125
  }
935
1126
 
936
1127
  /**
937
- * 핸들러 실행 (싱글톤 컨트롤러 or 일반 함수)
1128
+ * 핸들러 실행 (sync 우선)
1129
+ *
1130
+ * 컨트롤러 메서드 또는 일반 함수를 실행.
1131
+ * 반환값이 Promise이면 async 경로로 전파.
1132
+ *
1133
+ * @param {object|Function} handler - __handler__ 디스크립터 또는 일반 함수
1134
+ * @param {object} ctx - Context 인스턴스
1135
+ * @returns {undefined|Promise}
938
1136
  * @private
939
1137
  */
940
- async _executeHandler(handler, ctx) {
1138
+ _executeHandlerSync(handler, ctx) {
941
1139
  if (handler?.__handler__) {
942
1140
  // 앱별 싱글톤 컨트롤러 메서드 참조
943
1141
  const appEntry = this._appRegistry.get(ctx.appName);
944
1142
  const instance = appEntry?.controllerCache?.get(handler.controller);
945
1143
  if (instance && typeof instance[handler.method] === 'function') {
946
- await instance[handler.method](ctx);
947
- return;
1144
+ return instance[handler.method](ctx);
948
1145
  }
949
1146
  }
950
1147
 
951
1148
  // 일반 함수 핸들러
952
1149
  if (typeof handler === 'function') {
953
- await handler(ctx);
1150
+ return handler(ctx);
954
1151
  }
955
1152
  }
956
1153
 
1154
+ /**
1155
+ * Context 에러 핸들링 — ctx에 에러 응답 설정
1156
+ * @param {Error} err - 발생한 에러
1157
+ * @param {object} ctx - Context 인스턴스
1158
+ * @private
1159
+ */
1160
+ _handleContextError(err, ctx) {
1161
+ // 커스텀 에러 핸들러 체크 (useError로 등록)
1162
+ for (const { path: ePath, handler } of this._errorHandlers) {
1163
+ if (!ePath || ctx.path?.startsWith(ePath)) {
1164
+ try {
1165
+ handler(err, ctx);
1166
+ return;
1167
+ } catch { /* 커스텀 핸들러 실패 시 기본으로 */ }
1168
+ }
1169
+ }
1170
+ this._errorHandler.handle(err, ctx);
1171
+ }
1172
+
1173
+ /**
1174
+ * Context 기반 async 응답 전송 — sendAsyncResponse 호출
1175
+ *
1176
+ * Object.keys for 루프로 헤더 조립 (Object.entries 대신 → 배열 할당 제거)
1177
+ *
1178
+ * @param {number} requestId - Bridge 요청 ID
1179
+ * @param {object} ctx - Context 인스턴스
1180
+ * @private
1181
+ */
1182
+ _sendContextResponse(requestId, ctx) {
1183
+ const response = ctx._toFusionResponse();
1184
+ const contentType = response.headers?.['Content-Type'] || 'application/json';
1185
+
1186
+ // 헤더 조립: Object.keys for 루프 (Object.entries 대신 → 배열 생성 제거)
1187
+ let extraHeaders = '';
1188
+ if (response.headers) {
1189
+ const keys = Object.keys(response.headers);
1190
+ for (let i = 0; i < keys.length; i++) {
1191
+ const k = keys[i];
1192
+ if (k !== 'Content-Type') {
1193
+ extraHeaders += k + ': ' + response.headers[k] + '\r\n';
1194
+ }
1195
+ }
1196
+ }
1197
+
1198
+ try {
1199
+ this._coreApp._bridge.sendAsyncResponse(
1200
+ requestId, response.status,
1201
+ response.body || '', contentType, extraHeaders,
1202
+ );
1203
+ } catch (e) {
1204
+ console.error('[fuzionx] sendAsyncResponse failed:', e.message);
1205
+ }
1206
+ }
1207
+
1208
+ // ── 래거시 호환: async _executeChain (deprecated용 _createBridgeHandler에서 사용) ──
1209
+
1210
+ /**
1211
+ * 미들웨어 체인 + 핸들러 실행 (async 래거시)
1212
+ * @deprecated _executeSyncChain을 사용하세요. 이 메서드는 하위 호환용.
1213
+ * @private
1214
+ */
1215
+ async _executeChain(middlewareFns, route, ctx) {
1216
+ let idx = 0;
1217
+
1218
+ const next = async () => {
1219
+ if (ctx._sent) return;
1220
+ if (idx < middlewareFns.length) {
1221
+ const fn = middlewareFns[idx++];
1222
+ await fn(ctx, next);
1223
+ return;
1224
+ }
1225
+ await this._executeHandlerSync(route.handler, ctx);
1226
+ };
1227
+
1228
+ try {
1229
+ await next();
1230
+ } catch (err) {
1231
+ this._handleContextError(err, ctx);
1232
+ }
1233
+ }
1234
+
1235
+ /**
1236
+ * 핸들러 실행 (async 래거시 — 하위 호환)
1237
+ * @deprecated _executeHandlerSync를 사용하세요.
1238
+ * @private
1239
+ */
1240
+ async _executeHandler(handler, ctx) {
1241
+ return this._executeHandlerSync(handler, ctx);
1242
+ }
1243
+
957
1244
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
958
1245
  // Graceful Shutdown
959
1246
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━