@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 +3 -3
- package/src/app/AlloyApp.js +0 -610
- package/src/config/AlloyConfig.js +0 -131
- package/src/data/AlloyModel.js +0 -405
- package/src/data/AlloyMongoModel.js +0 -265
- package/src/data/AlloyRedisModel.js +0 -158
- package/src/data/GooseValidator.js +0 -168
- package/src/data/index.js +0 -6
- package/src/data/validationHelper.js +0 -79
- package/src/http/AlloyController.js +0 -322
- package/src/http/AlloyRouter.js +0 -130
- package/src/http/Validator.js +0 -52
- package/src/http/index.js +0 -6
- package/src/index.d.ts +0 -345
- package/src/index.js +0 -43
- package/src/security/AlloyCrypto.js +0 -96
- package/src/security/AlloySession.js +0 -81
- package/src/security/index.js +0 -5
- package/src/service/AlloyService.js +0 -171
- package/src/util/AlloyError.js +0 -23
- package/src/util/Pagination.js +0 -39
- package/src/util/index.js +0 -6
- package/src/ws/AlloyWebSocket.js +0 -130
- package/wasm/alloy_client_wasm.d.ts +0 -174
- package/wasm/alloy_client_wasm.js +0 -1215
- package/wasm/alloy_client_wasm_bg.wasm +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alloy-framework/core",
|
|
3
|
-
"version": "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.
|
|
26
|
-
"@alloy-framework/client-wasm": "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
|
},
|
package/src/app/AlloyApp.js
DELETED
|
@@ -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
|
-
}
|