@fuzionx/core 0.1.0 → 0.1.2

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.
@@ -1,9 +1,9 @@
1
1
  #!/bin/bash
2
- # ruxy reload — YAML 설정 핫 리로드
2
+ # fuzionx reload — YAML 설정 핫 리로드
3
3
  #
4
4
  # 사용법:
5
- # ./ruxy-reload.sh # 이름으로 PID 검색
6
- # ./ruxy-reload.sh <PID> # 직접 PID 지정
5
+ # ./fuzionx-reload.sh # 이름으로 PID 검색
6
+ # ./fuzionx-reload.sh <PID> # 직접 PID 지정
7
7
  #
8
8
  # SIGHUP을 보내면 서버가 YAML을 다시 읽고 보안 설정을 재적용합니다.
9
9
  # 서버 재시작 없이 CORS, rate-limit, HSTS, CSP, IP filter, 세션 설정 반영.
@@ -13,12 +13,12 @@ set -e
13
13
  if [ -n "$1" ]; then
14
14
  PID="$1"
15
15
  else
16
- # node 프로세스 중 ruxy 관련 검색
17
- PID=$(pgrep -f "ruxy|fusion" | head -1)
16
+ # node 프로세스 중 fuzionx 관련 검색
17
+ PID=$(pgrep -f "fuzionx|fusion" | head -1)
18
18
  fi
19
19
 
20
20
  if [ -z "$PID" ]; then
21
- echo "❌ ruxy 프로세스를 찾을 수 없습니다."
21
+ echo "❌ fuzionx 프로세스를 찾을 수 없습니다."
22
22
  echo " 사용법: $0 <PID>"
23
23
  exit 1
24
24
  fi
package/index.js CHANGED
@@ -1 +1 @@
1
- export { createApp, RuxyApp } from './lib/app.js';
1
+ export { createApp, FuzionXApp } from './lib/app.js';
package/lib/app.js CHANGED
@@ -1,12 +1,17 @@
1
1
  import { createRequire } from 'module';
2
2
  import path from 'path';
3
+ import cluster from 'cluster';
4
+ import os from 'os';
3
5
  import { Router } from './router.js';
4
- import { runMiddlewareChain } from './middleware.js';
6
+ import { runMiddlewareChain, runErrorChain } from './middleware.js';
5
7
  import { createReq, createRes } from './context.js';
6
8
  import { createSession } from './session.js';
7
9
  import { createI18n } from './i18n.js';
8
10
  import { createWs } from './ws.js';
9
11
  import { createCrypto } from './crypto.js';
12
+ import { createFile } from './file.js';
13
+ import { createHash } from './hash.js';
14
+ import { createMedia } from './media.js';
10
15
  import { createLogger, interceptConsole } from './logger.js';
11
16
 
12
17
  const require = createRequire(import.meta.url);
@@ -34,7 +39,7 @@ function loadBridge() {
34
39
  throw new Error('fuzionx-bridge native module을 찾을 수 없습니다. npm install @fuzionx/core를 실행해 주세요.');
35
40
  }
36
41
 
