@fuzionx/framework 0.1.42 → 0.1.44

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.
Files changed (80) hide show
  1. package/README.md +501 -501
  2. package/bin/fx.js +12 -12
  3. package/cli/db-sync.js +100 -100
  4. package/cli/index.js +494 -494
  5. package/cli/templates/make/app/controllers/HomeController.js +14 -14
  6. package/cli/templates/make/app/routes/api.js +7 -7
  7. package/cli/templates/make/app/routes/web.js +5 -5
  8. package/cli/templates/make/app/views/default/errors/404.html +11 -11
  9. package/cli/templates/make/app/views/default/errors/500.html +14 -14
  10. package/cli/templates/make/app/views/default/layouts/main.html +22 -22
  11. package/cli/templates/make/app/views/default/pages/home.html +11 -11
  12. package/cli/templates/make/controller.js.tpl +40 -40
  13. package/cli/templates/make/event.js.tpl +8 -8
  14. package/cli/templates/make/job.js.tpl +10 -10
  15. package/cli/templates/make/middleware.js.tpl +10 -10
  16. package/cli/templates/make/model.js.tpl +15 -15
  17. package/cli/templates/make/service.js.tpl +15 -15
  18. package/cli/templates/make/task.js.tpl +15 -15
  19. package/cli/templates/make/test.js.tpl +7 -7
  20. package/cli/templates/make/worker.js.tpl +14 -14
  21. package/cli/templates/make/ws.js.tpl +18 -18
  22. package/index.js +67 -67
  23. package/lib/core/AppError.js +46 -46
  24. package/lib/core/Application.js +1006 -1006
  25. package/lib/core/AutoLoader.js +227 -227
  26. package/lib/core/Base.js +64 -64
  27. package/lib/core/Config.js +331 -228
  28. package/lib/core/Context.js +484 -484
  29. package/lib/database/ConnectionManager.js +208 -208
  30. package/lib/database/MariaModel.js +29 -29
  31. package/lib/database/Model.js +247 -247
  32. package/lib/database/ModelRegistry.js +72 -72
  33. package/lib/database/MongoModel.js +232 -232
  34. package/lib/database/Pagination.js +37 -37
  35. package/lib/database/PostgreModel.js +29 -29
  36. package/lib/database/QueryBuilder.js +172 -172
  37. package/lib/database/SQLiteModel.js +27 -27
  38. package/lib/database/SqlModel.js +257 -257
  39. package/lib/database/SqlQueryBuilder.js +332 -332
  40. package/lib/helpers/CryptoHelper.js +48 -48
  41. package/lib/helpers/FileHelper.js +61 -61
  42. package/lib/helpers/HashHelper.js +39 -39
  43. package/lib/helpers/I18nHelper.js +174 -174
  44. package/lib/helpers/Logger.js +108 -108
  45. package/lib/helpers/MediaHelper.js +84 -84
  46. package/lib/http/Controller.js +34 -34
  47. package/lib/http/ErrorHandler.js +136 -136
  48. package/lib/http/Middleware.js +43 -43
  49. package/lib/http/Router.js +109 -109
  50. package/lib/http/Validation.js +125 -125
  51. package/lib/middleware/apiAuth.js +79 -79
  52. package/lib/middleware/auth.js +42 -42
  53. package/lib/middleware/bodyParser.js +19 -19
  54. package/lib/middleware/cors.js +47 -47
  55. package/lib/middleware/csrf.js +32 -32
  56. package/lib/middleware/index.js +13 -13
  57. package/lib/middleware/session.js +27 -27
  58. package/lib/middleware/theme.js +20 -20
  59. package/lib/realtime/RoomManager.js +85 -85
  60. package/lib/realtime/WsHandler.js +107 -107
  61. package/lib/schedule/Job.js +38 -38
  62. package/lib/schedule/Queue.js +103 -103
  63. package/lib/schedule/Scheduler.js +171 -171
  64. package/lib/schedule/Task.js +39 -39
  65. package/lib/schedule/WorkerPool.js +225 -225
  66. package/lib/services/EventBus.js +94 -94
  67. package/lib/services/Service.js +261 -261
  68. package/lib/services/Storage.js +112 -112
  69. package/lib/utilities/ArrUtil.js +112 -112
  70. package/lib/utilities/DateUtil.js +98 -98
  71. package/lib/utilities/FunctionUtil.js +119 -119
  72. package/lib/utilities/NumUtil.js +75 -75
  73. package/lib/utilities/ObjectUtil.js +170 -170
  74. package/lib/utilities/PaginationUtil.js +81 -81
  75. package/lib/utilities/StrUtil.js +105 -105
  76. package/lib/utilities/index.js +18 -18
  77. package/lib/view/OpenAPI.js +231 -231
  78. package/lib/view/View.js +83 -83
  79. package/package.json +2 -2
  80. 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
+ }
@@ -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
+ }