@fuzionx/core 0.1.0

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.
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ # ruxy reload — YAML 설정 핫 리로드
3
+ #
4
+ # 사용법:
5
+ # ./ruxy-reload.sh # 이름으로 PID 검색
6
+ # ./ruxy-reload.sh <PID> # 직접 PID 지정
7
+ #
8
+ # SIGHUP을 보내면 서버가 YAML을 다시 읽고 보안 설정을 재적용합니다.
9
+ # 서버 재시작 없이 CORS, rate-limit, HSTS, CSP, IP filter, 세션 설정 반영.
10
+
11
+ set -e
12
+
13
+ if [ -n "$1" ]; then
14
+ PID="$1"
15
+ else
16
+ # node 프로세스 중 ruxy 관련 검색
17
+ PID=$(pgrep -f "ruxy|fusion" | head -1)
18
+ fi
19
+
20
+ if [ -z "$PID" ]; then
21
+ echo "❌ ruxy 프로세스를 찾을 수 없습니다."
22
+ echo " 사용법: $0 <PID>"
23
+ exit 1
24
+ fi
25
+
26
+ echo "🔄 Sending SIGHUP to PID $PID..."
27
+ kill -HUP "$PID"
28
+ echo "✅ Config reload signal sent."
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createApp, RuxyApp } from './lib/app.js';
package/lib/app.js ADDED
@@ -0,0 +1,251 @@
1
+ import { createRequire } from 'module';
2
+ import path from 'path';
3
+ import { Router } from './router.js';
4
+ import { runMiddlewareChain } from './middleware.js';
5
+ import { createReq, createRes } from './context.js';
6
+ import { createSession } from './session.js';
7
+ import { createI18n } from './i18n.js';
8
+ import { createWs } from './ws.js';
9
+ import { createCrypto } from './crypto.js';
10
+ import { createLogger, interceptConsole } from './logger.js';
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ /**
15
+ * fuzionx-bridge native module 로드.
16
+ * 순서: @fuzionx/bridge (npm) → fuzionx-bridge (로컬 dev) → .node 파일 직접
17
+ */
18
+ function loadBridge() {
19
+ // 1. npm 설치된 @fuzionx/bridge
20
+ try { return require('@fuzionx/bridge'); } catch (_) {}
21
+
22
+ // 2. 로컬 개발 환경 (file: 의존성)
23
+ try { return require('fuzionx-bridge'); } catch (_) {}
24
+
25
+ // 3. .node 파일 직접 로드
26
+ const dir = path.dirname(new URL(import.meta.url).pathname);
27
+ const variants = [
28
+ path.resolve(dir, '../../fuzionx-bridge.linux-x64-gnu.node'),
29
+ path.resolve(dir, '../../../crates/fuzionx-bridge/fuzionx-bridge.linux-x64-gnu.node'),
30
+ ];
31
+ for (const p of variants) {
32
+ try { return require(p); } catch (_) {}
33
+ }
34
+ throw new Error('fuzionx-bridge native module을 찾을 수 없습니다. npm install @fuzionx/core를 실행해 주세요.');
35
+ }
36
+
37
+ export class RuxyApp {
38
+ /**
39
+ * @param {object} options
40
+ * @param {string} options.config - YAML 설정 파일 경로
41
+ * @param {number} [options.port=3000] - 서버 포트
42
+ */
43
+ constructor(options = {}) {
44
+ this._bridge = loadBridge();
45
+ this._router = new Router(this._bridge);
46
+ this._middlewares = [];
47
+ this._errorHandlers = [];
48
+ this._options = options;
49
+ this._booted = false;
50
+ this._port = options.port || 3000;
51
+
52
+ // 헬퍼 모듈
53
+ this.crypto = createCrypto(this._bridge);
54
+ this._ws = null; // lazy — get ws() 에서 초기화
55
+ this.i18n = null; // boot 후 초기화
56
+ }
57
+
58
+ /** YAML boot — 설정 파일 로드 */
59
+ _boot() {
60
+ if (this._booted) return;
61
+ const configPath = this._options.config;
62
+ if (!configPath) {
63
+ throw new Error('config 옵션이 필요합니다 (YAML 파일 경로)');
64
+ }
65
+ this._bridge.bootSystem({ configPath: path.resolve(configPath) });
66
+ this._booted = true;
67
+ this.i18n = createI18n(this._bridge);
68
+
69
+ // 통합 로거 초기화
70
+ this.logger = createLogger(this._bridge);
71
+
72
+ // console.* 가로채기 (YAML intercept_console 설정에 따름)
73
+ const cfg = this._bridge.getConfig();
74
+ if (cfg.interceptConsole !== false) {
75
+ this._restoreConsole = interceptConsole(this._bridge);
76
+ }
77
+
78
+ // 앱 로그 파일 초기화 (YAML logging.file.enabled 시)
79
+ if (cfg.logFilePath) {
80
+ try {
81
+ this._bridge.initAppFileLogger(cfg.logFilePath);
82
+ } catch (e) {
83
+ // 파일 로거 실패해도 서버는 계속 동작
84
+ this.logger.warn(`앱 파일 로거 초기화 실패: ${e.message}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ /** WebSocket 매니저 (lazy init — listen() 전에도 사용 가능) */
90
+ get ws() {
91
+ this._boot();
92
+ if (!this._ws) {
93
+ this._ws = createWs(this._bridge);
94
+ }
95
+ return this._ws;
96
+ }
97
+
98
+ /** 서버 설정값 */
99
+ get config() {
100
+ this._boot();
101
+ return this._bridge.getConfig();
102
+ }
103
+
104
+ /** 앱 커스텀 설정값 */
105
+ get appConfig() {
106
+ this._boot();
107
+ return this._bridge.getAppConfig();
108
+ }
109
+
110
+ /** ASP (Alloy Stealth Protocol) 설정값 */
111
+ get aspConfig() {
112
+ this._boot();
113
+ return JSON.parse(this._bridge.getAspConfig());
114
+ }
115
+
116
+ /**
117
+ * 템플릿 파일 렌더링 (bridge에서 파일 읽기 + {{key}} 치환)
118
+ * @param {string} filePath - 템플릿 파일 경로
119
+ * @param {object} [context={}] - 치환 변수
120
+ * @returns {string} 렌더링된 HTML
121
+ */
122
+ render(filePath, context = {}) {
123
+ this._boot();
124
+ return this._bridge.renderTemplateFile(filePath, JSON.stringify(context));
125
+ }
126
+
127
+ // ── HTTP Method 라우트 등록 ──
128
+
129
+ get(routePath, ...handlers) { this._router.add('GET', routePath, handlers); return this; }
130
+ post(routePath, ...handlers) { this._router.add('POST', routePath, handlers); return this; }
131
+ put(routePath, ...handlers) { this._router.add('PUT', routePath, handlers); return this; }
132
+ delete(routePath, ...handlers) { this._router.add('DELETE', routePath, handlers); return this; }
133
+ patch(routePath, ...handlers) { this._router.add('PATCH', routePath, handlers); return this; }
134
+
135
+ /**
136
+ * 미들웨어 등록
137
+ * @param {string|Function} pathOrHandler
138
+ * @param {...Function} handlers
139
+ */
140
+ use(pathOrHandler, ...handlers) {
141
+ if (typeof pathOrHandler === 'function') {
142
+ const all = [pathOrHandler, ...handlers];
143
+ for (const h of all) {
144
+ const target = h.length === 4 ? this._errorHandlers : this._middlewares;
145
+ target.push({ path: null, handler: h });
146
+ }
147
+ } else {
148
+ for (const h of handlers) {
149
+ const target = h.length === 4 ? this._errorHandlers : this._middlewares;
150
+ target.push({ path: pathOrHandler, handler: h });
151
+ }
152
+ }
153
+ return this;
154
+ }
155
+
156
+ /**
157
+ * Fusion 서버 시작
158
+ * @param {number} [port]
159
+ * @param {Function} [callback]
160
+ */
161
+ listen(port, callback) {
162
+ if (typeof port === 'function') { callback = port; port = this._port; }
163
+ port = port || this._port;
164
+
165
+ this._boot();
166
+ this._router.build();
167
+
168
+ this._bridge.startFusionServer(port, (rawReq) => this._handleRequest(rawReq));
169
+
170
+ // ── Graceful Shutdown (SIGTERM/SIGINT) ──
171
+ let shutdownCalled = false;
172
+ const gracefulShutdown = (signal) => {
173
+ if (shutdownCalled) return;
174
+ shutdownCalled = true;
175
+ console.log(`[ruxy] ${signal} received — graceful shutdown`);
176
+ this._bridge.stopFusionServer();
177
+ setTimeout(() => process.exit(0), 2000);
178
+ };
179
+ process.once('SIGTERM', () => gracefulShutdown('SIGTERM'));
180
+ process.once('SIGINT', () => gracefulShutdown('SIGINT'));
181
+
182
+ // ── Hot Reload (SIGHUP) ──
183
+ process.on('SIGHUP', () => {
184
+ try {
185
+ this._bridge.reloadConfig();
186
+ this._bridge.reloadFusionConfig();
187
+ console.log('[ruxy] SIGHUP — config hot-reload complete');
188
+ } catch (e) {
189
+ console.error('[ruxy] SIGHUP reload failed:', e.message);
190
+ }
191
+ });
192
+
193
+ // ── Idle Cleanup은 Rust uv_timer에서 자동 실행 (YAML websocket.check_interval/timeout) ──
194
+
195
+ // ── Error Recovery ──
196
+ process.on('uncaughtException', (err) => {
197
+ console.error('[ruxy] uncaughtException:', err.message);
198
+ });
199
+ process.on('unhandledRejection', (reason) => {
200
+ console.error('[ruxy] unhandledRejection:', reason);
201
+ });
202
+
203
+ if (callback) setTimeout(() => callback(), 100);
204
+ return this;
205
+ }
206
+
207
+ /** 요청 처리 — 미들웨어 체인 → 라우트 핸들러 */
208
+ _handleRequest(rawReq) {
209
+ const req = createReq(rawReq, this);
210
+ const res = createRes();
211
+
212
+ const route = this._router.getHandler(rawReq.handlerId);
213
+
214
+ // 경로 매치 미들웨어 필터
215
+ const applicable = this._middlewares.filter(m =>
216
+ m.path === null || req.url.startsWith(m.path)
217
+ );
218
+
219
+ const chain = applicable.map(m => m.handler);
220
+ if (route) {
221
+ chain.push(...route);
222
+ } else {
223
+ chain.push((_req, _res) => {
224
+ _res.status(404).json({ error: 'Not Found', path: _req.url });
225
+ });
226
+ }
227
+
228
+ try {
229
+ runMiddlewareChain(chain, req, res);
230
+ } catch (err) {
231
+ const errHandlers = this._errorHandlers.filter(m =>
232
+ m.path === null || req.url.startsWith(m.path)
233
+ );
234
+ if (errHandlers.length > 0) {
235
+ try {
236
+ for (const eh of errHandlers) eh.handler(err, req, res, () => {});
237
+ } catch (_) {
238
+ res.status(500).json({ error: 'Internal Server Error' });
239
+ }
240
+ } else {
241
+ res.status(500).json({ error: err.message || 'Internal Server Error' });
242
+ }
243
+ }
244
+
245
+ return res._toFusionResponse();
246
+ }
247
+ }
248
+
249
+ export function createApp(options) {
250
+ return new RuxyApp(options);
251
+ }
package/lib/context.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Context — Fusion raw req 객체를 Express 스타일 req/res로 변환.
3
+ */
4
+
5
+ /**
6
+ * Request 래퍼 생성
7
+ * @param {object} rawReq - Fusion 콜백의 raw 요청 객체
8
+ * @param {object} app - RuxyApp 인스턴스
9
+ */
10
+ export function createReq(rawReq, app) {
11
+ const req = {
12
+ method: rawReq.method,
13
+ url: rawReq.url,
14
+ path: rawReq.url,
15
+ query: rawReq.query || {},
16
+ params: rawReq.params || {},
17
+ headers: rawReq.headers || {},
18
+ ip: rawReq.remoteIp || '',
19
+ body: rawReq.body || '',
20
+ handlerId: rawReq.handlerId,
21
+ requestId: rawReq.requestId,
22
+ sessionId: rawReq.sessionId || null,
23
+ _app: app,
24
+ _rawSession: rawReq.session || null,
25
+ };
26
+
27
+ // req.json — lazy JSON 파싱
28
+ let _jsonCache = undefined;
29
+ Object.defineProperty(req, 'json', {
30
+ get() {
31
+ if (_jsonCache === undefined) {
32
+ try {
33
+ _jsonCache = req.body ? JSON.parse(req.body) : null;
34
+ } catch (_) {
35
+ _jsonCache = null;
36
+ }
37
+ }
38
+ return _jsonCache;
39
+ },
40
+ });
41
+
42
+ // req.session — 세션 헬퍼
43
+ const bridge = app._bridge;
44
+ req.session = {
45
+ _id: rawReq.sessionId || null,
46
+ _data: rawReq.session || {},
47
+
48
+ get(key) {
49
+ if (key) return this._data[key] || null;
50
+ return { ...this._data };
51
+ },
52
+
53
+ set(key, value) {
54
+ this._data[key] = String(value);
55
+ if (this._id) {
56
+ bridge.sessionSet(this._id, { ...this._data });
57
+ }
58
+ },
59
+
60
+ destroy() {
61
+ if (this._id) {
62
+ bridge.sessionDestroy(this._id);
63
+ this._data = {};
64
+ }
65
+ },
66
+
67
+ renew() {
68
+ if (this._id) {
69
+ return bridge.sessionRenew(this._id);
70
+ }
71
+ return null;
72
+ },
73
+ };
74
+
75
+ // req.t — i18n 번역
76
+ req.t = (key, defaultValue) => {
77
+ if (!app.i18n) return key;
78
+ // Accept-Language 또는 기본 로케일
79
+ const locale = req.headers['accept-language']?.split(',')[0]?.split('-')[0] || 'en';
80
+ const result = app.i18n.translate(locale, key);
81
+ if (result == null && defaultValue != null) {
82
+ app.i18n.updateMissing(key, defaultValue);
83
+ return defaultValue;
84
+ }
85
+ return result || defaultValue || key;
86
+ };
87
+
88
+ return req;
89
+ }
90
+
91
+ /**
92
+ * Response 래퍼 생성
93
+ */
94
+ export function createRes() {
95
+ const res = {
96
+ _statusCode: 200,
97
+ _body: '',
98
+ _headers: {},
99
+ _sent: false,
100
+
101
+ status(code) {
102
+ res._statusCode = code;
103
+ return res;
104
+ },
105
+
106
+ json(data) {
107
+ res._body = JSON.stringify(data);
108
+ res._headers['Content-Type'] = 'application/json';
109
+ res._sent = true;
110
+ return res;
111
+ },
112
+
113
+ send(text) {
114
+ res._body = String(text);
115
+ res._sent = true;
116
+ return res;
117
+ },
118
+
119
+ html(content) {
120
+ res._body = String(content);
121
+ res._headers['Content-Type'] = 'text/html; charset=utf-8';
122
+ res._sent = true;
123
+ return res;
124
+ },
125
+
126
+ redirect(url, code = 302) {
127
+ res._statusCode = code;
128
+ res._headers['Location'] = url;
129
+ res._body = '';
130
+ res._sent = true;
131
+ return res;
132
+ },
133
+
134
+ header(key, value) {
135
+ res._headers[key] = value;
136
+ return res;
137
+ },
138
+
139
+ /** Fusion 응답 포맷으로 변환 */
140
+ _toFusionResponse() {
141
+ return {
142
+ status: res._statusCode,
143
+ body: res._body,
144
+ headers: res._headers,
145
+ };
146
+ },
147
+ };
148
+
149
+ return res;
150
+ }
package/lib/crypto.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Crypto — fuzionx-bridge ASP crypto N-API를 래핑하는 헬퍼.
3
+ * app.crypto로 접근.
4
+ */
5
+
6
+ export function createCrypto(bridge) {
7
+ return {
8
+ /** UUID v4 생성 */
9
+ uuid: () => bridge.cryptoUuid(),
10
+
11
+ /** MD5 해시 */
12
+ md5: (input) => bridge.cryptoMd5(input),
13
+
14
+ /** SHA-256 해시 */
15
+ sha256: (input) => bridge.cryptoSha256(input),
16
+
17
+ /** AES-256-GCM 암호화 */
18
+ encrypt: (key, plaintext) => bridge.cryptoEncryptAes(key, plaintext),
19
+
20
+ /** AES-256-GCM 복호화 */
21
+ decrypt: (key, ciphertext) => bridge.cryptoDecryptAes(key, ciphertext),
22
+
23
+ /** Nibble 난독화 암호화 (string key → SHA256 wrapping) */
24
+ encryptCustom: (key, plaintext) => bridge.cryptoEncryptCustom(key, plaintext),
25
+
26
+ /** Nibble 난독화 복호화 (string key → SHA256 wrapping) */
27
+ decryptCustom: (key, ciphertext) => bridge.cryptoDecryptCustom(key, ciphertext),
28
+
29
+ /** ASP Transport 암호화 (hex key → raw 32바이트 직접 사용) */
30
+ encryptTransport: (keyHex, plaintext) => bridge.cryptoEncryptTransport(keyHex, plaintext),
31
+
32
+ /** ASP Transport 복호화 (hex key → raw 32바이트 직접 사용) */
33
+ decryptTransport: (keyHex, ciphertext) => bridge.cryptoDecryptTransport(keyHex, ciphertext),
34
+
35
+ /** UA Slice 추출 */
36
+ getUaSlice: (userAgent, timestamp) => bridge.cryptoGetUaSlice(userAgent, timestamp),
37
+
38
+ /** ASP Transport Key 파생 */
39
+ deriveTransportKey: (masterSecret, domain, path, uaSlice, timestamp) =>
40
+ bridge.cryptoDeriveTransportKey(masterSecret, domain, path, uaSlice, timestamp),
41
+ };
42
+ }
package/lib/i18n.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * I18n — fuzionx-bridge i18n N-API를 래핑하는 헬퍼.
3
+ * app.i18n으로 접근, req.t()는 context.js에서 구현.
4
+ */
5
+
6
+ export function createI18n(bridge) {
7
+ return {
8
+ /** 번역 조회 */
9
+ translate: (locale, key) => bridge.i18NTranslate(locale, key),
10
+
11
+ /** 로케일 목록 */
12
+ get locales() {
13
+ return bridge.i18NGetLocales();
14
+ },
15
+
16
+ /** 현재 로케일 변경 */
17
+ setLocale: (locale) => bridge.i18NSetLocale(locale),
18
+
19
+ /** 누락 키 업데이트 (auto_complete) */
20
+ updateMissing: (key, value) => bridge.i18NUpdateMissingKey(key, value),
21
+
22
+ /** auto_complete 활성화 여부 */
23
+ get autoComplete() {
24
+ return bridge.i18NIsAutoComplete();
25
+ },
26
+
27
+ /**
28
+ * SSR 템플릿 렌더링
29
+ * @param {string} template - Tera 템플릿 문자열
30
+ * @param {object} context - 템플릿 변수
31
+ * @param {string} [locale='en'] - 로케일
32
+ */
33
+ render: (template, context = {}, locale = 'en') => {
34
+ return bridge.ssrRenderString(template, context, locale);
35
+ },
36
+ };
37
+ }
package/lib/logger.js ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @fileoverview Ruxy Bridge 통합 로거.
3
+ *
4
+ * Rust tracing과 Node.js console을 일원화한다.
5
+ * - createLogger(bridge): app.logger 객체 생성
6
+ * - interceptConsole(bridge): console.* 가로채기
7
+ *
8
+ * @module logger
9
+ */
10
+
11
+ import { format } from 'node:util';
12
+
13
+ /**
14
+ * JS 호출 스택에서 실제 호출 위치(파일:라인)를 추출한다.
15
+ * error/warn 레벨에서만 사용 (new Error().stack 비용 고려).
16
+ *
17
+ * @param {number} depth - 스택 프레임 깊이 (기본 3: Error → getCallSite → logWarn → 실제 호출)
18
+ * @returns {string} 파일:라인 문자열 (예: "server.js:42") 또는 빈 문자열
19
+ */
20
+ function getCallSite(depth = 3) {
21
+ const stack = new Error().stack;
22
+ if (!stack) return '';
23
+
24
+ const lines = stack.split('\n');
25
+ // depth번째 라인이 실제 호출자
26
+ const line = lines[depth] || '';
27
+
28
+ // " at Object.<anonymous> (/app/server.js:42:5)" 패턴
29
+ const matchParen = line.match(/\((.+):(\d+):\d+\)/);
30
+ if (matchParen) return `${matchParen[1]}:${matchParen[2]}`;
31
+
32
+ // " at /app/server.js:42:5" 패턴 (익명 호출)
33
+ const matchDirect = line.match(/at (.+):(\d+):\d+/);
34
+ if (matchDirect) return `${matchDirect[1]}:${matchDirect[2]}`;
35
+
36
+ return '';
37
+ }
38
+
39
+ /**
40
+ * Rust N-API bridge를 통한 로거 객체를 생성한다.
41
+ *
42
+ * @param {object} bridge - fuzionx-bridge N-API 모듈
43
+ * @returns {{ info: Function, warn: Function, error: Function, debug: Function }}
44
+ *
45
+ * @example
46
+ * const logger = createLogger(bridge);
47
+ * logger.info('서버 시작');
48
+ * logger.error('DB 연결 실패'); // → [server.js:42] DB 연결 실패
49
+ */
50
+ export function createLogger(bridge) {
51
+ return {
52
+ /**
53
+ * INFO 레벨 로그.
54
+ * @param {...any} args - 로그 메시지 (util.format 지원)
55
+ */
56
+ info(...args) {
57
+ bridge.logInfo('app', format(...args));
58
+ },
59
+
60
+ /**
61
+ * WARN 레벨 로그 (호출 위치 자동 추출).
62
+ * @param {...any} args - 로그 메시지
63
+ */
64
+ warn(...args) {
65
+ const location = getCallSite(2); // Error → warn → 호출자
66
+ bridge.logWarn('app', format(...args), location || undefined);
67
+ },
68
+
69
+ /**
70
+ * ERROR 레벨 로그 (호출 위치 자동 추출).
71
+ * @param {...any} args - 로그 메시지
72
+ */
73
+ error(...args) {
74
+ const location = getCallSite(2); // Error → error → 호출자
75
+ bridge.logError('app', format(...args), location || undefined);
76
+ },
77
+
78
+ /**
79
+ * DEBUG 레벨 로그.
80
+ * @param {...any} args - 로그 메시지
81
+ */
82
+ debug(...args) {
83
+ bridge.logDebug('app', format(...args));
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * console.log/warn/error/debug를 가로채서 Rust tracing으로 전달한다.
90
+ *
91
+ * @param {object} bridge - fuzionx-bridge N-API 모듈
92
+ * @returns {Function} 원래 console을 복원하는 함수
93
+ *
94
+ * @example
95
+ * const restore = interceptConsole(bridge);
96
+ * console.log('hello'); // → tracing::info [console] hello
97
+ * restore(); // 원래 console 복원
98
+ */
99
+ export function interceptConsole(bridge) {
100
+ // 원본 보존
101
+ const orig = {
102
+ log: console.log,
103
+ info: console.info,
104
+ warn: console.warn,
105
+ error: console.error,
106
+ debug: console.debug,
107
+ };
108
+
109
+ console.log = (...args) => {
110
+ bridge.logInfo('console', format(...args));
111
+ };
112
+
113
+ console.info = (...args) => {
114
+ bridge.logInfo('console', format(...args));
115
+ };
116
+
117
+ console.warn = (...args) => {
118
+ const location = getCallSite(2);
119
+ bridge.logWarn('console', format(...args), location || undefined);
120
+ };
121
+
122
+ console.error = (...args) => {
123
+ const location = getCallSite(2);
124
+ bridge.logError('console', format(...args), location || undefined);
125
+ };
126
+
127
+ console.debug = (...args) => {
128
+ bridge.logDebug('console', format(...args));
129
+ };
130
+
131
+ /**
132
+ * 원래 console 함수를 복원한다.
133
+ */
134
+ return function restoreConsole() {
135
+ console.log = orig.log;
136
+ console.info = orig.info;
137
+ console.warn = orig.warn;
138
+ console.error = orig.error;
139
+ console.debug = orig.debug;
140
+ };
141
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Middleware — Express 스타일 동기 미들웨어 체인 엔진.
3
+ *
4
+ * Fusion 콜백은 동기 응답을 기대하므로 async를 지원하지 않음.
5
+ * next()를 호출하면 다음 미들웨어로 진행.
6
+ */
7
+
8
+ /**
9
+ * 미들웨어 체인 실행
10
+ * @param {Function[]} handlers - (req, res, next) 핸들러 배열
11
+ * @param {object} req
12
+ * @param {object} res
13
+ */
14
+ export function runMiddlewareChain(handlers, req, res) {
15
+ let index = 0;
16
+
17
+ function next(err) {
18
+ if (err) throw err;
19
+ if (res._sent || index >= handlers.length) return;
20
+
21
+ const handler = handlers[index++];
22
+ handler(req, res, next);
23
+ }
24
+
25
+ next();
26
+ }
package/lib/router.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Router — fuzionx-bridge의 registerRoute/buildRoutes를 래핑하는 라우트 매니저.
3
+ * Express 스타일 path를 Rust 라우터 형식({param})으로 변환.
4
+ */
5
+ export class Router {
6
+ constructor(bridge) {
7
+ this._bridge = bridge;
8
+ this._routes = []; // { method, path, handlers }
9
+ this._handlerMap = {}; // handlerId → handlers[]
10
+ this._nextId = 1;
11
+ this._built = false;
12
+ }
13
+
14
+ /**
15
+ * 라우트 추가
16
+ * @param {string} method - HTTP 메서드
17
+ * @param {string} routePath - Express 스타일 경로 (예: /api/users/:id)
18
+ * @param {Function[]} handlers - 핸들러 배열
19
+ */
20
+ add(method, routePath, handlers) {
21
+ if (this._built) {
22
+ throw new Error('라우트는 listen() 호출 전에 등록해야 합니다');
23
+ }
24
+
25
+ // Express :param → Rust {param} 변환
26
+ const rustPath = routePath.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
27
+ const handlerId = this._nextId++;
28
+
29
+ this._routes.push({ method, path: rustPath, handlerId, handlers });
30
+ this._handlerMap[handlerId] = handlers;
31
+
32
+ return handlerId;
33
+ }
34
+
35
+ /** 모든 등록된 라우트를 fuzionx-bridge에 등록하고 빌드 */
36
+ build() {
37
+ if (this._built) return;
38
+
39
+ for (const route of this._routes) {
40
+ this._bridge.registerRoute(route.method, route.path, route.handlerId);
41
+ }
42
+ this._bridge.buildRoutes();
43
+ this._built = true;
44
+ }
45
+
46
+ /**
47
+ * handlerId로 핸들러 배열 조회
48
+ * @param {number} handlerId
49
+ * @returns {Function[]|null}
50
+ */
51
+ getHandler(handlerId) {
52
+ if (handlerId < 0) return null;
53
+ return this._handlerMap[handlerId] || null;
54
+ }
55
+ }
package/lib/session.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Session — fuzionx-bridge 세션 N-API를 래핑하는 헬퍼.
3
+ * context.js에서 req.session으로 사용됨.
4
+ */
5
+
6
+ export function createSession(bridge) {
7
+ return {
8
+ /** 세션 데이터 조회 (전체) */
9
+ get: (sessionId) => bridge.sessionGet(sessionId),
10
+ /** 세션 데이터 저장 (data: HashMap, ttl: optional) */
11
+ set: (sessionId, data, ttlSecs) => bridge.sessionSet(sessionId, data, ttlSecs),
12
+ destroy: (sessionId) => bridge.sessionDestroy(sessionId),
13
+ renew: (sessionId, ttlSecs) => bridge.sessionRenew(sessionId, ttlSecs),
14
+ exists: (sessionId) => bridge.sessionExists(sessionId),
15
+ load: (sessionId) => bridge.sessionLoad(sessionId),
16
+ cleanup: () => bridge.sessionCleanup(),
17
+ };
18
+ }
package/lib/ws.js ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * WebSocket — 네임스페이스 기반 WS 매니저.
3
+ *
4
+ * 사용법:
5
+ * app.ws('/chat').on('connect', (socket) => { ... });
6
+ * app.ws('/chat').on('message', (socket, data) => { ... });
7
+ * app.ws('/chat').on('disconnect', (socket) => { ... });
8
+ *
9
+ * // 기본 네임스페이스 ("/")
10
+ * app.ws.on('message', (socket, data) => { ... });
11
+ */
12
+
13
+ /**
14
+ * 개별 WebSocket 연결을 나타내는 래퍼.
15
+ */
16
+ class RuxySocket {
17
+ constructor(sessionId, bridge) {
18
+ this._bridge = bridge;
19
+ this.sessionId = sessionId;
20
+ }
21
+
22
+ /** 이 소켓에 메시지 전송 */
23
+ send(data) {
24
+ const msg = typeof data === 'string' ? data : JSON.stringify(data);
25
+ this._bridge.wsSendTo(this.sessionId, msg);
26
+ }
27
+
28
+ /** 전체 연결에 브로드캐스트 */
29
+ broadcast(data) {
30
+ const msg = typeof data === 'string' ? data : JSON.stringify(data);
31
+ this._bridge.wsBroadcast(msg);
32
+ }
33
+
34
+ /** 자신을 제외하고 브로드캐스트 */
35
+ broadcastExcluding(data) {
36
+ const msg = typeof data === 'string' ? data : JSON.stringify(data);
37
+ this._bridge.wsBroadcastExcluding(msg, this.sessionId);
38
+ }
39
+
40
+ /** 여러 세션에 전송 */
41
+ sendToMany(sessionIds, data) {
42
+ const msg = typeof data === 'string' ? data : JSON.stringify(data);
43
+ this._bridge.wsSendToMany(sessionIds, msg);
44
+ }
45
+
46
+ /** 메타데이터 매칭 세션에 전송 */
47
+ sendToMatch(key, value, data) {
48
+ const msg = typeof data === 'string' ? data : JSON.stringify(data);
49
+ this._bridge.wsSendToMetadataMatch(key, value, msg);
50
+ }
51
+
52
+ /** 메타데이터 설정 */
53
+ setMetadata(key, value) {
54
+ this._bridge.wsSetMetadata(this.sessionId, key, value);
55
+ }
56
+
57
+ /** 메타데이터 조회 */
58
+ getMetadata(key) {
59
+ return this._bridge.wsGetMetadata(this.sessionId, key);
60
+ }
61
+
62
+ /** 연결 끊기 */
63
+ disconnect() {
64
+ this._bridge.wsDisconnect(this.sessionId);
65
+ }
66
+
67
+ /** JSON 직렬화 */
68
+ toJSON() {
69
+ return { sessionId: this.sessionId };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 네임스페이스별 핸들러 레지스트리.
75
+ */
76
+ class WsNamespace {
77
+ constructor(path) {
78
+ this.path = path;
79
+ this._handlers = { connect: null, message: null, disconnect: null };
80
+ }
81
+
82
+ /**
83
+ * 이벤트 핸들러 등록.
84
+ * @param {'connect'|'message'|'disconnect'} event
85
+ * @param {Function} handler
86
+ * @returns {WsNamespace} 체이닝 지원
87
+ */
88
+ on(event, handler) {
89
+ if (!['connect', 'message', 'disconnect'].includes(event)) {
90
+ throw new Error(`Unknown WS event: ${event}`);
91
+ }
92
+ this._handlers[event] = handler;
93
+ return this;
94
+ }
95
+ }
96
+
97
+ export function createWs(bridge) {
98
+ /** @type {Map<string, WsNamespace>} */
99
+ const namespaces = new Map();
100
+ let _initialized = false;
101
+
102
+ /**
103
+ * 내부: TSFN 콜백을 bridge에 한 번만 등록.
104
+ * 모든 네임스페이스의 이벤트가 여기로 들어옴 → path로 dispatch.
105
+ */
106
+ function ensureInitialized() {
107
+ if (_initialized) return;
108
+ _initialized = true;
109
+
110
+ // connect 이벤트 디스패쳐
111
+ bridge.wsOnConnect((namespace, sessionId) => {
112
+ const ns = namespaces.get(namespace);
113
+ if (ns && ns._handlers.connect) {
114
+ ns._handlers.connect(new RuxySocket(sessionId, bridge));
115
+ }
116
+ });
117
+
118
+ // message 이벤트 디스패쳐
119
+ bridge.wsOnMessage((namespace, sessionId, message) => {
120
+ const ns = namespaces.get(namespace);
121
+ if (ns && ns._handlers.message) {
122
+ ns._handlers.message(new RuxySocket(sessionId, bridge), message);
123
+ }
124
+ });
125
+
126
+ // disconnect 이벤트 디스패쳐
127
+ bridge.wsOnDisconnect((namespace, sessionId) => {
128
+ const ns = namespaces.get(namespace);
129
+ if (ns && ns._handlers.disconnect) {
130
+ ns._handlers.disconnect(new RuxySocket(sessionId, bridge));
131
+ }
132
+ });
133
+ }
134
+
135
+ /**
136
+ * 네임스페이스를 가져오거나 생성.
137
+ * @param {string} path
138
+ * @returns {WsNamespace}
139
+ */
140
+ function getOrCreateNs(path) {
141
+ ensureInitialized();
142
+ if (!namespaces.has(path)) {
143
+ namespaces.set(path, new WsNamespace(path));
144
+ }
145
+ return namespaces.get(path);
146
+ }
147
+
148
+ // Proxy: ws('/chat') → 네임스페이스, ws.on() → 기본 "/" 네임스페이스
149
+ const wsProxy = new Proxy(function wsFunc(path) {
150
+ return getOrCreateNs(path);
151
+ }, {
152
+ get(target, prop) {
153
+ // ws.on('event', handler) → 기본 "/" 네임스페이스
154
+ if (prop === 'on') {
155
+ const defaultNs = getOrCreateNs('/');
156
+ return defaultNs.on.bind(defaultNs);
157
+ }
158
+
159
+ // ws.broadcast, ws.sendTo 등 글로벌 유틸
160
+ switch (prop) {
161
+ case 'broadcast':
162
+ return (data) => bridge.wsBroadcast(typeof data === 'string' ? data : JSON.stringify(data));
163
+ case 'broadcastExcluding':
164
+ return (data, excludeSessionId) => bridge.wsBroadcastExcluding(typeof data === 'string' ? data : JSON.stringify(data), excludeSessionId);
165
+ case 'broadcastLocal':
166
+ return (data) => bridge.wsBroadcastLocal(typeof data === 'string' ? data : JSON.stringify(data));
167
+ case 'broadcastExcludingLocal':
168
+ return (data, excludeSessionId) => bridge.wsBroadcastExcludingLocal(typeof data === 'string' ? data : JSON.stringify(data), excludeSessionId);
169
+ case 'sendTo':
170
+ return (sessionId, data) => bridge.wsSendTo(sessionId, typeof data === 'string' ? data : JSON.stringify(data));
171
+ case 'sendToMany':
172
+ return (sessionIds, data) => bridge.wsSendToMany(sessionIds, typeof data === 'string' ? data : JSON.stringify(data));
173
+ case 'onlineCount':
174
+ return bridge.wsGetOnlineCount();
175
+ case 'sessionIds':
176
+ return bridge.wsGetSessionIds();
177
+ case 'removeInactive':
178
+ return (seconds) => bridge.wsRemoveInactive(seconds);
179
+ case 'disconnect':
180
+ return (sessionId) => bridge.wsDisconnect(sessionId);
181
+ case 'namespaces':
182
+ return namespaces;
183
+ default:
184
+ return Reflect.get(target, prop);
185
+ }
186
+ },
187
+ });
188
+
189
+ return wsProxy;
190
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@fuzionx/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Express-style Node.js framework powered by FuzionX native bridge — 167K RPS single process",
6
+ "main": "index.js",
7
+ "types": "types/index.d.ts",
8
+ "exports": {
9
+ ".": "./index.js"
10
+ },
11
+ "keywords": ["framework", "http", "server", "rust", "native", "high-performance", "fuzionx", "napi"],
12
+ "license": "MIT",
13
+ "engines": {
14
+ "node": ">=18.0.0"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/saytohenry/fuzionx"
19
+ },
20
+ "dependencies": {
21
+ "@fuzionx/bridge": "^0.1.0"
22
+ },
23
+ "files": [
24
+ "index.js",
25
+ "lib/",
26
+ "bin/",
27
+ "types/"
28
+ ]
29
+ }
@@ -0,0 +1,102 @@
1
+ // TypeScript 타입 정의 — ruxy framework
2
+
3
+ export interface RuxyOptions {
4
+ config: string;
5
+ port?: number;
6
+ }
7
+
8
+ export interface RuxyRequest {
9
+ method: string;
10
+ url: string;
11
+ path: string;
12
+ query: Record<string, string>;
13
+ params: Record<string, string>;
14
+ headers: Record<string, string>;
15
+ ip: string;
16
+ body: string;
17
+ json: any;
18
+ handlerId: number;
19
+ requestId: number;
20
+ sessionId: string | null;
21
+ session: SessionHelper;
22
+ t: (key: string, defaultValue?: string) => string;
23
+ }
24
+
25
+ export interface RuxyResponse {
26
+ status(code: number): RuxyResponse;
27
+ json(data: any): RuxyResponse;
28
+ send(text: string): RuxyResponse;
29
+ html(content: string): RuxyResponse;
30
+ redirect(url: string, code?: number): RuxyResponse;
31
+ header(key: string, value: string): RuxyResponse;
32
+ }
33
+
34
+ export type NextFunction = (err?: Error) => void;
35
+ export type RequestHandler = (req: RuxyRequest, res: RuxyResponse, next: NextFunction) => void;
36
+ export type ErrorHandler = (err: Error, req: RuxyRequest, res: RuxyResponse, next: NextFunction) => void;
37
+
38
+ export interface SessionHelper {
39
+ get(key?: string): any;
40
+ set(key: string, value: string): void;
41
+ destroy(): void;
42
+ renew(): string | null;
43
+ }
44
+
45
+ export interface I18nHelper {
46
+ translate(locale: string, key: string): string | null;
47
+ readonly locales: string[];
48
+ setLocale(locale: string): void;
49
+ updateMissing(key: string, value: string): void;
50
+ readonly autoComplete: boolean;
51
+ render(template: string, context?: Record<string, any>, locale?: string): string;
52
+ }
53
+
54
+ export interface WsHelper {
55
+ broadcast(data: any): void;
56
+ sendTo(sessionId: string, data: any): void;
57
+ sendToMany(sessionIds: string[], data: any): void;
58
+ broadcastExcluding(excludeId: string, data: any): void;
59
+ sendToMatch(key: string, value: string, data: any): void;
60
+ readonly onlineCount: number;
61
+ readonly sessionIds: string[];
62
+ disconnect(sessionId: string): void;
63
+ setMetadata(sessionId: string, key: string, value: string): void;
64
+ getMetadata(sessionId: string, key: string): string | null;
65
+ removeInactive(seconds: number): number;
66
+ }
67
+
68
+ export interface CryptoHelper {
69
+ uuid(): string;
70
+ md5(input: string): string;
71
+ sha256(input: string): string;
72
+ encrypt(plaintext: string, key: string): string;
73
+ decrypt(ciphertext: string, key: string): string;
74
+ encryptCustom(plaintext: string, key: string): string;
75
+ decryptCustom(ciphertext: string, key: string): string;
76
+ getUaSlice(userAgent: string): string;
77
+ deriveTransportKey(sessionKey: string, uaSlice: string): string;
78
+ }
79
+
80
+ export declare class RuxyApp {
81
+ constructor(options?: RuxyOptions);
82
+
83
+ readonly config: Record<string, any>;
84
+ readonly appConfig: Record<string, any>;
85
+ readonly crypto: CryptoHelper;
86
+ readonly ws: WsHelper | null;
87
+ readonly i18n: I18nHelper | null;
88
+
89
+ get(path: string, ...handlers: RequestHandler[]): this;
90
+ post(path: string, ...handlers: RequestHandler[]): this;
91
+ put(path: string, ...handlers: RequestHandler[]): this;
92
+ delete(path: string, ...handlers: RequestHandler[]): this;
93
+ patch(path: string, ...handlers: RequestHandler[]): this;
94
+
95
+ use(handler: RequestHandler | ErrorHandler): this;
96
+ use(path: string, ...handlers: (RequestHandler | ErrorHandler)[]): this;
97
+
98
+ listen(port?: number, callback?: () => void): this;
99
+ listen(callback: () => void): this;
100
+ }
101
+
102
+ export declare function createApp(options?: RuxyOptions): RuxyApp;