@fuzionx/core 0.1.0 → 0.1.1
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/bin/ruxy-reload.sh +6 -6
- package/index.js +1 -1
- package/lib/app.js +209 -37
- package/lib/context.js +202 -127
- package/lib/crypto.js +0 -19
- package/lib/file.js +34 -0
- package/lib/hash.js +22 -0
- package/lib/logger.js +1 -1
- package/lib/media.js +75 -0
- package/lib/middleware.js +39 -6
- package/lib/ws.js +4 -4
- package/package.json +2 -2
- package/types/index.d.ts +77 -15
package/bin/ruxy-reload.sh
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
2
|
+
# fuzionx reload — YAML 설정 핫 리로드
|
|
3
3
|
#
|
|
4
4
|
# 사용법:
|
|
5
|
-
# ./
|
|
6
|
-
# ./
|
|
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 프로세스 중
|
|
17
|
-
PID=$(pgrep -f "
|
|
16
|
+
# node 프로세스 중 fuzionx 관련 검색
|
|
17
|
+
PID=$(pgrep -f "fuzionx|fusion" | head -1)
|
|
18
18
|
fi
|
|
19
19
|
|
|
20
20
|
if [ -z "$PID" ]; then
|
|
21
|
-
echo "❌
|
|
21
|
+
echo "❌ fuzionx 프로세스를 찾을 수 없습니다."
|
|
22
22
|
echo " 사용법: $0 <PID>"
|
|
23
23
|
exit 1
|
|
24
24
|
fi
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createApp,
|
|
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
|
|
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
|
-
|
|
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(`[
|
|
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(
|
|
258
|
+
console.log(`[fuzionx] SIGHUP — config hot-reload complete (PID=${process.pid})`);
|
|
188
259
|
} catch (e) {
|
|
189
|
-
console.error(
|
|
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('[
|
|
268
|
+
console.error('[fuzionx] uncaughtException:', err.message);
|
|
198
269
|
});
|
|
199
270
|
process.on('unhandledRejection', (reason) => {
|
|
200
|
-
console.error('[
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
297
|
+
this._globalChain = globalMw;
|
|
298
|
+
this._chainCache = new Map();
|
|
213
299
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
m.path === null || req.url.startsWith(m.path)
|
|
217
|
-
);
|
|
300
|
+
// 에러 핸들러도 사전 계산
|
|
301
|
+
this._errorChain = this._errorHandlers.map(m => m.handler);
|
|
218
302
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
chain
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
46
|
+
this._json = this.body ? JSON.parse(this.body) : null;
|
|
34
47
|
} catch (_) {
|
|
35
|
-
|
|
48
|
+
this._json = null;
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
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 —
|
|
2
|
+
* Middleware — 동기 + 비동기 미들웨어 체인 엔진.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 >=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.1.1",
|
|
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.
|
|
21
|
+
"@fuzionx/bridge": "^0.1.1"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"index.js",
|
package/types/index.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
// TypeScript 타입 정의 —
|
|
1
|
+
// TypeScript 타입 정의 — fuzionx framework
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface FuzionXOptions {
|
|
4
4
|
config: string;
|
|
5
5
|
port?: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export interface
|
|
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
|
|
26
|
-
status(code: number):
|
|
27
|
-
json(data: any):
|
|
28
|
-
send(text: string):
|
|
29
|
-
html(content: string):
|
|
30
|
-
redirect(url: string, code?: number):
|
|
31
|
-
header(key: string, value: string):
|
|
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:
|
|
36
|
-
export type ErrorHandler = (err: Error, req:
|
|
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
|
|
81
|
-
|
|
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?:
|
|
163
|
+
export declare function createApp(options?: FuzionXOptions): FuzionXApp;
|
|
164
|
+
|