37
- export class RuxyApp {
42
+ export class FuzionXApp {
38
43
  /**
39
44
  * @param {object} options
40
45
  * @param {string} options.config - YAML 설정 파일 경로
@@ -49,8 +54,16 @@ export class RuxyApp {
49
54
  this._booted = false;
50
55
  this._port = options.port || 3000;
51
56
 
52
- // 헬퍼 모듈
57
+ // 사전 계산된 미들웨어 체인 캐시 (handlerId → handler[])
58
+ this._chainCache = null;
59
+ this._globalChain = null;
60
+ this._errorChain = null;
61
+
62
+ // 헬퍼 모듈 — 직통 N-API 호출, 오버헤드 없음
53
63
  this.crypto = createCrypto(this._bridge);
64
+ this.file = createFile(this._bridge);
65
+ this.hash = createHash(this._bridge);
66
+ this.media = createMedia(this._bridge); // utils-media feature 필요
54
67
  this._ws = null; // lazy — get ws() 에서 초기화
55
68
  this.i18n = null; // boot 후 초기화
56
69
  }
@@ -163,16 +176,74 @@ export class RuxyApp {
163
176
  port = port || this._port;
164
177
 
165
178
  this._boot();
166
- this._router.build();
167
179
 
180
+ // ── 워커 수 결정 (0=auto/cpus, 1=단일 프로세스) ──
181
+ const configWorkers = this._bridge.getWorkerCount();
182
+ const numWorkers = configWorkers > 0 ? configWorkers : os.cpus().length;
183
+
184
+ // ── Primary: cluster.fork() ──
185
+ if (numWorkers > 1 && cluster.isPrimary) {
186
+ console.log(`[fuzionx] Primary PID=${process.pid}, spawning ${numWorkers} workers on :${port}`);
187
+
188
+ for (let i = 0; i < numWorkers; i++) {
189
+ cluster.fork();
190
+ }
191
+
192
+ let shuttingDown = false;
193
+
194
+ cluster.on('exit', (worker, code) => {
195
+ console.log(`[fuzionx] Worker ${worker.process.pid} exited (code=${code})`);
196
+ if (!shuttingDown) {
197
+ console.log('[fuzionx] Restarting worker...');
198
+ cluster.fork();
199
+ }
200
+ });
201
+
202
+ // ── Primary Graceful Shutdown ──
203
+ const gracefulShutdown = (signal) => {
204
+ if (shuttingDown) return;
205
+ shuttingDown = true;
206
+ console.log(`[fuzionx] ${signal} received — shutting down all workers`);
207
+ for (const id in cluster.workers) {
208
+ cluster.workers[id].process.kill('SIGTERM');
209
+ }
210
+ setTimeout(() => process.exit(0), 3000);
211
+ };
212
+ process.once('SIGTERM', () => gracefulShutdown('SIGTERM'));
213
+ process.once('SIGINT', () => gracefulShutdown('SIGINT'));
214
+
215
+ // ── Primary Hot Reload: SIGHUP → 모든 워커에 전파 ──
216
+ process.on('SIGHUP', () => {
217
+ console.log('[fuzionx] SIGHUP — forwarding to all workers');
218
+ for (const id in cluster.workers) {
219
+ cluster.workers[id].process.kill('SIGHUP');
220
+ }
221
+ });
222
+
223
+ if (callback) setTimeout(() => callback(), 100);
224
+ return this;
225
+ }
226
+
227
+ // ── Worker (또는 단일 프로세스): Fusion 서버 시작 ──
228
+ if (cluster.isWorker) {
229
+ // 워커 프로세스는 독립적으로 boot 필요
230
+ this._boot();
231
+ }
232
+
233
+ this._router.build();
234
+ this._buildMiddlewareCache();
168
235
  this._bridge.startFusionServer(port, (rawReq) => this._handleRequest(rawReq));
169
236
 
170
- // ── Graceful Shutdown (SIGTERM/SIGINT) ──
237
+ if (cluster.isWorker) {
238
+ console.log(`[fuzionx] Worker PID=${process.pid} on :${port}`);
239
+ }
240
+
241
+ // ── Worker Graceful Shutdown (SIGTERM/SIGINT) ──
171
242
  let shutdownCalled = false;
172
243
  const gracefulShutdown = (signal) => {
173
244
  if (shutdownCalled) return;
174
245
  shutdownCalled = true;
175
- console.log(`[ruxy] ${signal} received — graceful shutdown`);
246
+ console.log(`[fuzionx] ${signal} received — graceful shutdown (PID=${process.pid})`);
176
247
  this._bridge.stopFusionServer();
177
248
  setTimeout(() => process.exit(0), 2000);
178
249
  };
@@ -184,9 +255,9 @@ export class RuxyApp {
184
255
  try {
185
256
  this._bridge.reloadConfig();
186
257
  this._bridge.reloadFusionConfig();
187
- console.log('[ruxy] SIGHUP — config hot-reload complete');
258
+ console.log(`[fuzionx] SIGHUP — config hot-reload complete (PID=${process.pid})`);
188
259
  } catch (e) {
189
- console.error('[ruxy] SIGHUP reload failed:', e.message);
260
+ console.error(`[fuzionx] SIGHUP reload failed (PID=${process.pid}):`, e.message);
190
261
  }
191
262
  });
192
263
 
@@ -194,58 +265,159 @@ export class RuxyApp {
194
265
 
195
266
  // ── Error Recovery ──
196
267
  process.on('uncaughtException', (err) => {
197
- console.error('[ruxy] uncaughtException:', err.message);
268
+ console.error('[fuzionx] uncaughtException:', err.message);
198
269
  });
199
270
  process.on('unhandledRejection', (reason) => {
200
- console.error('[ruxy] unhandledRejection:', reason);
271
+ console.error('[fuzionx] unhandledRejection:', reason);
201
272
  });
202
273
 
203
274
  if (callback) setTimeout(() => callback(), 100);
204
275
  return this;
205
276
  }
206
277
 
207
- /** 요청 처리 — 미들웨어 체인 → 라우트 핸들러 */
208
- _handleRequest(rawReq) {
209
- const req = createReq(rawReq, this);
210
- const res = createRes();
278
+ /**
279
+ * 미들웨어 체인 사전 계산.
280
+ * listen() 시점에 라우트별로 적용 가능한 미들웨어 + 핸들러를 합쳐서 캐시.
281
+ * _handleRequest()에서 .filter()/.map() 없이 O(1) 조회.
282
+ */
283
+ _buildMiddlewareCache() {
284
+ // 글로벌 미들웨어 (path === null)
285
+ const globalMw = [];
286
+ // 경로별 미들웨어 (path !== null)
287
+ const pathMw = [];
288
+
289
+ for (const m of this._middlewares) {
290
+ if (m.path === null) {
291
+ globalMw.push(m.handler);
292
+ } else {
293
+ pathMw.push(m);
294
+ }
295
+ }
211
296
 
212
- const route = this._router.getHandler(rawReq.handlerId);
297
+ this._globalChain = globalMw;
298
+ this._chainCache = new Map();
213
299
 
214
- // 경로 매치 미들웨어 필터
215
- const applicable = this._middlewares.filter(m =>
216
- m.path === null || req.url.startsWith(m.path)
217
- );
300
+ // 에러 핸들러도 사전 계산
301
+ this._errorChain = this._errorHandlers.map(m => m.handler);
218
302
 
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
- });
303
+ // 라우트별 체인 캐시
304
+ for (const route of this._router._routes) {
305
+ const chain = [...globalMw];
306
+ for (const m of pathMw) {
307
+ if (route.path.startsWith(m.path) ||
308
+ route.path === m.path ||
309
+ (m.path.endsWith('/') && route.path.startsWith(m.path))) {
310
+ chain.push(m.handler);
311
+ }
312
+ }
313
+ chain.push(...route.handlers);
314
+ this._chainCache.set(route.handlerId, chain);
226
315
  }
227
316
 
317
+ // 404 fallback 체인 (글로벌 미들웨어 + 404 핸들러)
318
+ this._notFoundChain = [...globalMw, (_req, _res) => {
319
+ _res.status(404).json({ error: 'Not Found', path: _req.path });
320
+ }];
321
+ }
322
+
323
+ /** 요청 처리 — 사전 계산된 체인으로 O(1) 조회 */
324
+ _handleRequest(rawReq) {
325
+ // sync 경로: 공유 인스턴스 재사용 (할당 0)
326
+ const req = createReq(rawReq, this, true);
327
+ const res = createRes(true);
328
+
329
+ // O(1) 체인 조회 — .filter()/.map() 없음
330
+ const chain = this._chainCache.get(rawReq.handlerId) || this._notFoundChain;
331
+
332
+ let result;
228
333
  try {
229
- runMiddlewareChain(chain, req, res);
334
+ result = runMiddlewareChain(chain, req, res);
230
335
  } 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 (_) {
336
+ this._handleError(err, req, res);
337
+ return res._toFusionResponse();
338
+ }
339
+
340
+ // Framework 등 외부 핸들러가 직접 { async: true } 반환 시
341
+ // Rust에 그대로 전달하여 pending_async 등록
342
+ if (result && result.async === true && typeof result.then !== 'function') {
343
+ return { async: true };
344
+ }
345
+
346
+ // 핸들러가 Promise를 리턴하면 — 비동기 경로
347
+ // async 핸들러는 응답 전까지 req/res가 유지되어야 하므로 새 인스턴스 생성
348
+ if (result && typeof result.then === 'function') {
349
+ const asyncReq = createReq(rawReq, this, false);
350
+ const asyncRes = createRes(false);
351
+ // sync 결과를 asyncRes에 복사 (미들웨어가 이미 설정한 값)
352
+ asyncRes._statusCode = res._statusCode;
353
+ asyncRes._body = res._body;
354
+ asyncRes._sent = res._sent;
355
+
356
+ result
357
+ .then(() => {
358
+ this._sendAsyncResponse(rawReq.requestId, asyncRes);
359
+ })
360
+ .catch((err) => {
361
+ this._handleError(err, asyncReq, asyncRes);
362
+ this._sendAsyncResponse(rawReq.requestId, asyncRes);
363
+ });
364
+
365
+ return { async: true };
366
+ }
367
+
368
+ return res._toFusionResponse();
369
+ }
370
+
371
+ /** 에러 핸들링 — 에러 핸들러 체인 or 500 */
372
+ _handleError(err, req, res) {
373
+ if (this._errorChain && this._errorChain.length > 0) {
374
+ try {
375
+ runErrorChain(this._errorChain, err, req, res);
376
+ } catch (_) {
377
+ if (!res._sent) {
238
378
  res.status(500).json({ error: 'Internal Server Error' });
239
379
  }
240
- } else {
380
+ }
381
+ } else {
382
+ console.error('[fuzionx] Handler error:', err.message || err);
383
+ if (!res._sent) {
241
384
  res.status(500).json({ error: err.message || 'Internal Server Error' });
242
385
  }
243
386
  }
387
+ }
244
388
 
245
- return res._toFusionResponse();
389
+ /** 비동기 응답 전송 — bridge.sendAsyncResponse 호출 */
390
+ _sendAsyncResponse(requestId, res) {
391
+ const resp = res._toFusionResponse();
392
+ const contentType = resp.headers?.['Content-Type'] || 'application/json';
393
+
394
+ // Content-Type 외 추가 헤더 → 배열 join (문자열 반복 연결 제거)
395
+ const headerParts = [];
396
+ if (resp.headers) {
397
+ const keys = Object.keys(resp.headers);
398
+ for (let i = 0; i < keys.length; i++) {
399
+ const k = keys[i];
400
+ if (k !== 'Content-Type') {
401
+ headerParts.push(k + ': ' + resp.headers[k]);
402
+ }
403
+ }
404
+ }
405
+ const extraHeaders = headerParts.length > 0 ? headerParts.join('\r\n') + '\r\n' : '';
406
+
407
+ try {
408
+ this._bridge.sendAsyncResponse(
409
+ requestId,
410
+ resp.status,
411
+ resp.body,
412
+ contentType,
413
+ extraHeaders,
414
+ );
415
+ } catch (e) {
416
+ console.error('[fuzionx] sendAsyncResponse failed:', e.message);
417
+ }
246
418
  }
247
419
  }
