@fuzionx/framework 0.1.45 → 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.
- package/lib/core/Application.js +425 -138
- package/lib/core/Context.js +540 -236
- package/lib/middleware/auth.js +1 -1
- package/lib/middleware/csrf.js +1 -1
- package/lib/middleware/session.js +5 -4
- package/package.json +2 -2
package/lib/core/Application.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
731
|
-
*
|
|
914
|
+
* sync/async 분기:
|
|
915
|
+
* - 미들웨어와 핸들러가 모두 sync → scalar 3개 복사 → Rust 즉시 write
|
|
916
|
+
* - async 감지 → { async: true } + sendAsyncResponse
|
|
732
917
|
*
|
|
733
|
-
* @param {Map<string, {route, middlewareFns}>} appMap
|
|
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
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
entry = appMap.get(
|
|
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 {
|
|
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
|
-
//
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1042
|
+
// async 체인 실행 후 sendAsyncResponse
|
|
861
1043
|
const promise = this._executeChain(middlewareFns, route, ctx);
|
|
862
1044
|
|
|
863
1045
|
promise.then(() => {
|
|
864
|
-
|
|
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
|
-
|
|
883
|
-
|
|
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
|
-
|
|
1068
|
+
_executeSyncChain(middlewareFns, ctx, resolvedHandler) {
|
|
903
1069
|
let idx = 0;
|
|
1070
|
+
const len = middlewareFns.length;
|
|
904
1071
|
|
|
905
|
-
|
|
1072
|
+
/**
|
|
1073
|
+
* next() — sync 우선 flat 호출
|
|
1074
|
+
* 미들웨어가 Promise를 반환하면 체인 나머지를 async로 이어감
|
|
1075
|
+
*/
|
|
1076
|
+
function next() {
|
|
906
1077
|
if (ctx._sent) return;
|
|
907
|
-
|
|
1078
|
+
|
|
1079
|
+
// 미들웨어 체인 순회
|
|
1080
|
+
if (idx < len) {
|
|
908
1081
|
const fn = middlewareFns[idx++];
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
915
|
-
}
|
|
1092
|
+
// 미들웨어 완료 → 사전 바인딩된 핸들러 직접 호출 (Map 조회 0)
|
|
1093
|
+
return resolvedHandler(ctx);
|
|
1094
|
+
}
|
|
916
1095
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
* 핸들러 실행 (
|
|
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
|
-
|
|
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
|
-
|
|
947
|
-
return;
|
|
1144
|
+
return instance[handler.method](ctx);
|
|
948
1145
|
}
|
|
949
1146
|
}
|
|
950
1147
|
|
|
951
1148
|
// 일반 함수 핸들러
|
|
952
1149
|
if (typeof handler === 'function') {
|
|
953
|
-
|
|
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
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|