@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
package/lib/schedule/Queue.js
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Queue — Task 큐 관리 (Memory/Redis)
|
|
3
|
-
*
|
|
4
|
-
* AutoLoader가 shared/jobs/ 에서 Task 클래스를 자동 스캔·등록.
|
|
5
|
-
* handle(data)은 메인 스레드에서 실행되므로 this.db, this.service() 등
|
|
6
|
-
* 앱 컨텍스트에 접근 가능합니다.
|
|
7
|
-
*
|
|
8
|
-
* CPU-intensive 작업은 handle() 내에서 this.worker.run()으로 위임.
|
|
9
|
-
*
|
|
10
|
-
* @see docs/framework/10-scheduler-queue.md
|
|
11
|
-
* @see lib/schedule/WorkerPool.js
|
|
12
|
-
*/
|
|
13
|
-
export default class Queue {
|
|
14
|
-
/**
|
|
15
|
-
* @param {import('../core/Application.js').default} app
|
|
16
|
-
* @param {object} [opts]
|
|
17
|
-
* @param {string} [opts.driver='memory'] - 'memory' | 'redis'
|
|
18
|
-
*/
|
|
19
|
-
constructor(app, opts = {}) {
|
|
20
|
-
this.app = app;
|
|
21
|
-
this.driver = opts.driver || 'memory';
|
|
22
|
-
this._tasks = new Map(); // name → TaskClass
|
|
23
|
-
this._queue = [];
|
|
24
|
-
this._processing = false;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Task 클래스 등록
|
|
29
|
-
* @param {string} name
|
|
30
|
-
* @param {typeof import('./Task.js').default} TaskClass
|
|
31
|
-
*/
|
|
32
|
-
register(name, TaskClass) {
|
|
33
|
-
this._tasks.set(name, TaskClass);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Task 디스패치 (큐에 추가)
|
|
38
|
-
* @param {string|Function} taskOrName
|
|
39
|
-
* @param {object} data
|
|
40
|
-
* @param {object} [opts]
|
|
41
|
-
*/
|
|
42
|
-
dispatch(taskOrName, data, opts = {}) {
|
|
43
|
-
const name = typeof taskOrName === 'string' ? taskOrName : taskOrName.name;
|
|
44
|
-
this._queue.push({ name, data, opts, retries: 0 });
|
|
45
|
-
|
|
46
|
-
// auto-process (비동기)
|
|
47
|
-
if (!this._processing) {
|
|
48
|
-
this._process();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 큐 처리 루프
|
|
54
|
-
* @private
|
|
55
|
-
*/
|
|
56
|
-
async _process() {
|
|
57
|
-
this._processing = true;
|
|
58
|
-
|
|
59
|
-
while (this._queue.length > 0) {
|
|
60
|
-
const job = this._queue.shift();
|
|
61
|
-
const TaskClass = this._tasks.get(job.name);
|
|
62
|
-
|
|
63
|
-
if (!TaskClass) {
|
|
64
|
-
this.app?.logger?.error?.(`[Queue] Task '${job.name}' not registered`);
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const timeout = TaskClass.timeout || 30000;
|
|
69
|
-
const task = new TaskClass(this.app);
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
let timer;
|
|
73
|
-
await Promise.race([
|
|
74
|
-
task.handle(job.data).finally(() => clearTimeout(timer)),
|
|
75
|
-
new Promise((_, reject) => {
|
|
76
|
-
timer = setTimeout(() => reject(new Error(`Task timeout (${timeout}ms)`)), timeout);
|
|
77
|
-
}),
|
|
78
|
-
]);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
job.retries++;
|
|
81
|
-
if (job.retries < (TaskClass.retries || 3)) {
|
|
82
|
-
const delay = TaskClass.retryDelay || 1000;
|
|
83
|
-
setTimeout(() => {
|
|
84
|
-
this._queue.push(job);
|
|
85
|
-
if (!this._processing) this._process();
|
|
86
|
-
}, delay);
|
|
87
|
-
} else {
|
|
88
|
-
try { await task.failed(job.data, err); } catch {}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
this._processing = false;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 남은 큐 수
|
|
98
|
-
* @returns {number}
|
|
99
|
-
*/
|
|
100
|
-
get pending() {
|
|
101
|
-
return this._queue.length;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Queue — Task 큐 관리 (Memory/Redis)
|
|
3
|
+
*
|
|
4
|
+
* AutoLoader가 shared/jobs/ 에서 Task 클래스를 자동 스캔·등록.
|
|
5
|
+
* handle(data)은 메인 스레드에서 실행되므로 this.db, this.service() 등
|
|
6
|
+
* 앱 컨텍스트에 접근 가능합니다.
|
|
7
|
+
*
|
|
8
|
+
* CPU-intensive 작업은 handle() 내에서 this.worker.run()으로 위임.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/framework/10-scheduler-queue.md
|
|
11
|
+
* @see lib/schedule/WorkerPool.js
|
|
12
|
+
*/
|
|
13
|
+
export default class Queue {
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('../core/Application.js').default} app
|
|
16
|
+
* @param {object} [opts]
|
|
17
|
+
* @param {string} [opts.driver='memory'] - 'memory' | 'redis'
|
|
18
|
+
*/
|
|
19
|
+
constructor(app, opts = {}) {
|
|
20
|
+
this.app = app;
|
|
21
|
+
this.driver = opts.driver || 'memory';
|
|
22
|
+
this._tasks = new Map(); // name → TaskClass
|
|
23
|
+
this._queue = [];
|
|
24
|
+
this._processing = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Task 클래스 등록
|
|
29
|
+
* @param {string} name
|
|
30
|
+
* @param {typeof import('./Task.js').default} TaskClass
|
|
31
|
+
*/
|
|
32
|
+
register(name, TaskClass) {
|
|
33
|
+
this._tasks.set(name, TaskClass);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Task 디스패치 (큐에 추가)
|
|
38
|
+
* @param {string|Function} taskOrName
|
|
39
|
+
* @param {object} data
|
|
40
|
+
* @param {object} [opts]
|
|
41
|
+
*/
|
|
42
|
+
dispatch(taskOrName, data, opts = {}) {
|
|
43
|
+
const name = typeof taskOrName === 'string' ? taskOrName : taskOrName.name;
|
|
44
|
+
this._queue.push({ name, data, opts, retries: 0 });
|
|
45
|
+
|
|
46
|
+
// auto-process (비동기)
|
|
47
|
+
if (!this._processing) {
|
|
48
|
+
this._process();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 큐 처리 루프
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
async _process() {
|
|
57
|
+
this._processing = true;
|
|
58
|
+
|
|
59
|
+
while (this._queue.length > 0) {
|
|
60
|
+
const job = this._queue.shift();
|
|
61
|
+
const TaskClass = this._tasks.get(job.name);
|
|
62
|
+
|
|
63
|
+
if (!TaskClass) {
|
|
64
|
+
this.app?.logger?.error?.(`[Queue] Task '${job.name}' not registered`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const timeout = TaskClass.timeout || 30000;
|
|
69
|
+
const task = new TaskClass(this.app);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
let timer;
|
|
73
|
+
await Promise.race([
|
|
74
|
+
task.handle(job.data).finally(() => clearTimeout(timer)),
|
|
75
|
+
new Promise((_, reject) => {
|
|
76
|
+
timer = setTimeout(() => reject(new Error(`Task timeout (${timeout}ms)`)), timeout);
|
|
77
|
+
}),
|
|
78
|
+
]);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
job.retries++;
|
|
81
|
+
if (job.retries < (TaskClass.retries || 3)) {
|
|
82
|
+
const delay = TaskClass.retryDelay || 1000;
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
this._queue.push(job);
|
|
85
|
+
if (!this._processing) this._process();
|
|
86
|
+
}, delay);
|
|
87
|
+
} else {
|
|
88
|
+
try { await task.failed(job.data, err); } catch {}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this._processing = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 남은 큐 수
|
|
98
|
+
* @returns {number}
|
|
99
|
+
*/
|
|
100
|
+
get pending() {
|
|
101
|
+
return this._queue.length;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scheduler — 정기 실행 작업 관리 (cron 스케줄)
|
|
3
|
-
*
|
|
4
|
-
* AutoLoader가 shared/jobs/ 에서 Job 클래스를 자동 스캔·등록.
|
|
5
|
-
* handle()은 메인 스레드에서 실행되므로 this.db, this.service() 등
|
|
6
|
-
* 앱 컨텍스트에 접근 가능합니다.
|
|
7
|
-
*
|
|
8
|
-
* CPU-intensive 작업은 handle() 내에서 this.worker.run()으로 위임.
|
|
9
|
-
*
|
|
10
|
-
* @see docs/framework/10-scheduler-queue.md
|
|
11
|
-
* @see lib/schedule/WorkerPool.js
|
|
12
|
-
*/
|
|
13
|
-
export default class Scheduler {
|
|
14
|
-
/**
|
|
15
|
-
* @param {import('../core/Application.js').default} app
|
|
16
|
-
*/
|
|
17
|
-
constructor(app) {
|
|
18
|
-
this.app = app;
|
|
19
|
-
/** @type {Array<{JobClass: typeof import('./Job.js').default}>} */
|
|
20
|
-
this._jobs = [];
|
|
21
|
-
/** @type {Array<NodeJS.Timeout>} */
|
|
22
|
-
this._timers = [];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Job 클래스 등록 (AutoLoader에서 호출)
|
|
27
|
-
* @param {typeof import('./Job.js').default} JobClass
|
|
28
|
-
*/
|
|
29
|
-
register(JobClass) {
|
|
30
|
-
if (!JobClass.schedule) return;
|
|
31
|
-
if (JobClass.enabled === false) return;
|
|
32
|
-
this._jobs.push({ JobClass });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 등록된 모든 Job 시작
|
|
37
|
-
*/
|
|
38
|
-
start() {
|
|
39
|
-
for (const { JobClass } of this._jobs) {
|
|
40
|
-
const interval = this._parseSchedule(JobClass.schedule);
|
|
41
|
-
if (!interval) {
|
|
42
|
-
this.app?.logger?.warn?.(`[Scheduler] Invalid schedule: ${JobClass.name} → '${JobClass.schedule}'`);
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const timeout = JobClass.timeout || 30000;
|
|
47
|
-
|
|
48
|
-
const runJob = async () => {
|
|
49
|
-
const job = new JobClass(this.app);
|
|
50
|
-
try {
|
|
51
|
-
let timer;
|
|
52
|
-
await Promise.race([
|
|
53
|
-
job.handle().finally(() => clearTimeout(timer)),
|
|
54
|
-
new Promise((_, reject) => {
|
|
55
|
-
timer = setTimeout(() => reject(new Error(`Job timeout (${timeout}ms)`)), timeout);
|
|
56
|
-
}),
|
|
57
|
-
]);
|
|
58
|
-
} catch (err) {
|
|
59
|
-
if (typeof job.onError === 'function') {
|
|
60
|
-
try { await job.onError(err); } catch {}
|
|
61
|
-
} else {
|
|
62
|
-
this.app?.logger?.error?.(`[Scheduler] ${JobClass.name} failed: ${err.message}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// daily/weekly → 첫 실행까지 지연 계산
|
|
68
|
-
const initialDelay = this._calcInitialDelay(JobClass.schedule);
|
|
69
|
-
if (initialDelay > 0) {
|
|
70
|
-
const delayed = setTimeout(() => {
|
|
71
|
-
runJob();
|
|
72
|
-
const timer = setInterval(runJob, interval);
|
|
73
|
-
this._timers.push(timer);
|
|
74
|
-
}, initialDelay);
|
|
75
|
-
this._timers.push(delayed);
|
|
76
|
-
} else {
|
|
77
|
-
const timer = setInterval(runJob, interval);
|
|
78
|
-
this._timers.push(timer);
|
|
79
|
-
// 즉시 첫 실행
|
|
80
|
-
runJob();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.app?.logger?.info?.(
|
|
84
|
-
`[Scheduler] ${JobClass.name} registered (${JobClass.schedule}, interval=${interval}ms)`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 모든 타이머 정지
|
|
91
|
-
*/
|
|
92
|
-
stop() {
|
|
93
|
-
for (const timer of this._timers) {
|
|
94
|
-
clearInterval(timer);
|
|
95
|
-
clearTimeout(timer);
|
|
96
|
-
}
|
|
97
|
-
this._timers = [];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ── Schedule 파싱 ──
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 간편 스케줄 → ms 변환
|
|
104
|
-
*
|
|
105
|
-
* 지원 포맷:
|
|
106
|
-
* 'every:5s' → 5000ms
|
|
107
|
-
* 'every:5m' → 300000ms
|
|
108
|
-
* 'every:1h' → 3600000ms
|
|
109
|
-
* 'daily:HH:MM' → 24h (첫 실행은 _calcInitialDelay)
|
|
110
|
-
* 'weekly:DAY:HH:MM' → 7d (첫 실행은 _calcInitialDelay)
|
|
111
|
-
*/
|
|
112
|
-
_parseSchedule(schedule) {
|
|
113
|
-
if (!schedule) return null;
|
|
114
|
-
|
|
115
|
-
// every:Ns/Nm/Nh
|
|
116
|
-
const everyMatch = schedule.match(/^every:(\d+)(s|m|h)$/i);
|
|
117
|
-
if (everyMatch) {
|
|
118
|
-
const val = parseInt(everyMatch[1], 10);
|
|
119
|
-
const unit = everyMatch[2].toLowerCase();
|
|
120
|
-
if (unit === 's') return val * 1000;
|
|
121
|
-
if (unit === 'm') return val * 60000;
|
|
122
|
-
if (unit === 'h') return val * 3600000;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// daily:HH:MM → 24시간 간격
|
|
126
|
-
if (/^daily:\d{2}:\d{2}$/.test(schedule)) {
|
|
127
|
-
return 24 * 60 * 60 * 1000;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// weekly:DAY:HH:MM → 7일 간격
|
|
131
|
-
if (/^weekly:\w+:\d{2}:\d{2}$/.test(schedule)) {
|
|
132
|
-
return 7 * 24 * 60 * 60 * 1000;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* daily/weekly 스케줄의 첫 실행 지연시간 계산
|
|
140
|
-
*/
|
|
141
|
-
_calcInitialDelay(schedule) {
|
|
142
|
-
const now = new Date();
|
|
143
|
-
|
|
144
|
-
// daily:HH:MM
|
|
145
|
-
const dailyMatch = schedule.match(/^daily:(\d{2}):(\d{2})$/);
|
|
146
|
-
if (dailyMatch) {
|
|
147
|
-
const target = new Date(now);
|
|
148
|
-
target.setHours(parseInt(dailyMatch[1], 10), parseInt(dailyMatch[2], 10), 0, 0);
|
|
149
|
-
if (target <= now) target.setDate(target.getDate() + 1);
|
|
150
|
-
return target - now;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// weekly:DAY:HH:MM
|
|
154
|
-
const weeklyMatch = schedule.match(/^weekly:(\w+):(\d{2}):(\d{2})$/);
|
|
155
|
-
if (weeklyMatch) {
|
|
156
|
-
const days = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
|
|
157
|
-
const targetDay = days[weeklyMatch[1].toLowerCase()];
|
|
158
|
-
if (targetDay === undefined) return 0;
|
|
159
|
-
|
|
160
|
-
const target = new Date(now);
|
|
161
|
-
target.setHours(parseInt(weeklyMatch[2], 10), parseInt(weeklyMatch[3], 10), 0, 0);
|
|
162
|
-
|
|
163
|
-
let diff = targetDay - now.getDay();
|
|
164
|
-
if (diff < 0 || (diff === 0 && target <= now)) diff += 7;
|
|
165
|
-
target.setDate(target.getDate() + diff);
|
|
166
|
-
return target - now;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return 0; // every:* → 즉시 시작
|
|
170
|
-
}
|
|
171
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler — 정기 실행 작업 관리 (cron 스케줄)
|
|
3
|
+
*
|
|
4
|
+
* AutoLoader가 shared/jobs/ 에서 Job 클래스를 자동 스캔·등록.
|
|
5
|
+
* handle()은 메인 스레드에서 실행되므로 this.db, this.service() 등
|
|
6
|
+
* 앱 컨텍스트에 접근 가능합니다.
|
|
7
|
+
*
|
|
8
|
+
* CPU-intensive 작업은 handle() 내에서 this.worker.run()으로 위임.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/framework/10-scheduler-queue.md
|
|
11
|
+
* @see lib/schedule/WorkerPool.js
|
|
12
|
+
*/
|
|
13
|
+
export default class Scheduler {
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('../core/Application.js').default} app
|
|
16
|
+
*/
|
|
17
|
+
constructor(app) {
|
|
18
|
+
this.app = app;
|
|
19
|
+
/** @type {Array<{JobClass: typeof import('./Job.js').default}>} */
|
|
20
|
+
this._jobs = [];
|
|
21
|
+
/** @type {Array<NodeJS.Timeout>} */
|
|
22
|
+
this._timers = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Job 클래스 등록 (AutoLoader에서 호출)
|
|
27
|
+
* @param {typeof import('./Job.js').default} JobClass
|
|
28
|
+
*/
|
|
29
|
+
register(JobClass) {
|
|
30
|
+
if (!JobClass.schedule) return;
|
|
31
|
+
if (JobClass.enabled === false) return;
|
|
32
|
+
this._jobs.push({ JobClass });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 등록된 모든 Job 시작
|
|
37
|
+
*/
|
|
38
|
+
start() {
|
|
39
|
+
for (const { JobClass } of this._jobs) {
|
|
40
|
+
const interval = this._parseSchedule(JobClass.schedule);
|
|
41
|
+
if (!interval) {
|
|
42
|
+
this.app?.logger?.warn?.(`[Scheduler] Invalid schedule: ${JobClass.name} → '${JobClass.schedule}'`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const timeout = JobClass.timeout || 30000;
|
|
47
|
+
|
|
48
|
+
const runJob = async () => {
|
|
49
|
+
const job = new JobClass(this.app);
|
|
50
|
+
try {
|
|
51
|
+
let timer;
|
|
52
|
+
await Promise.race([
|
|
53
|
+
job.handle().finally(() => clearTimeout(timer)),
|
|
54
|
+
new Promise((_, reject) => {
|
|
55
|
+
timer = setTimeout(() => reject(new Error(`Job timeout (${timeout}ms)`)), timeout);
|
|
56
|
+
}),
|
|
57
|
+
]);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (typeof job.onError === 'function') {
|
|
60
|
+
try { await job.onError(err); } catch {}
|
|
61
|
+
} else {
|
|
62
|
+
this.app?.logger?.error?.(`[Scheduler] ${JobClass.name} failed: ${err.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// daily/weekly → 첫 실행까지 지연 계산
|
|
68
|
+
const initialDelay = this._calcInitialDelay(JobClass.schedule);
|
|
69
|
+
if (initialDelay > 0) {
|
|
70
|
+
const delayed = setTimeout(() => {
|
|
71
|
+
runJob();
|
|
72
|
+
const timer = setInterval(runJob, interval);
|
|
73
|
+
this._timers.push(timer);
|
|
74
|
+
}, initialDelay);
|
|
75
|
+
this._timers.push(delayed);
|
|
76
|
+
} else {
|
|
77
|
+
const timer = setInterval(runJob, interval);
|
|
78
|
+
this._timers.push(timer);
|
|
79
|
+
// 즉시 첫 실행
|
|
80
|
+
runJob();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.app?.logger?.info?.(
|
|
84
|
+
`[Scheduler] ${JobClass.name} registered (${JobClass.schedule}, interval=${interval}ms)`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 모든 타이머 정지
|
|
91
|
+
*/
|
|
92
|
+
stop() {
|
|
93
|
+
for (const timer of this._timers) {
|
|
94
|
+
clearInterval(timer);
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
this._timers = [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Schedule 파싱 ──
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 간편 스케줄 → ms 변환
|
|
104
|
+
*
|
|
105
|
+
* 지원 포맷:
|
|
106
|
+
* 'every:5s' → 5000ms
|
|
107
|
+
* 'every:5m' → 300000ms
|
|
108
|
+
* 'every:1h' → 3600000ms
|
|
109
|
+
* 'daily:HH:MM' → 24h (첫 실행은 _calcInitialDelay)
|
|
110
|
+
* 'weekly:DAY:HH:MM' → 7d (첫 실행은 _calcInitialDelay)
|
|
111
|
+
*/
|
|
112
|
+
_parseSchedule(schedule) {
|
|
113
|
+
if (!schedule) return null;
|
|
114
|
+
|
|
115
|
+
// every:Ns/Nm/Nh
|
|
116
|
+
const everyMatch = schedule.match(/^every:(\d+)(s|m|h)$/i);
|
|
117
|
+
if (everyMatch) {
|
|
118
|
+
const val = parseInt(everyMatch[1], 10);
|
|
119
|
+
const unit = everyMatch[2].toLowerCase();
|
|
120
|
+
if (unit === 's') return val * 1000;
|
|
121
|
+
if (unit === 'm') return val * 60000;
|
|
122
|
+
if (unit === 'h') return val * 3600000;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// daily:HH:MM → 24시간 간격
|
|
126
|
+
if (/^daily:\d{2}:\d{2}$/.test(schedule)) {
|
|
127
|
+
return 24 * 60 * 60 * 1000;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// weekly:DAY:HH:MM → 7일 간격
|
|
131
|
+
if (/^weekly:\w+:\d{2}:\d{2}$/.test(schedule)) {
|
|
132
|
+
return 7 * 24 * 60 * 60 * 1000;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* daily/weekly 스케줄의 첫 실행 지연시간 계산
|
|
140
|
+
*/
|
|
141
|
+
_calcInitialDelay(schedule) {
|
|
142
|
+
const now = new Date();
|
|
143
|
+
|
|
144
|
+
// daily:HH:MM
|
|
145
|
+
const dailyMatch = schedule.match(/^daily:(\d{2}):(\d{2})$/);
|
|
146
|
+
if (dailyMatch) {
|
|
147
|
+
const target = new Date(now);
|
|
148
|
+
target.setHours(parseInt(dailyMatch[1], 10), parseInt(dailyMatch[2], 10), 0, 0);
|
|
149
|
+
if (target <= now) target.setDate(target.getDate() + 1);
|
|
150
|
+
return target - now;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// weekly:DAY:HH:MM
|
|
154
|
+
const weeklyMatch = schedule.match(/^weekly:(\w+):(\d{2}):(\d{2})$/);
|
|
155
|
+
if (weeklyMatch) {
|
|
156
|
+
const days = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
|
|
157
|
+
const targetDay = days[weeklyMatch[1].toLowerCase()];
|
|
158
|
+
if (targetDay === undefined) return 0;
|
|
159
|
+
|
|
160
|
+
const target = new Date(now);
|
|
161
|
+
target.setHours(parseInt(weeklyMatch[2], 10), parseInt(weeklyMatch[3], 10), 0, 0);
|
|
162
|
+
|
|
163
|
+
let diff = targetDay - now.getDay();
|
|
164
|
+
if (diff < 0 || (diff === 0 && target <= now)) diff += 7;
|
|
165
|
+
target.setDate(target.getDate() + diff);
|
|
166
|
+
return target - now;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return 0; // every:* → 즉시 시작
|
|
170
|
+
}
|
|
171
|
+
}
|
package/lib/schedule/Task.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task — 비동기 위임 작업 (큐)
|
|
3
|
-
*
|
|
4
|
-
* @see docs/framework/10-scheduler-queue.md
|
|
5
|
-
* @see docs/framework/class-design.mm.md (Task)
|
|
6
|
-
*/
|
|
7
|
-
import Job from './Job.js';
|
|
8
|
-
|
|
9
|
-
export default class Task extends Job {
|
|
10
|
-
/** @type {string} 큐 이름 */
|
|
11
|
-
static queue = 'default';
|
|
12
|
-
|
|
13
|
-
/** @type {number} 최대 재시도 횟수 */
|
|
14
|
-
static retries = 3;
|
|
15
|
-
|
|
16
|
-
/** @type {number} 재시도 딜레이 (ms) */
|
|
17
|
-
static retryDelay = 5000;
|
|
18
|
-
|
|
19
|
-
/** @type {number} 타임아웃 (ms) */
|
|
20
|
-
static timeout = 30000;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Task 실행 (서브클래스에서 오버라이드)
|
|
24
|
-
* @param {object} data - dispatch() 시 전달된 데이터
|
|
25
|
-
* @returns {Promise<void>}
|
|
26
|
-
*/
|
|
27
|
-
async handle(data) {
|
|
28
|
-
throw new Error(`${this.constructor.name}.handle() must be implemented`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 최종 실패 시 호출 (모든 재시도 소진)
|
|
33
|
-
* @param {object} data
|
|
34
|
-
* @param {Error} error
|
|
35
|
-
*/
|
|
36
|
-
async failed(data, error) {
|
|
37
|
-
this.logger.error(`Task ${this.constructor.name} failed permanently:`, error);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Task — 비동기 위임 작업 (큐)
|
|
3
|
+
*
|
|
4
|
+
* @see docs/framework/10-scheduler-queue.md
|
|
5
|
+
* @see docs/framework/class-design.mm.md (Task)
|
|
6
|
+
*/
|
|
7
|
+
import Job from './Job.js';
|
|
8
|
+
|
|
9
|
+
export default class Task extends Job {
|
|
10
|
+
/** @type {string} 큐 이름 */
|
|
11
|
+
static queue = 'default';
|
|
12
|
+
|
|
13
|
+
/** @type {number} 최대 재시도 횟수 */
|
|
14
|
+
static retries = 3;
|
|
15
|
+
|
|
16
|
+
/** @type {number} 재시도 딜레이 (ms) */
|
|
17
|
+
static retryDelay = 5000;
|
|
18
|
+
|
|
19
|
+
/** @type {number} 타임아웃 (ms) */
|
|
20
|
+
static timeout = 30000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Task 실행 (서브클래스에서 오버라이드)
|
|
24
|
+
* @param {object} data - dispatch() 시 전달된 데이터
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
async handle(data) {
|
|
28
|
+
throw new Error(`${this.constructor.name}.handle() must be implemented`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 최종 실패 시 호출 (모든 재시도 소진)
|
|
33
|
+
* @param {object} data
|
|
34
|
+
* @param {Error} error
|
|
35
|
+
*/
|
|
36
|
+
async failed(data, error) {
|
|
37
|
+
this.logger.error(`Task ${this.constructor.name} failed permanently:`, error);
|
|
38
|
+
}
|
|
39
|
+
}
|