@alloy-framework/core 0.4.0 β†’ 0.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alloy-framework/core",
3
- "version": "0.4.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "Alloy Framework β€” High-performance Node.js framework built on Rust N-API native engine",
6
6
  "main": "src/index.js",
@@ -22,8 +22,8 @@
22
22
  "wasm/"
23
23
  ],
24
24
  "dependencies": {
25
- "@alloy-framework/rust": "0.4.0",
26
- "@alloy-framework/client-wasm": "0.4.0",
25
+ "@alloy-framework/rust": "0.15.0",
26
+ "@alloy-framework/client-wasm": "0.15.0",
27
27
  "glob": "^13.0.0",
28
28
  "joi": "^18.0.2"
29
29
  },
@@ -1,610 +0,0 @@
1
- import native from '@alloy-framework/rust';
2
- import { dirname, join, resolve } from 'path';
3
- import { fileURLToPath } from 'url';
4
- import { existsSync } from 'fs';
5
- import { glob } from 'glob';
6
- import { isMainThread, Worker, workerData } from 'worker_threads';
7
-
8
- import { AlloyWebSocket } from '../ws/AlloyWebSocket.js';
9
- import { AlloyConfig } from '../config/AlloyConfig.js';
10
- import { AlloyRouter } from '../http/AlloyRouter.js';
11
- import { AlloyController } from '../http/AlloyController.js';
12
- import { AlloyError } from '../util/AlloyError.js';
13
-
14
- /**
15
- * πŸš€ Alloy μ•± λΆ€νŠΈμŠ€νŠΈλž˜νΌ
16
- * Rust λ„€μ΄ν‹°λΈŒ μ„œλ²„λ₯Ό λΆ€νŒ…ν•˜κ³ , WS 라이프사이클/λΌμš°ν„°/μ›Œμ»€/ν”ŒλŸ¬κ·ΈμΈμ„ μ˜€μΌ€μŠ€νŠΈλ ˆμ΄μ…˜ν•©λ‹ˆλ‹€.
17
- *
18
- * λ§ˆμŠ€ν„° μŠ€λ ˆλ“œ: bootSystem β†’ WS 등둝 β†’ Worker N개 spawn β†’ startServer
19
- * μ›Œμ»€ μŠ€λ ˆλ“œ: discoverRoutes β†’ registerWorker(id, callback)
20
- *
21
- * @example
22
- * import { AlloyApp } from '@alloy-framework/core';
23
- *
24
- * class MyApp extends AlloyApp {
25
- * constructor() {
26
- * super(import.meta.url);
27
- * this.ws({
28
- * onConnect(socket) { console.log('Connected:', socket.id); },
29
- * onMessage(socket, msg) { socket.broadcast(msg); },
30
- * onDisconnect(socket) { console.log('Disconnected:', socket.id); },
31
- * });
32
- * }
33
- * }
34
- *
35
- * const app = new MyApp();
36
- * await app.boot();
37
- * await app.start();
38
- */
39
- export class AlloyApp {
40
- /**
41
- * @param {string} importMetaUrl - import.meta.url (μ•± μ§„μž…μ  μœ„μΉ˜ ν•΄μ„μš©)
42
- */
43
- constructor(importMetaUrl) {
44
- /** @type {object} Rust N-API λ„€μ΄ν‹°λΈŒ λͺ¨λ“ˆ */
45
- this.native = native;
46
- /** @type {string} import.meta.url 원본 (Worker spawn μ‹œ μ‚¬μš©) */
47
- this._importMetaUrl = importMetaUrl;
48
- /** @type {string} μ•± 파일 μ ˆλŒ€κ²½λ‘œ (Worker spawn λŒ€μƒ) */
49
- this.filename = fileURLToPath(importMetaUrl);
50
- /** @type {string} μ•± 루트 디렉토리 */
51
- this.appDir = dirname(this.filename);
52
- /** @type {Map<string, AlloyWebSocket>} 둜컬 WS ν΄λΌμ΄μ–ΈνŠΈ λ§΅ */
53
- this.clients = new Map();
54
- /** @type {Array<object>} λ“±λ‘λœ ν”ŒλŸ¬κ·ΈμΈ λͺ©λ‘ */
55
- this._plugins = [];
56
- /** @type {object|null} WS 라이프사이클 ν•Έλ“€λŸ¬ */
57
- this._wsHandlers = null;
58
- /** @type {AlloyRouter|null} λΌμš°ν„° μΈμŠ€ν„΄μŠ€ */
59
- this.router = null;
60
- /** @type {Map<number, Worker>} μ›Œμ»€ μΈμŠ€ν„΄μŠ€ λ§΅ (λ§ˆμŠ€ν„° μ „μš©) */
61
- this.workers = new Map();
62
- /** @type {Map<number, number[]>} μ›Œμ»€λ³„ μž¬μ‹œμž‘ νƒ€μž„μŠ€νƒ¬ν”„ */
63
- this._workerRestartTimestamps = new Map();
64
- /** @type {object} 라이프사이클 ν›… */
65
- this.hooks = {
66
- onBeforeBoot: async () => {},
67
- onAfterBoot: async () => {},
68
- };
69
- }
70
-
71
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72
- // πŸ”Œ ν”ŒλŸ¬κ·ΈμΈ μ‹œμŠ€ν…œ
73
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
-
75
- /**
76
- * ν”ŒλŸ¬κ·ΈμΈ 등둝
77
- * @param {object} plugin - { name, onRequest?, onResponse?, onBoot?, ... }
78
- * @returns {this}
79
- */
80
- use(plugin) {
81
- if (!plugin || !plugin.name) {
82
- throw new AlloyError('PLUGIN_INVALID', 'ν”ŒλŸ¬κ·ΈμΈμ€ name 속성이 ν•„μˆ˜μž…λ‹ˆλ‹€', 500);
83
- }
84
- this._plugins.push(plugin);
85
- return this;
86
- }
87
-
88
- /**
89
- * λ“±λ‘λœ ν”ŒλŸ¬κ·ΈμΈ 쑰회
90
- * @param {string} name
91
- * @returns {object|undefined}
92
- */
93
- getPlugin(name) {
94
- return this._plugins.find(p => p.name === name);
95
- }
96
-
97
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
98
- // πŸ”Œ WebSocket ν•Έλ“€λŸ¬
99
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100
-
101
- /**
102
- * WebSocket 라이프사이클 ν•Έλ“€λŸ¬ 등둝
103
- * @param {object} handlers - { onConnect, onMessage, onDisconnect }
104
- * @returns {this}
105
- */
106
- ws(handlers) {
107
- this._wsHandlers = handlers;
108
- return this;
109
- }
110
-
111
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
112
- // πŸš€ λΆ€νŠΈ
113
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
114
-
115
- /**
116
- * μ•± λΆ€νŠΈ β€” λ§ˆμŠ€ν„°/μ›Œμ»€ λΆ„κΈ°
117
- * λ§ˆμŠ€ν„°: bootSystem + WS + μŠ€ν‚€λ§ˆ + ν”ŒλŸ¬κ·ΈμΈ
118
- * μ›Œμ»€: 라우트 λ””μŠ€μ»€λ²„λ¦¬λ§Œ
119
- */
120
- async boot() {
121
- if (isMainThread) {
122
- await this._bootMaster();
123
- } else {
124
- await this._bootWorker();
125
- }
126
- }
127
-
128
- /**
129
- * μ„œλ²„ μ‹œμž‘ β€” λ§ˆμŠ€ν„°/μ›Œμ»€ λΆ„κΈ°
130
- * λ§ˆμŠ€ν„°: μ›Œμ»€ N개 spawn + Rust μ„œλ²„ μ‹œμž‘
131
- * μ›Œμ»€: registerWorker(id, callback)
132
- */
133
- async start() {
134
- if (isMainThread) {
135
- await this._startMaster();
136
- } else {
137
- this._startWorker();
138
- }
139
- }
140
-
141
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
142
- // πŸ”‘ λ§ˆμŠ€ν„° μŠ€λ ˆλ“œ
143
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
144
-
145
- /**
146
- * λ§ˆμŠ€ν„° λΆ€νŠΈ β€” λ„€μ΄ν‹°λΈŒ μ‹œμŠ€ν…œ + WS + μŠ€ν‚€λ§ˆ + ν”ŒλŸ¬κ·ΈμΈ
147
- * @private
148
- */
149
- async _bootMaster() {
150
- // 1. 라이프사이클 ν›…
151
- await this.hooks.onBeforeBoot();
152
-
153
- // 2. console을 λ„€μ΄ν‹°λΈŒ 둜거둜 λ¦¬λ‹€μ΄λ ‰νŠΈ
154
- this._redirectConsole();
155
-
156
- // 3. alloy.conf 경둜 탐색 + λ„€μ΄ν‹°λΈŒ μ‹œμŠ€ν…œ λΆ€νŠΈ
157
- const configPath = join(this.appDir, 'alloy.conf');
158
- this.native.bootSystem(existsSync(configPath) ? configPath : undefined);
159
-
160
- // 4. ν”ŒλŸ¬κ·ΈμΈ onBoot ν›… μ‹€ν–‰
161
- for (const plugin of this._plugins) {
162
- if (typeof plugin.onBoot === 'function') {
163
- await plugin.onBoot(this);
164
- }
165
- }
166
-
167
- // 5. λΌμš°ν„° μ΄ˆκΈ°ν™” + κΈ€λ‘œλ²Œ 미듀웨어 + 라우트 Auto-Discovery
168
- this.router = new AlloyRouter(this);
169
- if (typeof this.middleware === 'function') {
170
- this.middleware(this.router);
171
- }
172
- await this._discoverRoutes();
173
-
174
- // 6. μŠ€ν‚€λ§ˆ 동기화 (models/ 쑴재 μ‹œ)
175
- await this._syncSchemas();
176
-
177
- // 7. WS 라이프사이클 등둝 (λ§ˆμŠ€ν„° μ „μš©)
178
- if (this._wsHandlers) {
179
- this._registerWsLifecycle();
180
- }
181
-
182
- // 8. 라이프사이클 ν›…
183
- await this.hooks.onAfterBoot();
184
-
185
- console.info('βœ… AlloyApp Booted Successfully');
186
- }
187
-
188
- /**
189
- * λ§ˆμŠ€ν„° μ„œλ²„ μ‹œμž‘ β€” μ›Œμ»€ spawn + Rust μ„œλ²„ μ‹œμž‘
190
- * @private
191
- */
192
- async _startMaster() {
193
- // 1. AlloyConfigμ—μ„œ μ›Œμ»€ 수 쑰회
194
- const workerCount = AlloyConfig.get('server.workers', 4);
195
-
196
- // 2. μ›Œμ»€ μŠ€λ ˆλ“œ N개 생성
197
- const MAX_RESTARTS_PER_MINUTE = 5;
198
- console.debug(`πŸ”§ Spawning ${workerCount} Worker(s)...`);
199
-
200
- for (let i = 1; i <= workerCount; i++) {
201
- this._spawnWorker(i, workerCount, MAX_RESTARTS_PER_MINUTE);
202
- }
203
-
204
- // 3. Rust μ„œλ²„ μ‹œμž‘ (비동기, 별도 μŠ€λ ˆλ“œ)
205
- this.native.startServer();
206
-
207
- // 4. Graceful Shutdown 핸듀링
208
- this._registerShutdown();
209
-
210
- // 5. 메인 μŠ€λ ˆλ“œ μœ μ§€ (μ›Œμ»€ 관리 + SIGINT ν•Έλ“€λŸ¬κ°€ λ™μž‘ν•˜λ €λ©΄ 이벀트 루프 ν•„μš”)
211
- setInterval(() => {}, 1000 * 60 * 60);
212
- }
213
-
214
- /**
215
- * μ›Œμ»€ μŠ€λ ˆλ“œ 생성 (비정상 μ’…λ£Œ μ‹œ μžλ™ μž¬μ‹œμž‘)
216
- * @param {number} id - μ›Œμ»€ ID (1-based)
217
- * @param {number} totalWorkers - 총 μ›Œμ»€ 수
218
- * @param {number} maxRestartsPerMinute - λΆ„λ‹Ή μ΅œλŒ€ μž¬μ‹œμž‘ 횟수
219
- * @private
220
- */
221
- _spawnWorker(id, totalWorkers, maxRestartsPerMinute = 5) {
222
- const worker = new Worker(this.filename, {
223
- workerData: { id, totalWorkers },
224
- stdout: 'inherit',
225
- stderr: 'inherit',
226
- });
227
-
228
- this.workers.set(id, worker);
229
-
230
- worker.on('online', () => {
231
- console.debug(`βœ… Worker #${id} online`);
232
- });
233
-
234
- worker.on('error', (err) => {
235
- console.error(`❌ Worker #${id} error:`, err.message);
236
- });
237
-
238
- worker.on('exit', (code) => {
239
- this.workers.delete(id);
240
-
241
- // shutdown 쀑이면 terminate().then()μ—μ„œ 이미 λ‘œκΉ…ν•˜λ―€λ‘œ μŠ€ν‚΅
242
- if (this._shuttingDown) return;
243
-
244
- if (code !== 0) {
245
- console.warn(`⚠️ Worker #${id} exited with code ${code}`);
246
-
247
- // κ³Όλ„ν•œ μž¬μ‹œμž‘ λ°©μ§€ (λΆ„λ‹Ή μ΅œλŒ€ 횟수 체크)
248
- const now = Date.now();
249
- const timestamps = this._workerRestartTimestamps.get(id) || [];
250
- const recentTimestamps = timestamps.filter(t => now - t < 60000);
251
-
252
- if (recentTimestamps.length >= maxRestartsPerMinute) {
253
- console.error(`🚨 Worker #${id}: ${maxRestartsPerMinute}회/λΆ„ μž¬μ‹œμž‘ ν•œλ„ 초과. μž¬μ‹œμž‘ 쀑단.`);
254
- this._workerRestartTimestamps.set(id, recentTimestamps);
255
- return;
256
- }
257
-
258
- recentTimestamps.push(now);
259
- this._workerRestartTimestamps.set(id, recentTimestamps);
260
-
261
- // 1초 ν›„ μž¬μ‹œμž‘
262
- setTimeout(() => {
263
- console.warn(`πŸ”„ Restarting Worker #${id}...`);
264
- this._spawnWorker(id, totalWorkers, maxRestartsPerMinute);
265
- }, 1000);
266
- } else {
267
- console.warn(`βœ… Worker #${id} exited normally`);
268
- }
269
- });
270
- }
271
-
272
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
273
- // οΏ½ μ›Œμ»€ μŠ€λ ˆλ“œ
274
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
275
-
276
- /**
277
- * μ›Œμ»€ λΆ€νŠΈ β€” console λ¦¬λ‹€μ΄λ ‰νŠΈ + 라우트 λ””μŠ€μ»€λ²„λ¦¬
278
- * @private
279
- */
280
- async _bootWorker() {
281
- this._redirectConsole();
282
-
283
- // μ›Œμ»€μ—μ„œλ„ 라우트λ₯Ό λ””μŠ€μ»€λ²„ (ν•Έλ“€λŸ¬ λ§΅ κ΅¬μΆ•μš©)
284
- this.router = new AlloyRouter(this);
285
- if (typeof this.middleware === 'function') {
286
- this.middleware(this.router);
287
- }
288
- await this._discoverRoutes();
289
-
290
- // unhandled rejection λ°©μ–΄ (μ›Œμ»€ ν¬λž˜μ‹œ λ°©μ§€)
291
- process.on('unhandledRejection', (reason) => {
292
- console.error(`❌ Worker #${workerData.id} Unhandled Rejection:`, reason);
293
- });
294
- }
295
-
296
- /**
297
- * μ›Œμ»€ μ„œλ²„ μ‹œμž‘ β€” Rust에 registerWorker 등둝
298
- * @private
299
- */
300
- _startWorker() {
301
- const { id } = workerData;
302
- const app = this;
303
-
304
- this.native.registerWorker(id, async (req, completer) => {
305
- try {
306
- if (req.handlerId) {
307
- const routeData = app.router.getHandler(req.handlerId);
308
- if (routeData) {
309
- await app._dispatch(routeData, req, completer);
310
- } else {
311
- console.error(`[Worker #${id}] Handler ID ${req.handlerId} not found`);
312
- completer.done({ status: 500, headers: {}, body: 'Handler Not Found' });
313
- }
314
- } else {
315
- completer.done({ status: 404, headers: {}, body: 'No Handler ID' });
316
- }
317
- } catch (err) {
318
- console.error(`❌ [Worker #${id}] Error:`, err.message, err);
319
- try {
320
- const accept = (req.headers?.accept || '').toLowerCase();
321
- const statusCode = err.statusCode || 500;
322
-
323
- if (accept.includes('text/html')) {
324
- // λΈŒλΌμš°μ € μš”μ²­ β†’ Rust Tera ν…œν”Œλ¦Ώ λ Œλ”λ§ μœ„μž„
325
- const template = statusCode === 404 ? '404.html' : 'error.html';
326
- completer.done({
327
- status: statusCode,
328
- headers: {},
329
- body: '',
330
- template,
331
- context: JSON.stringify({
332
- status: statusCode,
333
- code: err.code || 'INTERNAL_ERROR',
334
- message: err.message || 'Internal Server Error',
335
- timestamp: Date.now(),
336
- }),
337
- });
338
- } else {
339
- // API/JSON μš”μ²­ β†’ JSON μ—λŸ¬ 응닡
340
- completer.done({
341
- status: statusCode,
342
- headers: { 'Content-Type': 'application/json' },
343
- body: JSON.stringify({
344
- status: statusCode,
345
- code: err.code || 'INTERNAL_ERROR',
346
- message: err.message || 'Internal Server Error',
347
- timestamp: Date.now(),
348
- }),
349
- });
350
- }
351
- } catch { /* completer 이미 μ†ŒλΉ„λ¨ */ }
352
- }
353
- });
354
-
355
- console.debug(`πŸ‘· Worker #${id} registered`);
356
- }
357
-
358
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
359
- // οΏ½πŸ”’ λ‚΄λΆ€ λ©”μ„œλ“œ
360
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
361
-
362
- /**
363
- * console을 λ„€μ΄ν‹°λΈŒ 둜거둜 λ¦¬λ‹€μ΄λ ‰νŠΈ
364
- * @private
365
- */
366
- _redirectConsole() {
367
- if (!this.native.logInfo) return;
368
-
369
- const fmt = (args) => args.map(a => {
370
- if (a instanceof Error) return `${a.message}${a.stack ? '\n' + a.stack : ''}`;
371
- if (typeof a === 'object') { try { return JSON.stringify(a); } catch { return String(a); } }
372
- return String(a);
373
- }).join(' ');
374
-
375
- console.log = (...args) => this.native.logInfo(fmt(args));
376
- console.info = (...args) => this.native.logInfo(fmt(args));
377
- console.warn = (...args) => this.native.logWarn(fmt(args));
378
- console.error = (...args) => this.native.logError(fmt(args));
379
- console.debug = (...args) => this.native.logDebug(fmt(args));
380
- }
381
-
382
- /**
383
- * 라우트 파일 μžλ™ λ””μŠ€μ»€λ²„λ¦¬ (controllers/*Route.js)
384
- * @private
385
- */
386
- async _discoverRoutes() {
387
- const controllersDir = resolve(this.appDir, 'controllers');
388
- if (!existsSync(controllersDir)) return;
389
-
390
- // *Route.js νŒ¨ν„΄ μŠ€μΊ”
391
- const routeFiles = await glob('**/*Route.js', { cwd: controllersDir });
392
- routeFiles.sort(); // ID 결정적 생성을 μœ„ν•΄ μ •λ ¬
393
-
394
- for (const file of routeFiles) {
395
- const filePath = join(controllersDir, file);
396
- try {
397
- const fileUrl = `file://${filePath}`; // ESM은 file:// URL ν•„μˆ˜
398
- const routeModule = await import(fileUrl);
399
- // default export = function(router) νŒ¨ν„΄
400
- if (typeof routeModule.default === 'function') {
401
- routeModule.default(this.router);
402
- }
403
- } catch (e) {
404
- console.warn(`⚠️ 라우트 λ‘œλ“œ μ‹€νŒ¨: ${file}`, e.message);
405
- }
406
- }
407
- }
408
-
409
- /**
410
- * λͺ¨λΈ μŠ€ν‚€λ§ˆ μžλ™ 동기화
411
- * @private
412
- */
413
- async _syncSchemas() {
414
- const modelsDir = join(this.appDir, 'models');
415
- if (!existsSync(modelsDir)) return;
416
-
417
- const files = await glob(join(modelsDir, '**/*.js'));
418
- for (const file of files) {
419
- try {
420
- const mod = await import(`file://${file}`);
421
- const ModelClass = mod.default || Object.values(mod)[0];
422
- // alloySchema λ˜λŠ” schema (AlloyGoose ν˜•μ‹: name + fields) λ‘˜ λ‹€ 인식
423
- const gooseSchema = ModelClass?.alloySchema
424
- || (ModelClass?.schema?.name && ModelClass?.schema?.fields ? ModelClass.schema : null);
425
- if (gooseSchema) {
426
- this.native.addSchema(JSON.stringify(gooseSchema));
427
- }
428
- } catch (e) {
429
- console.warn(`⚠️ λͺ¨λΈ λ‘œλ“œ μ‹€νŒ¨: ${file}`, e.message);
430
- }
431
- }
432
-
433
- if (this.native.syncSchema) {
434
- try { await this.native.syncSchema('{}'); } catch (e) { console.warn('⚠️ μŠ€ν‚€λ§ˆ 동기화 μ‹€νŒ¨:', e.message); }
435
- }
436
- }
437
-
438
- /**
439
- * WS 라이프사이클 콜백 등둝 (λ§ˆμŠ€ν„°μ—μ„œλ§Œ μ‹€ν–‰)
440
- * @private
441
- */
442
- _registerWsLifecycle() {
443
- if (!this.native.registerWsLifecycle) return;
444
-
445
- const handlers = this._wsHandlers;
446
- const app = this;
447
-
448
- this.native.registerWsLifecycle(async (jsonStr) => {
449
- try {
450
- const event = JSON.parse(jsonStr);
451
- const { type, id, data } = event;
452
-
453
- if (type === 'CONNECT') {
454
- const connectionData = data ? JSON.parse(data) : {};
455
- const socket = new AlloyWebSocket(id, app.native, connectionData);
456
- app.clients.set(id, socket);
457
- if (handlers.onConnect) await handlers.onConnect.call(app, socket);
458
- } else if (type === 'MESSAGE') {
459
- const socket = app.clients.get(id) || new AlloyWebSocket(id, app.native);
460
- if (handlers.onMessage) await handlers.onMessage.call(app, socket, data);
461
- } else if (type === 'DISCONNECT') {
462
- const connectionData = data ? JSON.parse(data) : {};
463
- const socket = app.clients.get(id) || new AlloyWebSocket(id, app.native, connectionData);
464
- if (handlers.onDisconnect) await handlers.onDisconnect.call(app, socket);
465
- app.clients.delete(id);
466
- }
467
- } catch (e) {
468
- console.error('❌ WS Event 처리 였λ₯˜:', e.message);
469
- }
470
- });
471
- }
472
-
473
- /**
474
- * μš”μ²­ λ””μŠ€νŒ¨μΉ˜ β€” 미듀웨어 체인 + ν•Έλ“€λŸ¬ μ‹€ν–‰
475
- * @private
476
- */
477
- async _dispatch(routeData, req, completer) {
478
- const res = new AlloyController(req, completer);
479
- res.app = this;
480
-
481
- const { handler, middlewares: routeMiddlewares, validator } = routeData;
482
-
483
- // Joi 검증
484
- if (validator) {
485
- if (validator.body && typeof req.body === 'string' && req.body.trim()) {
486
- try { req.parsedBody = JSON.parse(req.body); } catch {
487
- const params = new URLSearchParams(req.body);
488
- req.parsedBody = Object.fromEntries(params.entries());
489
- }
490
- }
491
- if (validator.body) {
492
- const { error, value } = validator.body.validate(req.parsedBody || {}, { abortEarly: false, stripUnknown: true });
493
- if (error) return res.error(error.details.map(d => d.message).join(', '), 'VALIDATION_ERROR', 400);
494
- req.parsedBody = value;
495
- }
496
- }
497
-
498
- // 미듀웨어 체인
499
- const globalMiddlewares = this.router.middlewares || [];
500
- const allMiddlewares = [...globalMiddlewares, ...(routeMiddlewares || [])];
501
-
502
- let index = -1;
503
- const next = async () => {
504
- index++;
505
- if (index < allMiddlewares.length) {
506
- const mw = allMiddlewares[index];
507
- if (typeof mw.handle === 'function') await mw.handle(req, res, next);
508
- else if (typeof mw === 'function') await mw(req, res, next);
509
- else await next();
510
- } else {
511
- // μ΅œμ’… ν•Έλ“€λŸ¬ μ‹€ν–‰
512
- await this._executeHandler(handler, req, completer, res);
513
- }
514
- };
515
-
516
- if (allMiddlewares.length > 0) {
517
- await next();
518
- } else {
519
- await this._executeHandler(handler, req, completer, res);
520
- }
521
- }
522
-
523
- /**
524
- * ν•Έλ“€λŸ¬ μ‹€ν–‰ β€” 클래슀/ν•¨μˆ˜/λ””μŠ€ν¬λ¦½ν„° νŒ¨ν„΄ 지원
525
- * @private
526
- */
527
- async _executeHandler(handler, req, completer, existingRes = null) {
528
- // 1. { controller, method } λ””μŠ€ν¬λ¦½ν„° νŒ¨ν„΄
529
- if (handler && typeof handler === 'object' && handler.controller) {
530
- const controller = new handler.controller(req, completer);
531
- controller.app = this;
532
- // 미듀웨어 헀더 전달
533
- if (existingRes?._headers) {
534
- for (const [k, v] of Object.entries(existingRes._headers)) {
535
- if (!controller._headers[k]) controller._headers[k] = v;
536
- }
537
- }
538
- const methodName = handler.method || 'index';
539
- if (typeof controller[methodName] === 'function') {
540
- await controller[methodName]();
541
- } else {
542
- completer.done({ status: 500, headers: {}, body: `Method '${methodName}' not found` });
543
- }
544
- return;
545
- }
546
-
547
- // 2. 클래슀 λ˜λŠ” ν•¨μˆ˜
548
- if (typeof handler === 'function') {
549
- if (handler.prototype && handler.prototype.index) {
550
- // 컨트둀러 클래슀 β†’ index() 호좜
551
- const controller = new handler(req, completer);
552
- controller.app = this;
553
- await controller.index();
554
- } else {
555
- // 인라인 ν•Έλ“€λŸ¬ ν•¨μˆ˜
556
- if (existingRes) {
557
- await handler(req, existingRes);
558
- } else {
559
- const controller = new AlloyController(req, completer);
560
- controller.app = this;
561
- await handler(req, controller);
562
- }
563
- }
564
- }
565
- }
566
-
567
- /**
568
- * Graceful Shutdown 핸듀링
569
- * @private
570
- */
571
- _registerShutdown() {
572
- /** @type {boolean} shutdown μ§„ν–‰ 쀑 ν”Œλž˜κ·Έ β€” μ›Œμ»€ exit ν•Έλ“€λŸ¬ 쀑볡 λ°©μ§€ */
573
- this._shuttingDown = false;
574
-
575
- const shutdown = async (signal) => {
576
- // 쀑볡 호좜 λ°©μ§€ (SIGINT + SIGTERM λ™μ‹œ μˆ˜μ‹  μ‹œ)
577
- if (this._shuttingDown) return;
578
- this._shuttingDown = true;
579
-
580
- console.warn(`πŸ›‘ ${signal} received. Shutting down...`);
581
-
582
- // 1. Rust μ„œλ²„ μ’…λ£Œ μ‹œκ·Έλ„ (NATS leave λ°œν–‰ 포함)
583
- try {
584
- if (this.native.shutdown) this.native.shutdown();
585
- } catch (err) {
586
- console.error('❌ Shutdown signal failed:', err.message);
587
- }
588
-
589
- // 2. λͺ¨λ“  μ›Œμ»€ μ’…λ£Œ (μ΅œλŒ€ 5초 λŒ€κΈ°)
590
- const terminatePromises = [];
591
- for (const [id, worker] of this.workers) {
592
- terminatePromises.push(
593
- worker.terminate().then(() => {
594
- console.warn(`βœ… Worker #${id} terminated`);
595
- }).catch(err => {
596
- console.error(`❌ Worker #${id} terminate failed:`, err.message);
597
- })
598
- );
599
- }
600
- await Promise.allSettled(terminatePromises);
601
-
602
- // 3. ν”„λ‘œμ„ΈμŠ€ μ’…λ£Œ
603
- console.warn('πŸ‘‹ Goodbye!');
604
- process.exit(0);
605
- };
606
-
607
- process.on('SIGINT', () => shutdown('SIGINT'));
608
- process.on('SIGTERM', () => shutdown('SIGTERM'));
609
- }
610
- }