@fuzionx/framework 0.1.43 → 0.1.45
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/README.md +501 -501
- package/bin/fx.js +12 -12
- package/cli/db-sync.js +100 -100
- package/cli/index.js +494 -494
- package/cli/templates/make/app/controllers/HomeController.js +14 -14
- package/cli/templates/make/app/routes/api.js +7 -7
- package/cli/templates/make/app/routes/web.js +5 -5
- package/cli/templates/make/app/views/default/errors/404.html +11 -11
- package/cli/templates/make/app/views/default/errors/500.html +14 -14
- package/cli/templates/make/app/views/default/layouts/main.html +22 -22
- package/cli/templates/make/app/views/default/pages/home.html +11 -11
- package/cli/templates/make/controller.js.tpl +40 -40
- package/cli/templates/make/event.js.tpl +8 -8
- package/cli/templates/make/job.js.tpl +10 -10
- package/cli/templates/make/middleware.js.tpl +10 -10
- package/cli/templates/make/model.js.tpl +15 -15
- package/cli/templates/make/service.js.tpl +15 -15
- package/cli/templates/make/task.js.tpl +15 -15
- package/cli/templates/make/test.js.tpl +7 -7
- package/cli/templates/make/worker.js.tpl +14 -14
- package/cli/templates/make/ws.js.tpl +18 -18
- package/index.js +67 -67
- package/lib/core/AppError.js +46 -46
- package/lib/core/Application.js +1006 -1006
- package/lib/core/AutoLoader.js +227 -227
- package/lib/core/Base.js +64 -64
- package/lib/core/Config.js +331 -331
- package/lib/core/Context.js +484 -484
- package/lib/database/ConnectionManager.js +208 -208
- package/lib/database/MariaModel.js +29 -29
- package/lib/database/Model.js +247 -247
- package/lib/database/ModelRegistry.js +72 -72
- package/lib/database/MongoModel.js +232 -232
- package/lib/database/Pagination.js +37 -37
- package/lib/database/PostgreModel.js +29 -29
- package/lib/database/QueryBuilder.js +172 -172
- package/lib/database/SQLiteModel.js +27 -27
- package/lib/database/SqlModel.js +257 -257
- package/lib/database/SqlQueryBuilder.js +332 -332
- package/lib/helpers/CryptoHelper.js +48 -48
- package/lib/helpers/FileHelper.js +61 -61
- package/lib/helpers/HashHelper.js +39 -39
- package/lib/helpers/I18nHelper.js +174 -174
- package/lib/helpers/Logger.js +108 -108
- package/lib/helpers/MediaHelper.js +84 -84
- package/lib/http/Controller.js +34 -34
- package/lib/http/ErrorHandler.js +136 -136
- package/lib/http/Middleware.js +43 -43
- package/lib/http/Router.js +109 -109
- package/lib/http/Validation.js +125 -125
- package/lib/middleware/apiAuth.js +79 -79
- package/lib/middleware/auth.js +42 -42
- package/lib/middleware/bodyParser.js +19 -19
- package/lib/middleware/cors.js +47 -47
- package/lib/middleware/csrf.js +32 -32
- package/lib/middleware/index.js +13 -13
- package/lib/middleware/session.js +27 -27
- package/lib/middleware/theme.js +20 -20
- package/lib/realtime/RoomManager.js +85 -85
- package/lib/realtime/WsHandler.js +107 -107
- package/lib/schedule/Job.js +38 -38
- package/lib/schedule/Queue.js +103 -103
- package/lib/schedule/Scheduler.js +171 -171
- package/lib/schedule/Task.js +39 -39
- package/lib/schedule/WorkerPool.js +225 -225
- package/lib/services/EventBus.js +94 -94
- package/lib/services/Service.js +261 -261
- package/lib/services/Storage.js +112 -112
- package/lib/utilities/ArrUtil.js +112 -112
- package/lib/utilities/DateUtil.js +98 -98
- package/lib/utilities/FunctionUtil.js +119 -119
- package/lib/utilities/NumUtil.js +75 -75
- package/lib/utilities/ObjectUtil.js +170 -170
- package/lib/utilities/PaginationUtil.js +81 -81
- package/lib/utilities/StrUtil.js +105 -105
- package/lib/utilities/index.js +18 -18
- package/lib/view/OpenAPI.js +231 -231
- package/lib/view/View.js +83 -83
- package/package.json +2 -2
- package/testing/index.js +232 -232
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WorkerPool — CPU 격리 워커 관리 (worker_threads)
|
|
3
|
-
*
|
|
4
|
-
* Scheduler("언제"), Queue("나중에")와 함께 "별도로" 실행을 담당.
|
|
5
|
-
* CPU-heavy 작업을 별도 스레드에서 실행하여 메인 이벤트 루프 보호.
|
|
6
|
-
*
|
|
7
|
-
* workers/ 폴더 컨벤션:
|
|
8
|
-
* workers/
|
|
9
|
-
* ├── csv-parser.js
|
|
10
|
-
* ├── image-resize.js
|
|
11
|
-
* └── excel/
|
|
12
|
-
* └── csv-parser.js
|
|
13
|
-
*
|
|
14
|
-
* @see docs/framework/10-scheduler-queue.md
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* // 단축 이름 (workers/ 폴더 기준)
|
|
18
|
-
* const result = await app.worker.run('csv-parser', { path: '/tmp/data.csv' });
|
|
19
|
-
* const result = await app.worker.run('excel/csv-parser', { path: '/tmp/sheet.xlsx' });
|
|
20
|
-
*
|
|
21
|
-
* // 절대경로도 가능
|
|
22
|
-
* const result = await app.worker.run('/opt/workers/custom.js', data);
|
|
23
|
-
*
|
|
24
|
-
* // Queue Task 내에서 조합
|
|
25
|
-
* class HeavyTask extends Task {
|
|
26
|
-
* async handle(data) {
|
|
27
|
-
* const result = await this.worker.run('file-processor', data);
|
|
28
|
-
* await this.db.Report.create(result);
|
|
29
|
-
* }
|
|
30
|
-
* }
|
|
31
|
-
*/
|
|
32
|
-
import { Worker, isMainThread } from 'node:worker_threads';
|
|
33
|
-
import path from 'node:path';
|
|
34
|
-
import fs from 'node:fs';
|
|
35
|
-
|
|
36
|
-
export default class WorkerPool {
|
|
37
|
-
/**
|
|
38
|
-
* @param {import('../core/Application.js').default} app
|
|
39
|
-
* @param {object} [opts]
|
|
40
|
-
* @param {number} [opts.timeout=30000] - 기본 타임아웃 (ms)
|
|
41
|
-
*/
|
|
42
|
-
constructor(app, opts = {}) {
|
|
43
|
-
this.app = app;
|
|
44
|
-
this._defaultTimeout = opts.timeout || 30000;
|
|
45
|
-
/** @type {Set<Worker>} 활성 워커 추적 */
|
|
46
|
-
this._activeWorkers = new Set();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 워커 스크립트 실행
|
|
51
|
-
*
|
|
52
|
-
* 이름 해석 규칙:
|
|
53
|
-
* - 절대경로 → 그대로 사용
|
|
54
|
-
* - 확장자 포함 (`./xxx.js`) → baseDir 상대경로
|
|
55
|
-
* - 단축 이름 (`csv-parser`, `excel/csv-parser`) → workers/ 폴더에서 .js/.mjs 자동 탐색
|
|
56
|
-
*
|
|
57
|
-
* @param {string} name - 워커 이름 또는 경로
|
|
58
|
-
* @param {*} [data] - workerData로 전달
|
|
59
|
-
* @param {object} [opts]
|
|
60
|
-
* @param {number} [opts.timeout] - 타임아웃 (ms)
|
|
61
|
-
* @param {Array} [opts.transferList] - ArrayBuffer 등 zero-copy 전송
|
|
62
|
-
* @returns {Promise<*>} 워커 결과
|
|
63
|
-
*/
|
|
64
|
-
run(name, data, opts = {}) {
|
|
65
|
-
const timeout = opts.timeout ?? this._defaultTimeout;
|
|
66
|
-
const resolved = this._resolve(name);
|
|
67
|
-
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
const worker = new Worker(resolved, {
|
|
70
|
-
workerData: data,
|
|
71
|
-
transferList: opts.transferList,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
this._activeWorkers.add(worker);
|
|
75
|
-
|
|
76
|
-
let timer;
|
|
77
|
-
let settled = false;
|
|
78
|
-
|
|
79
|
-
const settle = (fn, value) => {
|
|
80
|
-
if (settled) return;
|
|
81
|
-
settled = true;
|
|
82
|
-
clearTimeout(timer);
|
|
83
|
-
this._activeWorkers.delete(worker);
|
|
84
|
-
fn(value);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
worker.on('message', (result) => settle(resolve, result));
|
|
88
|
-
worker.on('error', (err) => settle(reject, err));
|
|
89
|
-
worker.on('exit', (code) => {
|
|
90
|
-
if (!settled) {
|
|
91
|
-
settle(reject, new Error(`Worker exited with code ${code}`));
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (timeout > 0) {
|
|
96
|
-
timer = setTimeout(() => {
|
|
97
|
-
worker.terminate();
|
|
98
|
-
settle(reject, new Error(`Worker timeout (${timeout}ms)`));
|
|
99
|
-
}, timeout);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 인라인 순수 함수를 워커에서 실행
|
|
106
|
-
*
|
|
107
|
-
* 함수는 직렬화 가능해야 함 (클로저/외부 참조 불가).
|
|
108
|
-
* 워커 내에서 require() 사용 가능.
|
|
109
|
-
*
|
|
110
|
-
* @param {Function} fn - (data) => result 형태의 순수 함수
|
|
111
|
-
* @param {*} [data] - 함수에 전달할 데이터
|
|
112
|
-
* @param {object} [opts]
|
|
113
|
-
* @param {number} [opts.timeout] - 타임아웃 (ms)
|
|
114
|
-
* @returns {Promise<*>} 함수 실행 결과
|
|
115
|
-
*/
|
|
116
|
-
exec(fn, data, opts = {}) {
|
|
117
|
-
const timeout = opts.timeout ?? this._defaultTimeout;
|
|
118
|
-
const fnStr = fn.toString();
|
|
119
|
-
|
|
120
|
-
// 인라인 워커 코드
|
|
121
|
-
const workerCode = `
|
|
122
|
-
const { parentPort, workerData } = require('worker_threads');
|
|
123
|
-
const fn = ${fnStr};
|
|
124
|
-
(async () => {
|
|
125
|
-
try {
|
|
126
|
-
const result = await fn(workerData);
|
|
127
|
-
parentPort.postMessage(result);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
throw err;
|
|
130
|
-
}
|
|
131
|
-
})();
|
|
132
|
-
`;
|
|
133
|
-
|
|
134
|
-
return new Promise((resolve, reject) => {
|
|
135
|
-
const worker = new Worker(workerCode, {
|
|
136
|
-
eval: true,
|
|
137
|
-
workerData: data,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
this._activeWorkers.add(worker);
|
|
141
|
-
|
|
142
|
-
let timer;
|
|
143
|
-
let settled = false;
|
|
144
|
-
|
|
145
|
-
const settle = (cb, value) => {
|
|
146
|
-
if (settled) return;
|
|
147
|
-
settled = true;
|
|
148
|
-
clearTimeout(timer);
|
|
149
|
-
this._activeWorkers.delete(worker);
|
|
150
|
-
cb(value);
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
worker.on('message', (result) => settle(resolve, result));
|
|
154
|
-
worker.on('error', (err) => settle(reject, err));
|
|
155
|
-
worker.on('exit', (code) => {
|
|
156
|
-
if (!settled) {
|
|
157
|
-
settle(reject, new Error(`Worker exited with code ${code}`));
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
if (timeout > 0) {
|
|
162
|
-
timer = setTimeout(() => {
|
|
163
|
-
worker.terminate();
|
|
164
|
-
settle(reject, new Error(`Worker timeout (${timeout}ms)`));
|
|
165
|
-
}, timeout);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* 활성 워커 수
|
|
172
|
-
* @returns {number}
|
|
173
|
-
*/
|
|
174
|
-
get active() {
|
|
175
|
-
return this._activeWorkers.size;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* 모든 활성 워커 종료 (graceful shutdown)
|
|
180
|
-
* @returns {Promise<void>}
|
|
181
|
-
*/
|
|
182
|
-
async terminate() {
|
|
183
|
-
const terminations = [];
|
|
184
|
-
for (const worker of this._activeWorkers) {
|
|
185
|
-
terminations.push(worker.terminate());
|
|
186
|
-
}
|
|
187
|
-
await Promise.allSettled(terminations);
|
|
188
|
-
this._activeWorkers.clear();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 워커 이름 → 절대경로 해석
|
|
193
|
-
*
|
|
194
|
-
* 규칙:
|
|
195
|
-
* 1) 절대경로 → 그대로
|
|
196
|
-
* 2) 확장자 포함 (./foo.js, ../foo.mjs) → baseDir 기준 상대경로
|
|
197
|
-
* 3) 단축 이름 (csv-parser, excel/csv-parser) → workers/ 폴더에서 탐색
|
|
198
|
-
* 탐색 순서: .js → .mjs
|
|
199
|
-
*
|
|
200
|
-
* @param {string} name
|
|
201
|
-
* @returns {string} 절대경로
|
|
202
|
-
* @private
|
|
203
|
-
*/
|
|
204
|
-
_resolve(name) {
|
|
205
|
-
// 1) 절대경로
|
|
206
|
-
if (path.isAbsolute(name)) return name;
|
|
207
|
-
|
|
208
|
-
const baseDir = this.app?.baseDir || '.';
|
|
209
|
-
|
|
210
|
-
// 2) 확장자 포함 → baseDir 상대경로
|
|
211
|
-
if (/\.\w+$/.test(name)) {
|
|
212
|
-
return path.resolve(baseDir, name);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 3) 단축 이름 → shared/workers/ 폴더 탐색
|
|
216
|
-
const workersDir = path.resolve(baseDir, 'shared/workers');
|
|
217
|
-
for (const ext of ['.js', '.mjs']) {
|
|
218
|
-
const candidate = path.join(workersDir, name + ext);
|
|
219
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// 기본값: shared/workers/{name}.js (존재하지 않아도 Worker 생성 시 에러)
|
|
223
|
-
return path.join(workersDir, name + '.js');
|
|
224
|
-
}
|
|
225
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* WorkerPool — CPU 격리 워커 관리 (worker_threads)
|
|
3
|
+
*
|
|
4
|
+
* Scheduler("언제"), Queue("나중에")와 함께 "별도로" 실행을 담당.
|
|
5
|
+
* CPU-heavy 작업을 별도 스레드에서 실행하여 메인 이벤트 루프 보호.
|
|
6
|
+
*
|
|
7
|
+
* workers/ 폴더 컨벤션:
|
|
8
|
+
* workers/
|
|
9
|
+
* ├── csv-parser.js
|
|
10
|
+
* ├── image-resize.js
|
|
11
|
+
* └── excel/
|
|
12
|
+
* └── csv-parser.js
|
|
13
|
+
*
|
|
14
|
+
* @see docs/framework/10-scheduler-queue.md
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // 단축 이름 (workers/ 폴더 기준)
|
|
18
|
+
* const result = await app.worker.run('csv-parser', { path: '/tmp/data.csv' });
|
|
19
|
+
* const result = await app.worker.run('excel/csv-parser', { path: '/tmp/sheet.xlsx' });
|
|
20
|
+
*
|
|
21
|
+
* // 절대경로도 가능
|
|
22
|
+
* const result = await app.worker.run('/opt/workers/custom.js', data);
|
|
23
|
+
*
|
|
24
|
+
* // Queue Task 내에서 조합
|
|
25
|
+
* class HeavyTask extends Task {
|
|
26
|
+
* async handle(data) {
|
|
27
|
+
* const result = await this.worker.run('file-processor', data);
|
|
28
|
+
* await this.db.Report.create(result);
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
import { Worker, isMainThread } from 'node:worker_threads';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
|
|
36
|
+
export default class WorkerPool {
|
|
37
|
+
/**
|
|
38
|
+
* @param {import('../core/Application.js').default} app
|
|
39
|
+
* @param {object} [opts]
|
|
40
|
+
* @param {number} [opts.timeout=30000] - 기본 타임아웃 (ms)
|
|
41
|
+
*/
|
|
42
|
+
constructor(app, opts = {}) {
|
|
43
|
+
this.app = app;
|
|
44
|
+
this._defaultTimeout = opts.timeout || 30000;
|
|
45
|
+
/** @type {Set<Worker>} 활성 워커 추적 */
|
|
46
|
+
this._activeWorkers = new Set();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 워커 스크립트 실행
|
|
51
|
+
*
|
|
52
|
+
* 이름 해석 규칙:
|
|
53
|
+
* - 절대경로 → 그대로 사용
|
|
54
|
+
* - 확장자 포함 (`./xxx.js`) → baseDir 상대경로
|
|
55
|
+
* - 단축 이름 (`csv-parser`, `excel/csv-parser`) → workers/ 폴더에서 .js/.mjs 자동 탐색
|
|
56
|
+
*
|
|
57
|
+
* @param {string} name - 워커 이름 또는 경로
|
|
58
|
+
* @param {*} [data] - workerData로 전달
|
|
59
|
+
* @param {object} [opts]
|
|
60
|
+
* @param {number} [opts.timeout] - 타임아웃 (ms)
|
|
61
|
+
* @param {Array} [opts.transferList] - ArrayBuffer 등 zero-copy 전송
|
|
62
|
+
* @returns {Promise<*>} 워커 결과
|
|
63
|
+
*/
|
|
64
|
+
run(name, data, opts = {}) {
|
|
65
|
+
const timeout = opts.timeout ?? this._defaultTimeout;
|
|
66
|
+
const resolved = this._resolve(name);
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const worker = new Worker(resolved, {
|
|
70
|
+
workerData: data,
|
|
71
|
+
transferList: opts.transferList,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this._activeWorkers.add(worker);
|
|
75
|
+
|
|
76
|
+
let timer;
|
|
77
|
+
let settled = false;
|
|
78
|
+
|
|
79
|
+
const settle = (fn, value) => {
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
this._activeWorkers.delete(worker);
|
|
84
|
+
fn(value);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
worker.on('message', (result) => settle(resolve, result));
|
|
88
|
+
worker.on('error', (err) => settle(reject, err));
|
|
89
|
+
worker.on('exit', (code) => {
|
|
90
|
+
if (!settled) {
|
|
91
|
+
settle(reject, new Error(`Worker exited with code ${code}`));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (timeout > 0) {
|
|
96
|
+
timer = setTimeout(() => {
|
|
97
|
+
worker.terminate();
|
|
98
|
+
settle(reject, new Error(`Worker timeout (${timeout}ms)`));
|
|
99
|
+
}, timeout);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 인라인 순수 함수를 워커에서 실행
|
|
106
|
+
*
|
|
107
|
+
* 함수는 직렬화 가능해야 함 (클로저/외부 참조 불가).
|
|
108
|
+
* 워커 내에서 require() 사용 가능.
|
|
109
|
+
*
|
|
110
|
+
* @param {Function} fn - (data) => result 형태의 순수 함수
|
|
111
|
+
* @param {*} [data] - 함수에 전달할 데이터
|
|
112
|
+
* @param {object} [opts]
|
|
113
|
+
* @param {number} [opts.timeout] - 타임아웃 (ms)
|
|
114
|
+
* @returns {Promise<*>} 함수 실행 결과
|
|
115
|
+
*/
|
|
116
|
+
exec(fn, data, opts = {}) {
|
|
117
|
+
const timeout = opts.timeout ?? this._defaultTimeout;
|
|
118
|
+
const fnStr = fn.toString();
|
|
119
|
+
|
|
120
|
+
// 인라인 워커 코드
|
|
121
|
+
const workerCode = `
|
|
122
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
123
|
+
const fn = ${fnStr};
|
|
124
|
+
(async () => {
|
|
125
|
+
try {
|
|
126
|
+
const result = await fn(workerData);
|
|
127
|
+
parentPort.postMessage(result);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const worker = new Worker(workerCode, {
|
|
136
|
+
eval: true,
|
|
137
|
+
workerData: data,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this._activeWorkers.add(worker);
|
|
141
|
+
|
|
142
|
+
let timer;
|
|
143
|
+
let settled = false;
|
|
144
|
+
|
|
145
|
+
const settle = (cb, value) => {
|
|
146
|
+
if (settled) return;
|
|
147
|
+
settled = true;
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
this._activeWorkers.delete(worker);
|
|
150
|
+
cb(value);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
worker.on('message', (result) => settle(resolve, result));
|
|
154
|
+
worker.on('error', (err) => settle(reject, err));
|
|
155
|
+
worker.on('exit', (code) => {
|
|
156
|
+
if (!settled) {
|
|
157
|
+
settle(reject, new Error(`Worker exited with code ${code}`));
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (timeout > 0) {
|
|
162
|
+
timer = setTimeout(() => {
|
|
163
|
+
worker.terminate();
|
|
164
|
+
settle(reject, new Error(`Worker timeout (${timeout}ms)`));
|
|
165
|
+
}, timeout);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 활성 워커 수
|
|
172
|
+
* @returns {number}
|
|
173
|
+
*/
|
|
174
|
+
get active() {
|
|
175
|
+
return this._activeWorkers.size;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 모든 활성 워커 종료 (graceful shutdown)
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
*/
|
|
182
|
+
async terminate() {
|
|
183
|
+
const terminations = [];
|
|
184
|
+
for (const worker of this._activeWorkers) {
|
|
185
|
+
terminations.push(worker.terminate());
|
|
186
|
+
}
|
|
187
|
+
await Promise.allSettled(terminations);
|
|
188
|
+
this._activeWorkers.clear();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 워커 이름 → 절대경로 해석
|
|
193
|
+
*
|
|
194
|
+
* 규칙:
|
|
195
|
+
* 1) 절대경로 → 그대로
|
|
196
|
+
* 2) 확장자 포함 (./foo.js, ../foo.mjs) → baseDir 기준 상대경로
|
|
197
|
+
* 3) 단축 이름 (csv-parser, excel/csv-parser) → workers/ 폴더에서 탐색
|
|
198
|
+
* 탐색 순서: .js → .mjs
|
|
199
|
+
*
|
|
200
|
+
* @param {string} name
|
|
201
|
+
* @returns {string} 절대경로
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
_resolve(name) {
|
|
205
|
+
// 1) 절대경로
|
|
206
|
+
if (path.isAbsolute(name)) return name;
|
|
207
|
+
|
|
208
|
+
const baseDir = this.app?.baseDir || '.';
|
|
209
|
+
|
|
210
|
+
// 2) 확장자 포함 → baseDir 상대경로
|
|
211
|
+
if (/\.\w+$/.test(name)) {
|
|
212
|
+
return path.resolve(baseDir, name);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 3) 단축 이름 → shared/workers/ 폴더 탐색
|
|
216
|
+
const workersDir = path.resolve(baseDir, 'shared/workers');
|
|
217
|
+
for (const ext of ['.js', '.mjs']) {
|
|
218
|
+
const candidate = path.join(workersDir, name + ext);
|
|
219
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 기본값: shared/workers/{name}.js (존재하지 않아도 Worker 생성 시 에러)
|
|
223
|
+
return path.join(workersDir, name + '.js');
|
|
224
|
+
}
|
|
225
|
+
}
|
package/lib/services/EventBus.js
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EventBus — 앱 내 + Hub 경유 이벤트 시스템
|
|
3
|
-
*
|
|
4
|
-
* @see docs/framework/05-eventbus.md
|
|
5
|
-
*/
|
|
6
|
-
export default class EventBus {
|
|
7
|
-
/**
|
|
8
|
-
* @param {import('./Application.js').default} app
|
|
9
|
-
*/
|
|
10
|
-
constructor(app) {
|
|
11
|
-
this.app = app;
|
|
12
|
-
this._handlers = new Map();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 이벤트 구독
|
|
17
|
-
* @param {string} event
|
|
18
|
-
* @param {Function} handler - (data, meta) => void
|
|
19
|
-
*/
|
|
20
|
-
on(event, handler) {
|
|
21
|
-
if (!this._handlers.has(event)) {
|
|
22
|
-
this._handlers.set(event, []);
|
|
23
|
-
}
|
|
24
|
-
this._handlers.get(event).push(handler);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 이벤트 발행 (로컬)
|
|
29
|
-
* @param {string} event
|
|
30
|
-
* @param {*} data
|
|
31
|
-
* @param {object} [opts] - { hub: true } → Hub 전파
|
|
32
|
-
*/
|
|
33
|
-
async emit(event, data, opts = {}) {
|
|
34
|
-
const meta = {
|
|
35
|
-
remote: false,
|
|
36
|
-
server: this.app?.config?.get('app.name', 'fuzionx') || 'fuzionx',
|
|
37
|
-
timestamp: Date.now(),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const handlers = this._handlers.get(event);
|
|
41
|
-
if (handlers) {
|
|
42
|
-
for (const handler of handlers) {
|
|
43
|
-
try { await handler(data, meta); } catch (err) {
|
|
44
|
-
this.app?.logger?.error?.(`Event '${event}' handler error:`, err);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Hub 전파 (Hub 연동 시 구현)
|
|
50
|
-
if (opts.hub && this.app?.ws?.hub) {
|
|
51
|
-
// this.app.ws.hub.publish(event, data);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 원격 이벤트 수신 (Hub에서 호출)
|
|
57
|
-
* @param {string} event
|
|
58
|
-
* @param {*} data
|
|
59
|
-
* @param {object} remoteMeta
|
|
60
|
-
*/
|
|
61
|
-
async onRemoteEvent(event, data, remoteMeta) {
|
|
62
|
-
const handlers = this._handlers.get(event);
|
|
63
|
-
if (!handlers) return;
|
|
64
|
-
|
|
65
|
-
const meta = { ...remoteMeta, remote: true };
|
|
66
|
-
for (const handler of handlers) {
|
|
67
|
-
await handler(data, meta);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 구독 해제
|
|
73
|
-
* @param {string} event
|
|
74
|
-
* @param {Function} [handler] - 없으면 이벤트의 모든 핸들러 해제
|
|
75
|
-
*/
|
|
76
|
-
off(event, handler) {
|
|
77
|
-
if (!handler) {
|
|
78
|
-
this._handlers.delete(event);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const handlers = this._handlers.get(event);
|
|
82
|
-
if (handlers) {
|
|
83
|
-
const idx = handlers.indexOf(handler);
|
|
84
|
-
if (idx >= 0) handlers.splice(idx, 1);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 모든 구독 해제
|
|
90
|
-
*/
|
|
91
|
-
clear() {
|
|
92
|
-
this._handlers.clear();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* EventBus — 앱 내 + Hub 경유 이벤트 시스템
|
|
3
|
+
*
|
|
4
|
+
* @see docs/framework/05-eventbus.md
|
|
5
|
+
*/
|
|
6
|
+
export default class EventBus {
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('./Application.js').default} app
|
|
9
|
+
*/
|
|
10
|
+
constructor(app) {
|
|
11
|
+
this.app = app;
|
|
12
|
+
this._handlers = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 이벤트 구독
|
|
17
|
+
* @param {string} event
|
|
18
|
+
* @param {Function} handler - (data, meta) => void
|
|
19
|
+
*/
|
|
20
|
+
on(event, handler) {
|
|
21
|
+
if (!this._handlers.has(event)) {
|
|
22
|
+
this._handlers.set(event, []);
|
|
23
|
+
}
|
|
24
|
+
this._handlers.get(event).push(handler);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 이벤트 발행 (로컬)
|
|
29
|
+
* @param {string} event
|
|
30
|
+
* @param {*} data
|
|
31
|
+
* @param {object} [opts] - { hub: true } → Hub 전파
|
|
32
|
+
*/
|
|
33
|
+
async emit(event, data, opts = {}) {
|
|
34
|
+
const meta = {
|
|
35
|
+
remote: false,
|
|
36
|
+
server: this.app?.config?.get('app.name', 'fuzionx') || 'fuzionx',
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handlers = this._handlers.get(event);
|
|
41
|
+
if (handlers) {
|
|
42
|
+
for (const handler of handlers) {
|
|
43
|
+
try { await handler(data, meta); } catch (err) {
|
|
44
|
+
this.app?.logger?.error?.(`Event '${event}' handler error:`, err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Hub 전파 (Hub 연동 시 구현)
|
|
50
|
+
if (opts.hub && this.app?.ws?.hub) {
|
|
51
|
+
// this.app.ws.hub.publish(event, data);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 원격 이벤트 수신 (Hub에서 호출)
|
|
57
|
+
* @param {string} event
|
|
58
|
+
* @param {*} data
|
|
59
|
+
* @param {object} remoteMeta
|
|
60
|
+
*/
|
|
61
|
+
async onRemoteEvent(event, data, remoteMeta) {
|
|
62
|
+
const handlers = this._handlers.get(event);
|
|
63
|
+
if (!handlers) return;
|
|
64
|
+
|
|
65
|
+
const meta = { ...remoteMeta, remote: true };
|
|
66
|
+
for (const handler of handlers) {
|
|
67
|
+
await handler(data, meta);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 구독 해제
|
|
73
|
+
* @param {string} event
|
|
74
|
+
* @param {Function} [handler] - 없으면 이벤트의 모든 핸들러 해제
|
|
75
|
+
*/
|
|
76
|
+
off(event, handler) {
|
|
77
|
+
if (!handler) {
|
|
78
|
+
this._handlers.delete(event);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const handlers = this._handlers.get(event);
|
|
82
|
+
if (handlers) {
|
|
83
|
+
const idx = handlers.indexOf(handler);
|
|
84
|
+
if (idx >= 0) handlers.splice(idx, 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 모든 구독 해제
|
|
90
|
+
*/
|
|
91
|
+
clear() {
|
|
92
|
+
this._handlers.clear();
|
|
93
|
+
}
|
|
94
|
+
}
|