248
420
 
249
421
  export function createApp(options) {
250
- return new RuxyApp(options);
422
+ return new FuzionXApp(options);
251
423
  }
package/lib/context.js CHANGED
@@ -1,150 +1,225 @@
1
1
  /**
2
2
  * Context — Fusion raw req 객체를 Express 스타일 req/res로 변환.
3
+ *
4
+ * 최적화:
5
+ * - 프로토타입 기반 + 재사용 가능한 응답 객체
6
+ * - Object.defineProperty 제거 → 함수 기반 lazy JSON
7
+ * - 요청당 클로저 최소화
8
+ * - _toFusionResponse()에서 할당 0
3
9
  */
4
10
 
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) {
11
+ // ── req.session 프로토타입 ──
12
+ const SessionProto = {
13
+ get(key) {
14
+ if (key) return this._data[key] || null;
15
+ return { ...this._data };
16
+ },
17
+ set(key, value) {
18
+ this._data[key] = String(value);
19
+ if (this._id) {
20
+ this._bridge.sessionSet(this._id, { ...this._data });
21
+ }
22
+ },
23
+ destroy() {
24
+ if (this._id) {
25
+ this._bridge.sessionDestroy(this._id);
26
+ this._data = {};
27
+ }
28
+ },
29
+ renew() {
30
+ if (this._id) {
31
+ return this._bridge.sessionRenew(this._id);
32
+ }
33
+ return null;
34
+ },
35
+ };
36
+
37
+ // ── req 프로토타입 ──
38
+ const ReqProto = {
39
+ /** lazy JSON 파싱 — body가 이미 object면 그대로 반환 */
40
+ get json() {
41
+ if (this._json === undefined) {
42
+ if (typeof this.body === 'object' && this.body !== null) {
43
+ this._json = this.body;
44
+ } else {
32
45
  try {
33
- _jsonCache = req.body ? JSON.parse(req.body) : null;
46
+ this._json = this.body ? JSON.parse(this.body) : null;
34
47
  } catch (_) {
35
- _jsonCache = null;
48
+ this._json = null;
36
49
  }
37
50
  }
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) => {
51
+ }
52
+ return this._json;
53
+ },
54
+ /** i18n 번역 */
55
+ t(key, defaultValue) {
56
+ const app = this._app;
77
57
  if (!app.i18n) return key;
78
- // Accept-Language 또는 기본 로케일
79
- const locale = req.headers['accept-language']?.split(',')[0]?.split('-')[0] || 'en';
58
+ const locale = this.headers?.['accept-language']?.split(',')[0]?.split('-')[0] || 'en';
80
59
  const result = app.i18n.translate(locale, key);
81
60
  if (result == null && defaultValue != null) {
82
61
  app.i18n.updateMissing(key, defaultValue);
83
62
  return defaultValue;
84
63
  }
85
64
  return result || defaultValue || key;
86
- };
87
-
88
- return req;
89
- }
65
+ },
66
+ };
67
+
68
+ // ── 사전 할당된 session 인스턴스 (재사용) ──
69
+ const _sharedSession = Object.create(SessionProto);
70
+ _sharedSession._id = null;
71
+ _sharedSession._data = {};
72
+ _sharedSession._bridge = null;
73
+
74
+ // ── 사전 할당된 req/res 인스턴스 (단일 스레드 재사용) ──
75
+ const _sharedReq = Object.create(ReqProto);
76
+ _sharedReq.method = '';
77
+ _sharedReq.url = '';
78
+ _sharedReq.path = '';
79
+ _sharedReq.query = null;
80
+ _sharedReq.params = null;
81
+ _sharedReq.headers = null;
82
+ _sharedReq.ip = '';
83
+ _sharedReq.body = '';
84
+ _sharedReq.handlerId = 0;
85
+ _sharedReq.requestId = 0;
86
+ _sharedReq.sessionId = null;
87
+ _sharedReq.files = null;
88
+ _sharedReq._app = null;
89
+ _sharedReq._json = undefined;
90
+ _sharedReq.session = _sharedSession;
90
91
 
