@fuzionx/framework 0.1.38 → 0.1.41
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 +99 -99
- package/cli/index.js +493 -493
- 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 -998
- package/lib/core/AutoLoader.js +226 -226
- package/lib/core/Base.js +64 -64
- package/lib/core/Config.js +228 -228
- package/lib/core/Context.js +484 -460
- 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 -321
- 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 -105
- 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 -124
- 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 -102
- package/lib/schedule/Scheduler.js +171 -170
- 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/cli/fx.js +0 -3
package/lib/services/Service.js
CHANGED
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Service — 비즈니스 로직 기본 클래스
|
|
3
|
-
*
|
|
4
|
-
* 컨트롤러는 얇게, 비즈니스 로직은 서비스에.
|
|
5
|
-
* 트랜잭션, 캐싱, 병렬 실행, 재시도, 뮤텍스 지원.
|
|
6
|
-
*
|
|
7
|
-
* @see docs/framework/07-service-layer.md
|
|
8
|
-
* @see docs/framework/class-design.mm.md (Service)
|
|
9
|
-
*/
|
|
10
|
-
import Base from '../core/Base.js';
|
|
11
|
-
import { ServiceError } from '../core/AppError.js';
|
|
12
|
-
|
|
13
|
-
/** @type {Map<string, { value: *, expires: number }>} 전역 캐시 스토어 */
|
|
14
|
-
const _cache = new Map();
|
|
15
|
-
|
|
16
|
-
/** @type {Map<string, Promise>} 전역 뮤텍스 락 */
|
|
17
|
-
const _locks = new Map();
|
|
18
|
-
|
|
19
|
-
export default class Service extends Base {
|
|
20
|
-
|
|
21
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
22
|
-
// 트랜잭션
|
|
23
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* SQL 트랜잭션 실행
|
|
27
|
-
* @param {Function} callback - (trx) => Promise<T>
|
|
28
|
-
* @returns {Promise<T>}
|
|
29
|
-
*/
|
|
30
|
-
async transaction(callback) {
|
|
31
|
-
const cm = this.app?._connectionManager;
|
|
32
|
-
if (!cm) throw new Error('Database not configured for transactions');
|
|
33
|
-
|
|
34
|
-
const conn = cm.get();
|
|
35
|
-
if (conn.type === 'knex' && conn.db) {
|
|
36
|
-
return conn.db.transaction(callback);
|
|
37
|
-
}
|
|
38
|
-
throw new Error('SQL database not configured for transactions');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* MongoDB 트랜잭션 실행
|
|
43
|
-
* @param {string} connName - MongoDB 연결명
|
|
44
|
-
* @param {Function} callback - (session) => Promise<T>
|
|
45
|
-
* @returns {Promise<T>}
|
|
46
|
-
*/
|
|
47
|
-
async mongoTransaction(connName, callback) {
|
|
48
|
-
const cm = this.app?._connectionManager;
|
|
49
|
-
if (!cm) throw new Error('Database not configured');
|
|
50
|
-
|
|
51
|
-
const conn = cm.get(connName);
|
|
52
|
-
if (conn.type !== 'mongo' || !conn.mongoose) {
|
|
53
|
-
throw new Error(`MongoDB connection '${connName}' not found`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const session = await conn.mongoose.startSession();
|
|
57
|
-
try {
|
|
58
|
-
session.startTransaction();
|
|
59
|
-
const result = await callback(session);
|
|
60
|
-
await session.commitTransaction();
|
|
61
|
-
return result;
|
|
62
|
-
} catch (err) {
|
|
63
|
-
await session.abortTransaction();
|
|
64
|
-
throw err;
|
|
65
|
-
} finally {
|
|
66
|
-
session.endSession();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
71
|
-
// 캐시
|
|
72
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* TTL 기반 인메모리 캐시로 감싸기
|
|
76
|
-
*
|
|
77
|
-
* @param {string} key - 캐시 키
|
|
78
|
-
* @param {number} ttl - TTL (초)
|
|
79
|
-
* @param {Function} fn - 캐시 미스 시 실행할 함수
|
|
80
|
-
* @returns {Promise<*>}
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* const stats = await this.withCache('dashboard:stats', 300, async () => {
|
|
84
|
-
* return this.db.Order.raw('SELECT SUM(total) ...');
|
|
85
|
-
* });
|
|
86
|
-
*/
|
|
87
|
-
async withCache(key, ttl, fn) {
|
|
88
|
-
const cached = _cache.get(key);
|
|
89
|
-
if (cached && cached.expires > Date.now()) {
|
|
90
|
-
return cached.value;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const value = await fn();
|
|
94
|
-
_cache.set(key, {
|
|
95
|
-
value,
|
|
96
|
-
expires: Date.now() + (ttl * 1000),
|
|
97
|
-
});
|
|
98
|
-
return value;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* 캐시 키 무효화
|
|
103
|
-
* @param {string} key
|
|
104
|
-
*/
|
|
105
|
-
invalidateCache(key) {
|
|
106
|
-
_cache.delete(key);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 전체 캐시 초기화
|
|
111
|
-
*/
|
|
112
|
-
clearCache() {
|
|
113
|
-
_cache.clear();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* 캐시 존재 여부
|
|
118
|
-
* @param {string} key
|
|
119
|
-
* @returns {boolean}
|
|
120
|
-
*/
|
|
121
|
-
hasCache(key) {
|
|
122
|
-
const cached = _cache.get(key);
|
|
123
|
-
return cached ? cached.expires > Date.now() : false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
127
|
-
// 병렬 실행
|
|
128
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* 여러 함수를 병렬 실행
|
|
132
|
-
*
|
|
133
|
-
* @param {Array<Function>} tasks - () => Promise 형태의 함수 배열
|
|
134
|
-
* @param {object} [opts]
|
|
135
|
-
* @param {boolean} [opts.failFast=false] - true면 하나 실패 시 전체 reject
|
|
136
|
-
* @returns {Promise<Array>} 결과 배열 (failFast=false 시 에러도 포함)
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* const [users, orders, stats] = await this.runParallel([
|
|
140
|
-
* () => this.db.User.count(),
|
|
141
|
-
* () => this.db.Order.where('status', 'active').get(),
|
|
142
|
-
* () => this.calculateStats(),
|
|
143
|
-
* ]);
|
|
144
|
-
*/
|
|
145
|
-
async runParallel(tasks, opts = {}) {
|
|
146
|
-
const promises = tasks.map(fn => fn());
|
|
147
|
-
|
|
148
|
-
if (opts.failFast) {
|
|
149
|
-
return Promise.all(promises);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const results = await Promise.allSettled(promises);
|
|
153
|
-
return results.map(r => {
|
|
154
|
-
if (r.status === 'fulfilled') return r.value;
|
|
155
|
-
// 실패 시 에러 객체 반환 (throw하지 않음)
|
|
156
|
-
return r.reason;
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
161
|
-
// 재시도
|
|
162
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 재시도 + 지수 백오프
|
|
166
|
-
*
|
|
167
|
-
* @param {Function} fn - 실행할 함수
|
|
168
|
-
* @param {object} [opts]
|
|
169
|
-
* @param {number} [opts.retries=3] - 재시도 횟수
|
|
170
|
-
* @param {number} [opts.delay=1000] - 초기 지연 (ms)
|
|
171
|
-
* @param {number} [opts.backoff=2] - 백오프 배수
|
|
172
|
-
* @param {Function} [opts.shouldRetry] - (err, attempt) => boolean
|
|
173
|
-
* @returns {Promise<*>}
|
|
174
|
-
*
|
|
175
|
-
* @example
|
|
176
|
-
* const data = await this.retryable(
|
|
177
|
-
* () => fetch('https://api.external.com/data'),
|
|
178
|
-
* { retries: 3, delay: 1000, backoff: 2 }
|
|
179
|
-
* );
|
|
180
|
-
*/
|
|
181
|
-
async retryable(fn, opts = {}) {
|
|
182
|
-
const { retries = 3, delay = 1000, backoff = 2, shouldRetry } = opts;
|
|
183
|
-
let lastError;
|
|
184
|
-
let currentDelay = delay;
|
|
185
|
-
|
|
186
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
187
|
-
try {
|
|
188
|
-
return await fn();
|
|
189
|
-
} catch (err) {
|
|
190
|
-
lastError = err;
|
|
191
|
-
|
|
192
|
-
if (attempt === retries) break;
|
|
193
|
-
if (shouldRetry && !shouldRetry(err, attempt)) break;
|
|
194
|
-
|
|
195
|
-
await new Promise(r => setTimeout(r, currentDelay));
|
|
196
|
-
currentDelay *= backoff;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
throw lastError;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
204
|
-
// 뮤텍스
|
|
205
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* 동시 실행 방지 (in-process lock)
|
|
209
|
-
*
|
|
210
|
-
* 같은 key에 대해 동시에 1개만 실행.
|
|
211
|
-
* 이전 실행 완료 후 다음 실행.
|
|
212
|
-
*
|
|
213
|
-
* @param {string} key - 락 키
|
|
214
|
-
* @param {Function} fn - 실행할 함수
|
|
215
|
-
* @returns {Promise<*>}
|
|
216
|
-
*
|
|
217
|
-
* @example
|
|
218
|
-
* await this.mutex('order:process:' + orderId, async () => {
|
|
219
|
-
* const order = await this.db.Order.findOrFail(orderId);
|
|
220
|
-
* await order.update({ status: 'processing' });
|
|
221
|
-
* });
|
|
222
|
-
*/
|
|
223
|
-
async mutex(key, fn) {
|
|
224
|
-
// 이전 락이 있으면 대기
|
|
225
|
-
while (_locks.has(key)) {
|
|
226
|
-
await _locks.get(key);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 새 락 설정
|
|
230
|
-
let releaseLock;
|
|
231
|
-
const lockPromise = new Promise(resolve => { releaseLock = resolve; });
|
|
232
|
-
_locks.set(key, lockPromise);
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
return await fn();
|
|
236
|
-
} finally {
|
|
237
|
-
_locks.delete(key);
|
|
238
|
-
releaseLock();
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
243
|
-
// 에러
|
|
244
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* 에러 생성 헬퍼
|
|
248
|
-
* @param {string} message
|
|
249
|
-
* @param {number} [status=400]
|
|
250
|
-
* @returns {ServiceError}
|
|
251
|
-
*/
|
|
252
|
-
error(message, status = 400) {
|
|
253
|
-
return new ServiceError(message, status);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 캐시 스토어 접근 (테스트용)
|
|
259
|
-
* @returns {Map}
|
|
260
|
-
*/
|
|
261
|
-
export { _cache, _locks };
|
|
1
|
+
/**
|
|
2
|
+
* Service — 비즈니스 로직 기본 클래스
|
|
3
|
+
*
|
|
4
|
+
* 컨트롤러는 얇게, 비즈니스 로직은 서비스에.
|
|
5
|
+
* 트랜잭션, 캐싱, 병렬 실행, 재시도, 뮤텍스 지원.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/framework/07-service-layer.md
|
|
8
|
+
* @see docs/framework/class-design.mm.md (Service)
|
|
9
|
+
*/
|
|
10
|
+
import Base from '../core/Base.js';
|
|
11
|
+
import { ServiceError } from '../core/AppError.js';
|
|
12
|
+
|
|
13
|
+
/** @type {Map<string, { value: *, expires: number }>} 전역 캐시 스토어 */
|
|
14
|
+
const _cache = new Map();
|
|
15
|
+
|
|
16
|
+
/** @type {Map<string, Promise>} 전역 뮤텍스 락 */
|
|
17
|
+
const _locks = new Map();
|
|
18
|
+
|
|
19
|
+
export default class Service extends Base {
|
|
20
|
+
|
|
21
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
22
|
+
// 트랜잭션
|
|
23
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SQL 트랜잭션 실행
|
|
27
|
+
* @param {Function} callback - (trx) => Promise<T>
|
|
28
|
+
* @returns {Promise<T>}
|
|
29
|
+
*/
|
|
30
|
+
async transaction(callback) {
|
|
31
|
+
const cm = this.app?._connectionManager;
|
|
32
|
+
if (!cm) throw new Error('Database not configured for transactions');
|
|
33
|
+
|
|
34
|
+
const conn = cm.get();
|
|
35
|
+
if (conn.type === 'knex' && conn.db) {
|
|
36
|
+
return conn.db.transaction(callback);
|
|
37
|
+
}
|
|
38
|
+
throw new Error('SQL database not configured for transactions');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* MongoDB 트랜잭션 실행
|
|
43
|
+
* @param {string} connName - MongoDB 연결명
|
|
44
|
+
* @param {Function} callback - (session) => Promise<T>
|
|
45
|
+
* @returns {Promise<T>}
|
|
46
|
+
*/
|
|
47
|
+
async mongoTransaction(connName, callback) {
|
|
48
|
+
const cm = this.app?._connectionManager;
|
|
49
|
+
if (!cm) throw new Error('Database not configured');
|
|
50
|
+
|
|
51
|
+
const conn = cm.get(connName);
|
|
52
|
+
if (conn.type !== 'mongo' || !conn.mongoose) {
|
|
53
|
+
throw new Error(`MongoDB connection '${connName}' not found`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const session = await conn.mongoose.startSession();
|
|
57
|
+
try {
|
|
58
|
+
session.startTransaction();
|
|
59
|
+
const result = await callback(session);
|
|
60
|
+
await session.commitTransaction();
|
|
61
|
+
return result;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
await session.abortTransaction();
|
|
64
|
+
throw err;
|
|
65
|
+
} finally {
|
|
66
|
+
session.endSession();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
71
|
+
// 캐시
|
|
72
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* TTL 기반 인메모리 캐시로 감싸기
|
|
76
|
+
*
|
|
77
|
+
* @param {string} key - 캐시 키
|
|
78
|
+
* @param {number} ttl - TTL (초)
|
|
79
|
+
* @param {Function} fn - 캐시 미스 시 실행할 함수
|
|
80
|
+
* @returns {Promise<*>}
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const stats = await this.withCache('dashboard:stats', 300, async () => {
|
|
84
|
+
* return this.db.Order.raw('SELECT SUM(total) ...');
|
|
85
|
+
* });
|
|
86
|
+
*/
|
|
87
|
+
async withCache(key, ttl, fn) {
|
|
88
|
+
const cached = _cache.get(key);
|
|
89
|
+
if (cached && cached.expires > Date.now()) {
|
|
90
|
+
return cached.value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const value = await fn();
|
|
94
|
+
_cache.set(key, {
|
|
95
|
+
value,
|
|
96
|
+
expires: Date.now() + (ttl * 1000),
|
|
97
|
+
});
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 캐시 키 무효화
|
|
103
|
+
* @param {string} key
|
|
104
|
+
*/
|
|
105
|
+
invalidateCache(key) {
|
|
106
|
+
_cache.delete(key);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 전체 캐시 초기화
|
|
111
|
+
*/
|
|
112
|
+
clearCache() {
|
|
113
|
+
_cache.clear();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 캐시 존재 여부
|
|
118
|
+
* @param {string} key
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
121
|
+
hasCache(key) {
|
|
122
|
+
const cached = _cache.get(key);
|
|
123
|
+
return cached ? cached.expires > Date.now() : false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
127
|
+
// 병렬 실행
|
|
128
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 여러 함수를 병렬 실행
|
|
132
|
+
*
|
|
133
|
+
* @param {Array<Function>} tasks - () => Promise 형태의 함수 배열
|
|
134
|
+
* @param {object} [opts]
|
|
135
|
+
* @param {boolean} [opts.failFast=false] - true면 하나 실패 시 전체 reject
|
|
136
|
+
* @returns {Promise<Array>} 결과 배열 (failFast=false 시 에러도 포함)
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* const [users, orders, stats] = await this.runParallel([
|
|
140
|
+
* () => this.db.User.count(),
|
|
141
|
+
* () => this.db.Order.where('status', 'active').get(),
|
|
142
|
+
* () => this.calculateStats(),
|
|
143
|
+
* ]);
|
|
144
|
+
*/
|
|
145
|
+
async runParallel(tasks, opts = {}) {
|
|
146
|
+
const promises = tasks.map(fn => fn());
|
|
147
|
+
|
|
148
|
+
if (opts.failFast) {
|
|
149
|
+
return Promise.all(promises);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const results = await Promise.allSettled(promises);
|
|
153
|
+
return results.map(r => {
|
|
154
|
+
if (r.status === 'fulfilled') return r.value;
|
|
155
|
+
// 실패 시 에러 객체 반환 (throw하지 않음)
|
|
156
|
+
return r.reason;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
161
|
+
// 재시도
|
|
162
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 재시도 + 지수 백오프
|
|
166
|
+
*
|
|
167
|
+
* @param {Function} fn - 실행할 함수
|
|
168
|
+
* @param {object} [opts]
|
|
169
|
+
* @param {number} [opts.retries=3] - 재시도 횟수
|
|
170
|
+
* @param {number} [opts.delay=1000] - 초기 지연 (ms)
|
|
171
|
+
* @param {number} [opts.backoff=2] - 백오프 배수
|
|
172
|
+
* @param {Function} [opts.shouldRetry] - (err, attempt) => boolean
|
|
173
|
+
* @returns {Promise<*>}
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* const data = await this.retryable(
|
|
177
|
+
* () => fetch('https://api.external.com/data'),
|
|
178
|
+
* { retries: 3, delay: 1000, backoff: 2 }
|
|
179
|
+
* );
|
|
180
|
+
*/
|
|
181
|
+
async retryable(fn, opts = {}) {
|
|
182
|
+
const { retries = 3, delay = 1000, backoff = 2, shouldRetry } = opts;
|
|
183
|
+
let lastError;
|
|
184
|
+
let currentDelay = delay;
|
|
185
|
+
|
|
186
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
187
|
+
try {
|
|
188
|
+
return await fn();
|
|
189
|
+
} catch (err) {
|
|
190
|
+
lastError = err;
|
|
191
|
+
|
|
192
|
+
if (attempt === retries) break;
|
|
193
|
+
if (shouldRetry && !shouldRetry(err, attempt)) break;
|
|
194
|
+
|
|
195
|
+
await new Promise(r => setTimeout(r, currentDelay));
|
|
196
|
+
currentDelay *= backoff;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw lastError;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
204
|
+
// 뮤텍스
|
|
205
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 동시 실행 방지 (in-process lock)
|
|
209
|
+
*
|
|
210
|
+
* 같은 key에 대해 동시에 1개만 실행.
|
|
211
|
+
* 이전 실행 완료 후 다음 실행.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} key - 락 키
|
|
214
|
+
* @param {Function} fn - 실행할 함수
|
|
215
|
+
* @returns {Promise<*>}
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* await this.mutex('order:process:' + orderId, async () => {
|
|
219
|
+
* const order = await this.db.Order.findOrFail(orderId);
|
|
220
|
+
* await order.update({ status: 'processing' });
|
|
221
|
+
* });
|
|
222
|
+
*/
|
|
223
|
+
async mutex(key, fn) {
|
|
224
|
+
// 이전 락이 있으면 대기
|
|
225
|
+
while (_locks.has(key)) {
|
|
226
|
+
await _locks.get(key);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 새 락 설정
|
|
230
|
+
let releaseLock;
|
|
231
|
+
const lockPromise = new Promise(resolve => { releaseLock = resolve; });
|
|
232
|
+
_locks.set(key, lockPromise);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
return await fn();
|
|
236
|
+
} finally {
|
|
237
|
+
_locks.delete(key);
|
|
238
|
+
releaseLock();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
243
|
+
// 에러
|
|
244
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 에러 생성 헬퍼
|
|
248
|
+
* @param {string} message
|
|
249
|
+
* @param {number} [status=400]
|
|
250
|
+
* @returns {ServiceError}
|
|
251
|
+
*/
|
|
252
|
+
error(message, status = 400) {
|
|
253
|
+
return new ServiceError(message, status);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 캐시 스토어 접근 (테스트용)
|
|
259
|
+
* @returns {Map}
|
|
260
|
+
*/
|
|
261
|
+
export { _cache, _locks };
|