91
92
  /**
92
- * Response 래퍼 생성
93
+ * Request 래퍼 생성 — 프로토타입 기반 + 재사용
94
+ * @param {object} rawReq - Fusion 콜백의 raw 요청 객체
95
+ * @param {object} app - FuzionXApp 인스턴스
96
+ * @param {boolean} [reusable=true] - true면 공유 인스턴스 재사용 (sync용), false면 새로 생성 (async용)
93
97
  */
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
- },
98
+ export function createReq(rawReq, app, reusable = true) {
99
+ let req;
100
+ if (reusable) {
101
+ // sync 핸들러: 공유 인스턴스 재사용 (할당 0)
102
+ req = _sharedReq;
103
+ } else {
104
+ // async 핸들러: 새 인스턴스 (응답 전까지 유지 필요)
105
+ req = Object.create(ReqProto);
106
+ }
107
+
108
+ req.method = rawReq.method;
109
+ req.url = rawReq.url;
110
+ req.path = rawReq.path;
111
+ req.query = rawReq.query || null;
112
+ req.params = rawReq.params || null;
113
+ req.headers = rawReq.headers || null;
114
+ req.ip = rawReq.remoteIp || '';
115
+ req.body = rawReq.body || '';
116
+ req.handlerId = rawReq.handlerId;
117
+ req.requestId = rawReq.requestId;
118
+ req.sessionId = rawReq.sessionId || null;
119
+ req._app = app;
120
+ req._json = undefined;
121
+ req.files = rawReq.files || null;
122
+ req.formFields = rawReq.formFields || null;
123
+
124
+ // session 업데이트 (재사용)
125
+ if (reusable) {
126
+ _sharedSession._id = rawReq.sessionId || null;
127
+ _sharedSession._data = rawReq.session || {};
128
+ _sharedSession._bridge = app._bridge;
129
+ } else {
130
+ const session = Object.create(SessionProto);
131
+ session._id = rawReq.sessionId || null;
132
+ session._data = rawReq.session || {};
133
+ session._bridge = app._bridge;
134
+ req.session = session;
135
+ }
118
136
 
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
- },
137
+ return req;
138
+ }
138
139
 
139
- /** Fusion 응답 포맷으로 변환 */
140
- _toFusionResponse() {
141
- return {
142
- status: res._statusCode,
143
- body: res._body,
144
- headers: res._headers,
145
- };
146
- },
147
- };
140
+ // ── Response 프로토타입 ──
141
+ const ResProto = {
142
+ status(code) {
143
+ this._statusCode = code;
144
+ return this;
145
+ },
146
+ json(data) {
147
+ this._body = JSON.stringify(data);
148
+ this._headers['Content-Type'] = 'application/json';
149
+ this._sent = true;
150
+ return this;
151
+ },
152
+ send(text) {
153
+ this._body = String(text);
154
+ this._sent = true;
155
+ return this;
156
+ },
157
+ html(content) {
158
+ this._body = String(content);
159
+ this._headers['Content-Type'] = 'text/html; charset=utf-8';
160
+ this._sent = true;
161
+ return this;
162
+ },
163
+ redirect(url, code = 302) {
164
+ this._statusCode = code;
165
+ this._headers['Location'] = url;
166
+ this._body = '';
167
+ this._sent = true;
168
+ return this;
169
+ },
170
+ end() {
171
+ this._body = '';
172
+ this._sent = true;
173
+ return this;
174
+ },
175
+ header(key, value) {
176
+ this._headers[key] = value;
177
+ return this;
178
+ },
179
+ /** Fusion 응답 포맷으로 변환 — 공유 응답 객체 재사용 */
180
+ _toFusionResponse() {
181
+ _sharedResponse.status = this._statusCode;
182
+ _sharedResponse.body = this._body;
183
+ _sharedResponse.headers = this._headers;
184
+ return _sharedResponse;
185
+ },
186
+ };
187
+
188
+ // ── 사전 할당된 응답 객체 (재사용) ──
189
+ const _sharedResponse = { status: 200, body: '', headers: null };
190
+ const _sharedHeaders = {};
191
+
192
+ // ── 사전 할당된 res 인스턴스 ──
193
+ const _sharedRes = Object.create(ResProto);
194
+ _sharedRes._statusCode = 200;
195
+ _sharedRes._body = '';
196
+ _sharedRes._headers = _sharedHeaders;
197
+ _sharedRes._sent = false;
148
198
 
199
+ /**
200
+ * Response 래퍼 생성 — 프로토타입 기반 + 재사용
201
+ * @param {boolean} [reusable=true] - true면 공유 인스턴스 재사용, false면 새로 생성
202
+ */
203
+ export function createRes(reusable = true) {
204
+ if (reusable) {
205
+ // sync: 공유 인스턴스 리셋 후 재사용 (할당 0)
206
+ _sharedRes._statusCode = 200;
207
+ _sharedRes._body = '';
208
+ // headers 객체 재사용 — 이전 키 제거
209
+ const keys = Object.keys(_sharedHeaders);
210
+ for (let i = 0; i < keys.length; i++) {
211
+ delete _sharedHeaders[keys[i]];
212
+ }
213
+ _sharedRes._headers = _sharedHeaders;
214
+ _sharedRes._sent = false;
215
+ return _sharedRes;
216
+ }
217
+
218
+ // async: 새 인스턴스
219
+ const res = Object.create(ResProto);
220
+ res._statusCode = 200;
221
+ res._body = '';
222
+ res._headers = {};
223
+ res._sent = false;
149
224
  return res;
150
225
  }
package/lib/crypto.js CHANGED
@@ -19,24 +19,5 @@ export function createCrypto(bridge) {
19
19
 
20
20
  /** AES-256-GCM 복호화 */
21
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
22
  };
42
23
  }
package/lib/file.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * FileHelper — fuzionx-bridge 파일 유틸리티 N-API 래핑.
3
+ * app.file로 접근.
4
+ *
5
+ * 최적화: 모든 호출은 Rust N-API 직통, JS 오버헤드 없음.
6
+ */
7
+
8
+ export function createFile(bridge) {
9
+ return {
10
+ /** 파일 이동 (cross-device 자동 대응) */
11
+ move: (src, dst) => bridge.fileMoveFile(src, dst),
12
+
13
+ /** 파일 복사 — 복사된 바이트 수 반환 */
14
+ copy: (src, dst) => bridge.fileCopyFile(src, dst),
15
+
16
+ /** 디렉토리 재귀 생성 */
17
+ ensureDir: (dirPath) => bridge.fileEnsureDir(dirPath),
18
+
19
+ /** 파일 크기 (bytes) */
20
+ size: (filePath) => bridge.fileSize(filePath),
21
+
22
+ /** 파일 존재 여부 */
23
+ exists: (filePath) => bridge.fileExists(filePath),
24
+
25
+ /** 파일 삭제 */
26
+ remove: (filePath) => bridge.fileRemove(filePath),
27
+
28
+ /** 임시 파일 경로 생성 (UUID 기반) */
29
+ tempPath: (prefix) => bridge.fileTempPath(prefix || 'fuzionx'),
30
+
31
+ /** 확장자 추출 (소문자, dot 제외) — null 가능 */
32
+ extension: (filePath) => bridge.fileExtension(filePath),
33
+ };
34
+ }
package/lib/hash.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * HashHelper — fuzionx-bridge 해싱 유틸리티 N-API 래핑.
3
+ * app.hash로 접근.
4
+ *
5
+ * 최적화: Rust bcrypt/argon2 직통 호출, JS crypto 미사용.
6
+ */
7
+
8
+ export function createHash(bridge) {
9
+ return {
10
+ /** bcrypt 해시 생성 (cost 기본 12) */
11
+ bcrypt: (password, cost) => bridge.hashBcrypt(password, cost),
12
+
13
+ /** bcrypt 해시 검증 */
14
+ bcryptVerify: (password, hash) => bridge.hashBcryptVerify(password, hash),
15
+
16
+ /** argon2id 해시 생성 (랜덤 salt) */
17
+ argon2: (password) => bridge.hashArgon2(password),
18
+
19
+ /** argon2 해시 검증 */
20
+ argon2Verify: (password, hash) => bridge.hashArgon2Verify(password, hash),
21
+ };
22
+ }
package/lib/logger.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @fileoverview Ruxy Bridge 통합 로거.
2
+ * @fileoverview FuzionX Bridge 통합 로거.
3
3
  *
4
4
  * Rust tracing과 Node.js console을 일원화한다.
5
5
  * - createLogger(bridge): app.logger 객체 생성
package/lib/media.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * MediaHelper — fuzionx-bridge 미디어(이미지+비디오) N-API 래핑.
3
+ * app.media로 접근.
4
+ *
5
+ * 최적화:
6
+ * - 이미지 처리: Rust image+webp crate 직통 (Lanczos3 리사이즈)
7
+ * - 비디오: ffmpeg CLI 호출 (Rust std::process::Command)
8
+ * - JS 힙 메모리 무할당 — 파일-to-파일 처리
9
+ */
10
+
11
+ export function createMedia(bridge) {
12
+ return {
13
+ // ── 이미지 ──
14
+
15
+ /**
16
+ * 이미지 리사이즈 (종횡비 유지).
17
+ * @param {string} input - 입력 파일 경로
18
+ * @param {string} output - 출력 파일 경로
19
+ * @param {number} width - 최대 가로
20
+ * @param {number} height - 최대 세로
21
+ * @param {string} [format='webp'] - 출력 포맷
22
+ * @param {number} [quality=80] - 품질 (1-100)
23
+ * @returns {string} 출력 경로
24
+ */
25
+ resize: (input, output, width, height, format, quality) =>
26
+ bridge.mediaResize(input, output, width, height, format, quality),
27
+
28
+ /**
29
+ * 다중 사이즈 리사이즈 (한 번의 디코드로 여러 출력).
30
+ * @param {string} input - 입력 파일 경로
31
+ * @param {string} outputDir - 출력 디렉토리
32
+ * @param {string} baseName - 기본 파일명
33
+ * @param {Array<{width:number,height:number,format?:string,quality?:number,suffix:string}>} specs
34
+ * @returns {string[]} 출력 경로 배열
35
+ */
36
+ resizeMultiple: (input, outputDir, baseName, specs) =>
37
+ bridge.mediaResizeMultiple(input, outputDir, baseName, specs),
38
+
39
+ /**
40
+ * 이미지 정보 조회.
41
+ * @returns {{width:number, height:number, format:string}}
42
+ */
43
+ imageInfo: (filePath) => bridge.mediaGetImageInfo(filePath),
44
+
45
+ /**
46
+ * WebP 변환.
47
+ * @param {string} input - 입력 경로
48
+ * @param {string} output - 출력 경로
49
+ * @param {number} [quality=80] - 품질
50
+ * @returns {string} 출력 경로
51
+ */
52
+ toWebp: (input, output, quality) =>
53
+ bridge.mediaConvertToWebp(input, output, quality),
54
+
55
+ // ── 비디오 ──
56
+
57
+ /**
58
+ * 비디오 썸네일 추출 (ffmpeg).
59
+ * @param {string} input - 비디오 경로
60
+ * @param {string} output - 출력 이미지 경로
61
+ * @param {number} [atSeconds=3] - 캡처 시점 (초)
62
+ * @param {number} [width=0] - 출력 가로 (0=원본)
63
+ * @param {string} [format='jpeg'] - 출력 포맷
64
+ * @returns {string} 출력 경로
65
+ */
66
+ videoThumbnail: (input, output, atSeconds, width, format) =>
67
+ bridge.mediaVideoThumbnail(input, output, atSeconds, width, format),
68
+
69
+ /**
70
+ * 비디오 정보 조회 (ffprobe).
71
+ * @returns {{duration:number, width:number, height:number, codec:string, fps:number}}
72
+ */
73
+ videoInfo: (filePath) => bridge.mediaVideoInfo(filePath),
74
+ };
75
+ }
package/lib/middleware.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Middleware — Express 스타일 동기 미들웨어 체인 엔진.
2
+ * Middleware — 동기 + 비동기 미들웨어 체인 엔진.
3
3
  *
4
- * Fusion 콜백은 동기 응답을 기대하므로 async 지원하지 않음.
5
- * next()를 호출하면 다음 미들웨어로 진행.
4
+ * Express 호환: next()가 Promise를 반환하면 체인 전체가 async 전파.
5
+ * Fusion 콜백에서 Promise가 감지되면 bridge의 sendAsyncResponse로 처리.
6
6
  */
7
7
 
8
8
  /**
@@ -10,17 +10,50 @@
10
10
  * @param {Function[]} handlers - (req, res, next) 핸들러 배열
11
11
  * @param {object} req
12
12
  * @param {object} res
13
+ * @returns {undefined|Promise} 비동기 핸들러가 있으면 Promise 리턴
13
14
  */
14
15
  export function runMiddlewareChain(handlers, req, res) {
15
16
  let index = 0;
17
+ const len = handlers.length;
16
18
 
17
19
  function next(err) {
18
20
  if (err) throw err;
19
- if (res._sent || index >= handlers.length) return;
21
+ if (res._sent || index >= len) return;
20
22
 
21
23
  const handler = handlers[index++];
22
- handler(req, res, next);
24
+ const result = handler(req, res, next);
25
+
26
+ // 핸들러가 Promise를 리턴하면 (async 함수) → 전파
27
+ // 이후 체인은 Promise 안에서 이미 next()로 실행됨
28
+ return result;
29
+ }
30
+
31
+ return next();
32
+ }
33
+
34
+ /**
35
+ * 에러 핸들러 체인 실행
36
+ * @param {Function[]} handlers - (err, req, res, next) 핸들러 배열
37
+ * @param {Error} err
38
+ * @param {object} req
39
+ * @param {object} res
40
+ */
41
+ export function runErrorChain(handlers, err, req, res) {
42
+ let index = 0;
43
+ const len = handlers.length;
44
+
45
+ function next(nextErr) {
46
+ if (res._sent || index >= len) return;
47
+ const handler = handlers[index++];
48
+ try {
49
+ handler(nextErr || err, req, res, next);
50
+ } catch (e) {
51
+ // 에러 핸들러가 또 throw하면 다음 핸들러로
52
+ if (index < len) {
53
+ next(e);
54
+ }
55
+ }
23
56
  }
24
57
 
25
- next();
58
+ next(err);
26
59
  }
package/lib/ws.js CHANGED
@@ -13,7 +13,7 @@
13
13
  /**
14
14
  * 개별 WebSocket 연결을 나타내는 래퍼.
15
15
  */
16
- class RuxySocket {
16
+ class FuzionXSocket {
17
17
  constructor(sessionId, bridge) {
18
18
  this._bridge = bridge;
19
19
  this.sessionId = sessionId;
@@ -111,7 +111,7 @@ export function createWs(bridge) {
111
111
  bridge.wsOnConnect((namespace, sessionId) => {
112
112
  const ns = namespaces.get(namespace);
113
113
  if (ns && ns._handlers.connect) {
114
- ns._handlers.connect(new RuxySocket(sessionId, bridge));
114
+ ns._handlers.connect(new FuzionXSocket(sessionId, bridge));
115
115
  }
116
116
  });
117
117
 
@@ -119,7 +119,7 @@ export function createWs(bridge) {
119
119
  bridge.wsOnMessage((namespace, sessionId, message) => {
120
120
  const ns = namespaces.get(namespace);
121
121
  if (ns && ns._handlers.message) {
122
- ns._handlers.message(new RuxySocket(sessionId, bridge), message);
122
+ ns._handlers.message(new FuzionXSocket(sessionId, bridge), message);
123
123
  }
124
124
  });
125
125
 
@@ -127,7 +127,7 @@ export function createWs(bridge) {
127
127
  bridge.wsOnDisconnect((namespace, sessionId) => {
128
128
  const ns = namespaces.get(namespace);
129
129
  if (ns && ns._handlers.disconnect) {
130
- ns._handlers.disconnect(new RuxySocket(sessionId, bridge));
130
+ ns._handlers.disconnect(new FuzionXSocket(sessionId, bridge));
131
131
  }
132
132
  });
133
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzionx/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Express-style Node.js framework powered by FuzionX native bridge — 167K RPS single process",
6
6
  "main": "index.js",
@@ -18,7 +18,7 @@
18
18
  "url": "https://github.com/saytohenry/fuzionx"
19
19
  },
20
20
  "dependencies": {
21
- "@fuzionx/bridge": "^0.1.0"
21
+ "@fuzionx/bridge": "^0.1.2"
22
22
  },
23
23
  "files": [
24
24
  "index.js",
package/types/index.d.ts CHANGED
@@ -1,11 +1,19 @@
1
- // TypeScript 타입 정의 — ruxy framework
1
+ // TypeScript 타입 정의 — fuzionx framework
2
2
 
3
- export interface RuxyOptions {
3
+ export interface FuzionXOptions {
4
4
  config: string;
5
5
  port?: number;
6
6
  }
7
7
 
8
- export interface RuxyRequest {
8
+ export interface UploadedFile {
9
+ fieldName: string;
10
+ originalName: string;
11
+ mimeType: string;
12
+ size: number;
13
+ tempPath: string;
14
+ }
15
+
16
+ export interface FuzionXRequest {
9
17
  method: string;
10
18
  url: string;
11
19
  path: string;
@@ -18,22 +26,23 @@ export interface RuxyRequest {
18
26
  handlerId: number;
19
27
  requestId: number;
20
28
  sessionId: string | null;
29
+ files: UploadedFile[] | null;
21
30
  session: SessionHelper;
22
31
  t: (key: string, defaultValue?: string) => string;
23
32
  }
24
33
 
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;
34
+ export interface FuzionXResponse {
35
+ status(code: number): FuzionXResponse;
36
+ json(data: any): FuzionXResponse;
37
+ send(text: string): FuzionXResponse;
38
+ html(content: string): FuzionXResponse;
39
+ redirect(url: string, code?: number): FuzionXResponse;
40
+ header(key: string, value: string): FuzionXResponse;
32
41
  }
33
42
 
34
43
  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;
44
+ export type RequestHandler = (req: FuzionXRequest, res: FuzionXResponse, next: NextFunction) => void;
45
+ export type ErrorHandler = (err: Error, req: FuzionXRequest, res: FuzionXResponse, next: NextFunction) => void;
37
46
 
38
47
  export interface SessionHelper {
39
48
  get(key?: string): any;
@@ -77,12 +86,64 @@ export interface CryptoHelper {
77
86
  deriveTransportKey(sessionKey: string, uaSlice: string): string;
78
87
  }
79
88
 
80
- export declare class RuxyApp {
81
- constructor(options?: RuxyOptions);
89
+ export interface FileHelper {
90
+ move(src: string, dst: string): void;
91
+ copy(src: string, dst: string): number;
92
+ ensureDir(dirPath: string): void;
93
+ size(filePath: string): number;
94
+ exists(filePath: string): boolean;
95
+ remove(filePath: string): boolean;
96
+ tempPath(prefix?: string): string;
97
+ extension(filePath: string): string | null;
98
+ }
99
+
100
+ export interface HashHelper {
101
+ bcrypt(password: string, cost?: number): string;
102
+ bcryptVerify(password: string, hash: string): boolean;
103
+ argon2(password: string): string;
104
+ argon2Verify(password: string, hash: string): boolean;
105
+ }
106
+
107
+ export interface ResizeSpec {
108
+ width: number;
109
+ height: number;
110
+ format?: string;
111
+ quality?: number;
112
+ suffix: string;
113
+ }
114
+
115
+ export interface ImageInfo {
116
+ width: number;
117
+ height: number;
118
+ format: string;
119
+ }
120
+
121
+ export interface VideoInfo {
122
+ duration: number;
123
+ width: number;
124
+ height: number;
125
+ codec: string;
126
+ fps: number;
127
+ }
128
+
129
+ export interface MediaHelper {
130
+ resize(input: string, output: string, width: number, height: number, format?: string, quality?: number): string;
131
+ resizeMultiple(input: string, outputDir: string, baseName: string, specs: ResizeSpec[]): string[];
132
+ imageInfo(filePath: string): ImageInfo;
133
+ toWebp(input: string, output: string, quality?: number): string;
134
+ videoThumbnail(input: string, output: string, atSeconds?: number, width?: number, format?: string): string;
135
+ videoInfo(filePath: string): VideoInfo;
136
+ }
137
+
138
+ export declare class FuzionXApp {
139
+ constructor(options?: FuzionXOptions);
82
140
 
83
141
  readonly config: Record<string, any>;
84
142
  readonly appConfig: Record<string, any>;
85
143
  readonly crypto: CryptoHelper;
144
+ readonly file: FileHelper;
145
+ readonly hash: HashHelper;
146
+ readonly media: MediaHelper;
86
147
  readonly ws: WsHelper | null;
87
148
  readonly i18n: I18nHelper | null;
88
149
 
@@ -99,4 +160,5 @@ export declare class RuxyApp {
99
160
  listen(callback: () => void): this;
100
161
  }
101
162
 
102
- export declare function createApp(options?: RuxyOptions): RuxyApp;
163
+ export declare function createApp(options?: FuzionXOptions): FuzionXApp;
164
+