koatty_schedule 3.3.1 → 3.3.3

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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @Author: richen
3
- * @Date: 2025-06-22 23:41:54
3
+ * @Date: 2025-10-31 17:50:55
4
4
  * @License: BSD (3-Clause)
5
5
  * @Copyright (c) - <richenlin(at)gmail.com>
6
6
  * @HomePage: https://koatty.org/
@@ -8,10 +8,10 @@
8
8
  'use strict';
9
9
 
10
10
  var koatty_container = require('koatty_container');
11
- var koatty_lib = require('koatty_lib');
12
11
  var redlock = require('@sesamecare-oss/redlock');
13
- var ioredis = require('ioredis');
14
12
  var koatty_logger = require('koatty_logger');
13
+ var Redis = require('ioredis');
14
+ var koatty_lib = require('koatty_lib');
15
15
  var cron = require('cron');
16
16
 
17
17
  /*
@@ -24,7 +24,6 @@ var cron = require('cron');
24
24
  * @Copyright (c): <richenlin(at)gmail.com>
25
25
  */
26
26
  const COMPONENT_SCHEDULED = 'COMPONENT_SCHEDULED';
27
- const COMPONENT_REDLOCK = 'COMPONENT_REDLOCK';
28
27
  /**
29
28
  * Decorator types supported by the system
30
29
  */
@@ -34,45 +33,110 @@ var DecoratorType;
34
33
  DecoratorType["REDLOCK"] = "REDLOCK";
35
34
  })(DecoratorType || (DecoratorType = {}));
36
35
  /**
37
- * Validate cron expression format
36
+ * Validate cron expression format (supports both 5-part and 6-part formats)
37
+ *
38
+ * 6-part format: second minute hour day month weekday
39
+ * 5-part format: minute hour day month weekday
40
+ *
38
41
  * @param cron - Cron expression to validate
39
42
  * @throws {Error} When cron expression is invalid
40
43
  */
41
44
  function validateCronExpression(cron) {
42
45
  if (!cron || typeof cron !== 'string') {
43
- throw new Error('Cron expression must be a non-empty string');
46
+ throw new Error('Cron 表达式必须是非空字符串 (Cron expression must be a non-empty string)');
44
47
  }
45
48
  const cronParts = cron.trim().split(/\s+/);
46
49
  // Cron expressions should have 5 or 6 parts (with or without seconds)
47
50
  if (cronParts.length < 5 || cronParts.length > 6) {
48
- throw new Error(`Invalid cron expression format. Expected 5 or 6 parts, got ${cronParts.length}`);
51
+ throw new Error(`Cron 表达式格式无效。期望 5 或 6 部分,实际得到 ${cronParts.length} 部分 (Invalid cron format. Expected 5 or 6 parts, got ${cronParts.length})`);
49
52
  }
50
- // For 6-part cron (with seconds), validate each part
51
- if (cronParts.length === 6) {
52
- const [seconds, minutes, hours] = cronParts;
53
- // Basic validation for obvious invalid values
54
- if (!/^(\*|[0-9]|[0-5][0-9]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(seconds)) {
55
- throw new Error('Invalid seconds field in cron expression');
53
+ // Determine if this is a 6-part (with seconds) or 5-part expression
54
+ const hasSecs = cronParts.length === 6;
55
+ const offset = hasSecs ? 0 : -1;
56
+ // Extract parts with proper indexing
57
+ const seconds = hasSecs ? cronParts[0] : null;
58
+ const minutes = cronParts[offset + 1];
59
+ const hours = cronParts[offset + 2];
60
+ const dayOfMonth = cronParts[offset + 3];
61
+ const month = cronParts[offset + 4];
62
+ const dayOfWeek = cronParts[offset + 5];
63
+ // Validate seconds (0-59) if present
64
+ if (seconds !== null) {
65
+ validateCronField(seconds, 0, 59, 'seconds', '秒');
66
+ }
67
+ // Validate minutes (0-59)
68
+ validateCronField(minutes, 0, 59, 'minutes', '分钟');
69
+ // Validate hours (0-23)
70
+ validateCronField(hours, 0, 23, 'hours', '小时');
71
+ // Validate day of month (1-31)
72
+ validateCronField(dayOfMonth, 1, 31, 'day of month', '日期');
73
+ // Validate month (1-12 or JAN-DEC)
74
+ validateCronField(month, 1, 12, 'month', '月份', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']);
75
+ // Validate day of week (0-7 or SUN-SAT, where 0 and 7 both represent Sunday)
76
+ validateCronField(dayOfWeek, 0, 7, 'day of week', '星期', ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']);
77
+ }
78
+ /**
79
+ * Validate individual cron field
80
+ * @param field - Field value to validate
81
+ * @param min - Minimum allowed value
82
+ * @param max - Maximum allowed value
83
+ * @param fieldName - English field name for error messages
84
+ * @param fieldNameCN - Chinese field name for error messages
85
+ * @param allowedStrings - Optional array of allowed string values (e.g., month/weekday names)
86
+ */
87
+ function validateCronField(field, min, max, fieldName, fieldNameCN, allowedStrings) {
88
+ // Allow wildcard
89
+ if (field === '*') {
90
+ return;
91
+ }
92
+ // Allow question mark (for day of month / day of week)
93
+ if (field === '?') {
94
+ return;
95
+ }
96
+ // Check for allowed string values (month/weekday names)
97
+ if (allowedStrings && allowedStrings.some(str => field.toUpperCase().includes(str))) {
98
+ return;
99
+ }
100
+ // Step values (e.g., */5, 0-30/5)
101
+ if (field.includes('/')) {
102
+ const [range, step] = field.split('/');
103
+ const stepValue = parseInt(step);
104
+ if (isNaN(stepValue) || stepValue <= 0) {
105
+ throw new Error(`${fieldNameCN}字段的步长值无效: ${step} (Invalid step value for ${fieldName}: ${step})`);
106
+ }
107
+ if (range !== '*') {
108
+ validateCronField(range, min, max, fieldName, fieldNameCN, allowedStrings);
56
109
  }
57
- if (!/^(\*|[0-9]|[0-5][0-9]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(minutes)) {
58
- throw new Error('Invalid minutes field in cron expression');
110
+ return;
111
+ }
112
+ // Range values (e.g., 1-5)
113
+ if (field.includes('-')) {
114
+ const [start, end] = field.split('-');
115
+ const startValue = parseInt(start);
116
+ const endValue = parseInt(end);
117
+ if (isNaN(startValue) || startValue < min || startValue > max) {
118
+ throw new Error(`${fieldNameCN}字段的范围起始值无效: ${start},必须在 ${min}-${max} 之间 (Invalid range start for ${fieldName}: ${start}, must be between ${min}-${max})`);
59
119
  }
60
- if (!/^(\*|[0-9]|1[0-9]|2[0-3]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(hours)) {
61
- throw new Error('Invalid hours field in cron expression');
120
+ if (isNaN(endValue) || endValue < min || endValue > max) {
121
+ throw new Error(`${fieldNameCN}字段的范围结束值无效: ${end},必须在 ${min}-${max} 之间 (Invalid range end for ${fieldName}: ${end}, must be between ${min}-${max})`);
62
122
  }
63
- // Check for simple out-of-range values
64
- const secondsValue = parseInt(seconds);
65
- if (!isNaN(secondsValue) && (secondsValue < 0 || secondsValue > 59)) {
66
- throw new Error('Seconds value must be between 0 and 59');
123
+ if (startValue > endValue) {
124
+ throw new Error(`${fieldNameCN}字段的范围无效: ${start}-${end},起始值不能大于结束值 (Invalid range for ${fieldName}: ${start}-${end}, start cannot be greater than end)`);
67
125
  }
126
+ return;
68
127
  }
69
- // Additional basic checks for common invalid patterns
70
- if (cron.includes('60')) {
71
- // Check if 60 appears as a standalone number (not part of a larger number)
72
- const parts = cron.split(/[\s,\-\/]/);
73
- if (parts.some(part => part === '60')) {
74
- throw new Error('Invalid time value: 60 is not valid for any time field');
128
+ // List values (e.g., 1,3,5)
129
+ if (field.includes(',')) {
130
+ const values = field.split(',');
131
+ for (const value of values) {
132
+ validateCronField(value.trim(), min, max, fieldName, fieldNameCN, allowedStrings);
75
133
  }
134
+ return;
135
+ }
136
+ // Single numeric value
137
+ const numValue = parseInt(field);
138
+ if (isNaN(numValue) || numValue < min || numValue > max) {
139
+ throw new Error(`${fieldNameCN}字段的值无效: ${field},必须在 ${min}-${max} 之间 (Invalid ${fieldName} value: ${field}, must be between ${min}-${max})`);
76
140
  }
77
141
  }
78
142
  /**
@@ -110,13 +174,6 @@ function validateRedLockMethodOptions(options) {
110
174
  * Global configuration storage
111
175
  */
112
176
  let globalScheduledOptions = {};
113
- /**
114
- * Set global scheduled options
115
- * @param options - Global scheduled options
116
- */
117
- function setGlobalScheduledOptions(options) {
118
- globalScheduledOptions = { ...options };
119
- }
120
177
  /**
121
178
  * Get global scheduled options
122
179
  * @returns Global scheduled options
@@ -129,8 +186,8 @@ function getGlobalScheduledOptions() {
129
186
  * @param userTimezone - User specified timezone
130
187
  * @returns Effective timezone
131
188
  */
132
- function getEffectiveTimezone(userTimezone) {
133
- return userTimezone || globalScheduledOptions.timezone || 'Asia/Beijing';
189
+ function getEffectiveTimezone(options, userTimezone) {
190
+ return userTimezone || options.timezone || 'Asia/Beijing';
134
191
  }
135
192
  /**
136
193
  * Get effective RedLock method options with priority: method options > global options > defaults
@@ -148,147 +205,247 @@ function getEffectiveRedLockOptions(methodOptions) {
148
205
  }
149
206
 
150
207
  /*
151
- * @Description:
208
+ * @Description: Abstract interfaces for Redis client and distributed lock
152
209
  * @Usage:
153
210
  * @Author: richen
154
- * @Date: 2025-06-09 16:00:00
155
- * @LastEditTime: 2025-06-09 16:00:00
211
+ * @Date: 2025-10-30 12:00:00
212
+ * @LastEditTime: 2025-10-30 12:00:00
156
213
  * @License: BSD (3-Clause)
157
214
  * @Copyright (c): <richenlin(at)gmail.com>
158
215
  */
159
216
  /**
160
- * Redis-based distributed lock decorator
161
- *
162
- * @export
163
- * @param {string} [name] - The locker name. If name is duplicated, lock sharing contention will result.
164
- * If not provided, a unique name will be auto-generated using method name + random suffix.
165
- * IMPORTANT: Auto-generated names are unique per method deployment and not predictable.
166
- * @param {RedLockMethodOptions} [options] - Lock configuration options for this method
167
- *
168
- * @returns {MethodDecorator}
169
- * @throws {Error} When decorator is used on wrong class type or invalid configuration
170
- *
171
- * @example
172
- * ```typescript
173
- * class UserService {
174
- * @RedLock('user_update_lock', { lockTimeOut: 5000, maxRetries: 2 })
175
- * async updateUser(id: string, data: any) {
176
- * // This method will be protected by a distributed lock with predictable name
177
- * }
178
- *
179
- * @RedLock() // Auto-generated unique name like "deleteUser_abc123_xyz789"
180
- * async deleteUser(id: string) {
181
- * // This method will be protected by a distributed lock with auto-generated unique name
182
- * }
183
- * }
184
- * ```
217
+ * Redis connection mode
185
218
  */
186
- function RedLock(lockName, options) {
187
- return (target, propertyKey, descriptor) => {
188
- const methodName = propertyKey.toString();
189
- // 验证装饰器使用的类型(从原型对象获取类构造函数)
190
- const targetClass = target.constructor;
191
- const componentType = koatty_container.IOCContainer.getType(targetClass);
192
- if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
193
- throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes.");
194
- }
195
- // 验证方法名
196
- if (!methodName || typeof methodName !== 'string') {
197
- throw Error("Method name is required for @RedLock decorator");
198
- }
199
- // 验证方法描述符
200
- if (!descriptor || typeof descriptor.value !== 'function') {
201
- throw Error("@RedLock decorator can only be applied to methods");
202
- }
203
- // 生成唯一的锁名称:用户指定的 > 自动生成的唯一名称
204
- if (!lockName || lockName.trim() === '') {
205
- const randomSuffix = Math.random().toString(36).substring(2, 8); // 6位随机字符
206
- const timestamp = Date.now().toString(36); // 时间戳转36进制
207
- lockName = `${methodName}_${randomSuffix}_${timestamp}`;
208
- }
209
- // 验证选项
210
- if (options) {
211
- validateRedLockMethodOptions(options);
212
- }
213
- // 保存类到IOC容器
214
- koatty_container.IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
215
- // 保存RedLock元数据到 IOC 容器(lockName已确定)
216
- koatty_container.IOCContainer.attachClassMetadata(COMPONENT_REDLOCK, DecoratorType.REDLOCK, {
217
- method: methodName,
218
- name: lockName, // 确定的锁名称,不会为undefined
219
- options
220
- }, target, methodName);
221
- };
222
- }
219
+ exports.RedisMode = void 0;
220
+ (function (RedisMode) {
221
+ RedisMode["STANDALONE"] = "standalone";
222
+ RedisMode["SENTINEL"] = "sentinel";
223
+ RedisMode["CLUSTER"] = "cluster"; // 集群模式
224
+ })(exports.RedisMode || (exports.RedisMode = {}));
223
225
 
224
226
  /*
225
- * @Description:
227
+ * @Description: Redis client factory supporting multiple modes
226
228
  * @Usage:
227
229
  * @Author: richen
228
- * @Date: 2025-06-09 16:00:00
229
- * @LastEditTime: 2025-06-09 16:00:00
230
+ * @Date: 2025-10-30 12:00:00
231
+ * @LastEditTime: 2025-10-30 12:00:00
230
232
  * @License: BSD (3-Clause)
231
233
  * @Copyright (c): <richenlin(at)gmail.com>
232
234
  */
233
235
  /**
234
- * Schedule task decorator with optimized preprocessing
235
- *
236
- * @export
237
- * @param {string} cron - Cron expression for task scheduling
238
- * @param {string} [timezone='Asia/Beijing'] - Timezone for the schedule
239
- *
240
- * Cron expression format:
241
- * * Seconds: 0-59
242
- * * Minutes: 0-59
243
- * * Hours: 0-23
244
- * * Day of Month: 1-31
245
- * * Months: 1-12 (Jan-Dec)
246
- * * Day of Week: 1-7 (Sun-Sat)
247
- *
248
- * @returns {MethodDecorator}
249
- * @throws {Error} When cron expression is invalid or decorator is used on wrong class type
236
+ * Redis client wrapper that implements IRedisClient interface
237
+ * Wraps ioredis client to provide unified interface
250
238
  */
251
- function Scheduled(cron, timezone) {
252
- // 参数验证
253
- if (koatty_lib.Helper.isEmpty(cron)) {
254
- throw Error("Cron expression is required and cannot be empty");
239
+ class RedisClientAdapter {
240
+ client;
241
+ constructor(client) {
242
+ this.client = client;
255
243
  }
256
- // 验证cron表达式格式
257
- try {
258
- validateCronExpression(cron);
244
+ get status() {
245
+ return this.client.status;
259
246
  }
260
- catch (error) {
261
- throw Error(`Invalid cron expression: ${error.message}`);
247
+ async call(command, ...args) {
248
+ return this.client.call(command, ...args);
262
249
  }
263
- // 验证时区
264
- if (timezone && typeof timezone !== 'string') {
265
- throw Error("Timezone must be a string");
250
+ async set(key, value, mode, duration) {
251
+ if (mode && duration) {
252
+ // Use type assertion for mode parameter due to ioredis strict typing
253
+ return this.client.set(key, value, mode, duration);
254
+ }
255
+ return this.client.set(key, value);
266
256
  }
267
- return (target, propertyKey, descriptor) => {
268
- // 验证装饰器使用的类型(从原型对象获取类构造函数)
269
- const targetClass = target.constructor;
270
- const componentType = koatty_container.IOCContainer.getType(targetClass);
271
- if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
272
- throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes.");
257
+ async get(key) {
258
+ return this.client.get(key);
259
+ }
260
+ async del(...keys) {
261
+ return this.client.del(...keys);
262
+ }
263
+ async exists(key) {
264
+ return this.client.exists(key);
265
+ }
266
+ async eval(script, numKeys, ...args) {
267
+ return this.client.eval(script, numKeys, ...args);
268
+ }
269
+ async quit() {
270
+ return this.client.quit();
271
+ }
272
+ disconnect() {
273
+ this.client.disconnect();
274
+ }
275
+ /**
276
+ * Get underlying Redis/Cluster instance
277
+ * Used for RedLock initialization
278
+ */
279
+ getClient() {
280
+ return this.client;
281
+ }
282
+ }
283
+ /**
284
+ * Redis client factory
285
+ * Creates appropriate Redis client based on configuration
286
+ */
287
+ class RedisFactory {
288
+ /**
289
+ * Create Redis client based on configuration mode
290
+ * @param config - Redis configuration
291
+ * @returns Redis client adapter
292
+ */
293
+ static createClient(config) {
294
+ const mode = config.mode || exports.RedisMode.STANDALONE;
295
+ koatty_logger.DefaultLogger.Debug(`Creating Redis client in ${mode} mode`);
296
+ switch (mode) {
297
+ case exports.RedisMode.STANDALONE:
298
+ return this.createStandaloneClient(config);
299
+ case exports.RedisMode.SENTINEL:
300
+ return this.createSentinelClient(config);
301
+ case exports.RedisMode.CLUSTER:
302
+ return this.createClusterClient(config);
303
+ default:
304
+ throw new Error(`不支持的 Redis 模式: ${mode} (Unsupported Redis mode: ${mode})`);
273
305
  }
274
- // 验证方法名
275
- const methodName = propertyKey.toString();
276
- if (!methodName || typeof methodName !== 'string') {
277
- throw Error("Method name is required for @Scheduled decorator");
306
+ }
307
+ /**
308
+ * Create standalone Redis client
309
+ * @param config - Standalone configuration
310
+ */
311
+ static createStandaloneClient(config) {
312
+ koatty_logger.DefaultLogger.Debug(`Creating standalone Redis client: ${config.host}:${config.port}`);
313
+ const options = {
314
+ host: config.host,
315
+ port: config.port,
316
+ password: config.password || undefined,
317
+ db: config.db || 0,
318
+ keyPrefix: config.keyPrefix || '',
319
+ connectTimeout: config.connectTimeout || 10000,
320
+ commandTimeout: config.commandTimeout || 5000,
321
+ maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
322
+ retryStrategy: (times) => {
323
+ const delay = Math.min(times * 50, 2000);
324
+ koatty_logger.DefaultLogger.Debug(`Redis reconnecting, attempt ${times}, delay ${delay}ms`);
325
+ return delay;
326
+ },
327
+ reconnectOnError: (err) => {
328
+ koatty_logger.DefaultLogger.Warn('Redis connection error, attempting reconnect:', err.message);
329
+ return true;
330
+ }
331
+ };
332
+ const client = new Redis(options);
333
+ client.on('connect', () => {
334
+ koatty_logger.DefaultLogger.Info('Redis standalone client connected successfully');
335
+ });
336
+ client.on('error', (err) => {
337
+ koatty_logger.DefaultLogger.Error('Redis standalone client error:', err);
338
+ });
339
+ return new RedisClientAdapter(client);
340
+ }
341
+ /**
342
+ * Create sentinel Redis client
343
+ * @param config - Sentinel configuration
344
+ */
345
+ static createSentinelClient(config) {
346
+ koatty_logger.DefaultLogger.Debug(`Creating sentinel Redis client for master: ${config.name}`);
347
+ const options = {
348
+ sentinels: config.sentinels,
349
+ name: config.name,
350
+ password: config.password || undefined,
351
+ sentinelPassword: config.sentinelPassword || undefined,
352
+ db: config.db || 0,
353
+ keyPrefix: config.keyPrefix || '',
354
+ connectTimeout: config.connectTimeout || 10000,
355
+ commandTimeout: config.commandTimeout || 5000,
356
+ maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
357
+ retryStrategy: (times) => {
358
+ const delay = Math.min(times * 50, 2000);
359
+ koatty_logger.DefaultLogger.Debug(`Sentinel Redis reconnecting, attempt ${times}, delay ${delay}ms`);
360
+ return delay;
361
+ }
362
+ };
363
+ const client = new Redis(options);
364
+ client.on('connect', () => {
365
+ koatty_logger.DefaultLogger.Info(`Redis sentinel client connected to master: ${config.name}`);
366
+ });
367
+ client.on('error', (err) => {
368
+ koatty_logger.DefaultLogger.Error('Redis sentinel client error:', err);
369
+ });
370
+ return new RedisClientAdapter(client);
371
+ }
372
+ /**
373
+ * Create cluster Redis client
374
+ * @param config - Cluster configuration
375
+ */
376
+ static createClusterClient(config) {
377
+ koatty_logger.DefaultLogger.Debug(`Creating cluster Redis client with ${config.nodes.length} nodes`);
378
+ const clusterOptions = {
379
+ redisOptions: {
380
+ password: config.redisOptions?.password || config.password || undefined,
381
+ db: config.redisOptions?.db || config.db || 0,
382
+ keyPrefix: config.keyPrefix || '',
383
+ connectTimeout: config.connectTimeout || 10000,
384
+ commandTimeout: config.commandTimeout || 5000,
385
+ maxRetriesPerRequest: config.maxRetriesPerRequest || 3
386
+ },
387
+ clusterRetryStrategy: (times) => {
388
+ const delay = Math.min(times * 50, 2000);
389
+ koatty_logger.DefaultLogger.Debug(`Cluster Redis reconnecting, attempt ${times}, delay ${delay}ms`);
390
+ return delay;
391
+ }
392
+ };
393
+ const cluster = new Redis.Cluster(config.nodes, clusterOptions);
394
+ cluster.on('connect', () => {
395
+ koatty_logger.DefaultLogger.Info('Redis cluster client connected successfully');
396
+ });
397
+ cluster.on('error', (err) => {
398
+ koatty_logger.DefaultLogger.Error('Redis cluster client error:', err);
399
+ });
400
+ cluster.on('node error', (err, address) => {
401
+ koatty_logger.DefaultLogger.Error(`Redis cluster node error at ${address}:`, err);
402
+ });
403
+ return new RedisClientAdapter(cluster);
404
+ }
405
+ /**
406
+ * Validate Redis configuration
407
+ * @param config - Redis configuration to validate
408
+ */
409
+ static validateConfig(config) {
410
+ if (!config) {
411
+ throw new Error('Redis 配置不能为空 (Redis configuration cannot be empty)');
278
412
  }
279
- // 验证方法描述符
280
- if (!descriptor || typeof descriptor.value !== 'function') {
281
- throw Error("@Scheduled decorator can only be applied to methods");
413
+ const mode = config.mode || exports.RedisMode.STANDALONE;
414
+ switch (mode) {
415
+ case exports.RedisMode.STANDALONE:
416
+ this.validateStandaloneConfig(config);
417
+ break;
418
+ case exports.RedisMode.SENTINEL:
419
+ this.validateSentinelConfig(config);
420
+ break;
421
+ case exports.RedisMode.CLUSTER:
422
+ this.validateClusterConfig(config);
423
+ break;
424
+ default:
425
+ throw new Error(`不支持的 Redis 模式: ${mode} (Unsupported Redis mode: ${mode})`);
282
426
  }
283
- // 保存类到IOC容器
284
- koatty_container.IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
285
- // 保存调度元数据到 IOC 容器
286
- koatty_container.IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
287
- method: methodName,
288
- cron,
289
- timezone // 保存用户指定的值,可能为undefined
290
- }, target, methodName);
291
- };
427
+ }
428
+ static validateStandaloneConfig(config) {
429
+ if (!config.host) {
430
+ throw new Error('单机模式需要 host 配置 (Standalone mode requires host configuration)');
431
+ }
432
+ if (!config.port) {
433
+ throw new Error('单机模式需要 port 配置 (Standalone mode requires port configuration)');
434
+ }
435
+ }
436
+ static validateSentinelConfig(config) {
437
+ if (!config.sentinels || config.sentinels.length === 0) {
438
+ throw new Error('哨兵模式需要至少一个哨兵节点配置 (Sentinel mode requires at least one sentinel node)');
439
+ }
440
+ if (!config.name) {
441
+ throw new Error('哨兵模式需要 master name 配置 (Sentinel mode requires master name)');
442
+ }
443
+ }
444
+ static validateClusterConfig(config) {
445
+ if (!config.nodes || config.nodes.length === 0) {
446
+ throw new Error('集群模式需要至少一个节点配置 (Cluster mode requires at least one node)');
447
+ }
448
+ }
292
449
  }
293
450
 
294
451
  /*
@@ -308,12 +465,8 @@ const defaultRedLockConfig = {
308
465
  clockDriftFactor: 0.01,
309
466
  maxRetries: 3,
310
467
  retryDelayMs: 200,
311
- driftFactor: 0.01,
312
- retryCount: 3,
313
- retryDelay: 200,
314
- retryJitter: 200,
315
- automaticExtensionThreshold: 500,
316
468
  redisConfig: {
469
+ mode: exports.RedisMode.STANDALONE,
317
470
  host: '127.0.0.1',
318
471
  port: 6379,
319
472
  password: '',
@@ -321,16 +474,27 @@ const defaultRedLockConfig = {
321
474
  keyPrefix: 'redlock:'
322
475
  }
323
476
  };
477
+ /**
478
+ * Default Redlock Settings for @sesamecare-oss/redlock
479
+ */
480
+ const defaultRedlockSettings = {
481
+ driftFactor: 0.01,
482
+ retryCount: 3,
483
+ retryDelay: 200,
484
+ retryJitter: 200,
485
+ automaticExtensionThreshold: 500
486
+ };
324
487
  /**
325
488
  * RedLock distributed lock manager
326
489
  * Integrated with koatty IOC container
327
490
  * Implements singleton pattern for safe instance management
491
+ * Implements IDistributedLock interface for abstraction
328
492
  */
329
493
  class RedLocker {
330
494
  static instance = null;
331
495
  static instanceLock = Symbol('RedLocker.instanceLock');
332
496
  redlock = null;
333
- redis = null;
497
+ redisClient = null;
334
498
  config;
335
499
  isInitialized = false;
336
500
  initializationPromise = null;
@@ -424,8 +588,12 @@ class RedLocker {
424
588
  await this.initializationPromise;
425
589
  }
426
590
  catch (error) {
427
- // 初始化失败时清理缓存,允许重试
591
+ // 初始化失败时完整清理状态,允许重试
428
592
  this.initializationPromise = null;
593
+ this.isInitialized = false;
594
+ this.redlock = null;
595
+ // 注意:不清理 redis 连接,因为它可能来自 IOC 容器
596
+ koatty_logger.DefaultLogger.Warn('RedLocker initialization failed, state has been reset for retry');
429
597
  throw error;
430
598
  }
431
599
  }
@@ -435,34 +603,54 @@ class RedLocker {
435
603
  */
436
604
  async performInitialization() {
437
605
  try {
606
+ // Validate Redis configuration
607
+ if (this.config.redisConfig) {
608
+ RedisFactory.validateConfig(this.config.redisConfig);
609
+ }
438
610
  // Try to get Redis instance from IOC container first
439
611
  try {
440
- this.redis = koatty_container.IOCContainer.get('Redis', 'COMPONENT');
441
- koatty_logger.DefaultLogger.Debug('Using Redis instance from IOC container');
612
+ const existingRedis = koatty_container.IOCContainer.get('Redis', 'COMPONENT');
613
+ // If Redis instance exists in container, wrap it
614
+ if (existingRedis) {
615
+ // Check if it's already a RedisClientAdapter
616
+ if (existingRedis instanceof RedisClientAdapter) {
617
+ this.redisClient = existingRedis;
618
+ }
619
+ else {
620
+ // Wrap raw Redis/Cluster instance (type assertion needed)
621
+ this.redisClient = new RedisClientAdapter(existingRedis);
622
+ }
623
+ koatty_logger.DefaultLogger.Debug('Using Redis instance from IOC container');
624
+ }
442
625
  }
443
626
  catch {
444
- // Create new Redis connection if not available in container
445
- this.redis = new ioredis.Redis({
446
- host: this.config.redisConfig.host,
447
- port: this.config.redisConfig.port,
448
- password: this.config.redisConfig.password || undefined,
449
- db: this.config.redisConfig.db || 0,
450
- keyPrefix: this.config.redisConfig.keyPrefix,
451
- maxRetriesPerRequest: 3
452
- });
627
+ // IOC container doesn't have Redis, create new connection
628
+ }
629
+ // Create new Redis connection if not available in container
630
+ if (!this.redisClient && this.config.redisConfig) {
631
+ this.redisClient = RedisFactory.createClient(this.config.redisConfig);
453
632
  koatty_logger.DefaultLogger.Debug('Created new Redis connection for RedLocker');
454
633
  }
455
- if (!this.redis) {
456
- throw new Error('Failed to initialize Redis connection');
634
+ if (!this.redisClient) {
635
+ throw new Error('Failed to initialize Redis connection: no configuration provided');
457
636
  }
637
+ // Get underlying client for Redlock
638
+ const underlyingClient = this.redisClient.getClient();
639
+ // Merge default settings with user configuration
640
+ // Extract Settings properties from config (which extends Partial<Settings>)
641
+ const userSettings = this.config;
642
+ const redlockSettings = {
643
+ ...defaultRedlockSettings,
644
+ ...(userSettings.driftFactor !== undefined && { driftFactor: userSettings.driftFactor }),
645
+ ...(userSettings.retryCount !== undefined && { retryCount: userSettings.retryCount }),
646
+ ...(userSettings.retryDelay !== undefined && { retryDelay: userSettings.retryDelay }),
647
+ ...(userSettings.retryJitter !== undefined && { retryJitter: userSettings.retryJitter }),
648
+ ...(userSettings.automaticExtensionThreshold !== undefined && {
649
+ automaticExtensionThreshold: userSettings.automaticExtensionThreshold
650
+ })
651
+ };
458
652
  // Initialize Redlock with the Redis instance
459
- this.redlock = new redlock.Redlock([this.redis], {
460
- driftFactor: this.config.driftFactor,
461
- retryCount: this.config.retryCount,
462
- retryDelay: this.config.retryDelay,
463
- retryJitter: this.config.retryJitter,
464
- automaticExtensionThreshold: this.config.automaticExtensionThreshold
465
- });
653
+ this.redlock = new redlock.Redlock([underlyingClient], redlockSettings);
466
654
  // Set up error handlers
467
655
  this.redlock.on('clientError', (err) => {
468
656
  koatty_logger.DefaultLogger.Error('Redis client error in RedLock:', err);
@@ -563,7 +751,7 @@ class RedLocker {
563
751
  * @returns true if initialized, false otherwise
564
752
  */
565
753
  isReady() {
566
- return this.isInitialized && !!this.redlock && !!this.redis;
754
+ return this.isInitialized && !!this.redlock && !!this.redisClient;
567
755
  }
568
756
  /**
569
757
  * Get current configuration
@@ -591,11 +779,11 @@ class RedLocker {
591
779
  */
592
780
  async close() {
593
781
  try {
594
- if (this.redis && this.redis.status === 'ready') {
595
- await this.redis.quit();
782
+ if (this.redisClient && this.redisClient.status === 'ready') {
783
+ await this.redisClient.quit();
596
784
  koatty_logger.DefaultLogger.Debug('Redis connection closed');
597
785
  }
598
- this.redis = null;
786
+ this.redisClient = null;
599
787
  this.redlock = null;
600
788
  this.isInitialized = false;
601
789
  }
@@ -629,13 +817,14 @@ class RedLocker {
629
817
  async healthCheck() {
630
818
  try {
631
819
  await this.initialize();
632
- const redisStatus = this.redis?.status || 'unknown';
820
+ const redisStatus = this.redisClient?.status || 'unknown';
633
821
  const isReady = this.isReady();
634
822
  return {
635
823
  status: isReady ? 'healthy' : 'unhealthy',
636
824
  details: {
637
825
  initialized: this.isInitialized,
638
826
  redisStatus,
827
+ redisMode: this.config.redisConfig?.mode || 'unknown',
639
828
  redlockReady: !!this.redlock,
640
829
  containerRegistered: this.getContainerInfo().registered
641
830
  }
@@ -662,18 +851,22 @@ class RedLocker {
662
851
  * @License: BSD (3-Clause)
663
852
  * @Copyright (c): <richenlin(at)gmail.com>
664
853
  */
665
- /**
666
- * 返回一个 Promise,在指定时间后 reject
667
- * @param ms
668
- * @returns
669
- */
670
854
  function timeoutPromise(ms) {
671
- return new Promise((resolve, reject) => {
672
- const timeoutId = setTimeout(() => {
673
- clearTimeout(timeoutId);
855
+ let timeoutId = null;
856
+ const promise = new Promise((resolve, reject) => {
857
+ timeoutId = setTimeout(() => {
858
+ timeoutId = null;
674
859
  reject(new Error('TIME_OUT_ERROR'));
675
860
  }, ms);
676
861
  });
862
+ // 添加取消方法,防止内存泄漏
863
+ promise.cancel = () => {
864
+ if (timeoutId !== null) {
865
+ clearTimeout(timeoutId);
866
+ timeoutId = null;
867
+ }
868
+ };
869
+ return promise;
677
870
  }
678
871
 
679
872
  /*
@@ -688,6 +881,8 @@ function timeoutPromise(ms) {
688
881
  /**
689
882
  * Initiation schedule locker client.
690
883
  *
884
+ * @param {RedLockOptions} options - RedLock 配置选项
885
+ * @param {Koatty} app - Koatty 应用实例
691
886
  * @returns {Promise<void>}
692
887
  */
693
888
  async function initRedLock(options, app) {
@@ -695,11 +890,12 @@ async function initRedLock(options, app) {
695
890
  koatty_logger.DefaultLogger.Warn(`RedLock initialization skipped: Koatty app not available or not initialized`);
696
891
  return;
697
892
  }
698
- app.once("appStart", async function () {
893
+ app.once("appReady", async function () {
699
894
  try {
700
895
  if (koatty_lib.Helper.isEmpty(options)) {
701
896
  throw Error(`Missing RedLock configuration. Please write a configuration item with the key name 'RedLock' in the db.ts file.`);
702
897
  }
898
+ // 获取RedLocker实例,在首次使用时自动初始化
703
899
  const redLocker = RedLocker.getInstance(options);
704
900
  await redLocker.initialize();
705
901
  koatty_logger.DefaultLogger.Info('RedLock initialized successfully');
@@ -710,72 +906,12 @@ async function initRedLock(options, app) {
710
906
  }
711
907
  });
712
908
  }
713
- /**
714
- * 批量注入RedLock锁 - 从IOC容器读取类元数据并应用所有RedLock装饰器
715
- *
716
- * @param {RedLockOptions} options - RedLock 配置选项
717
- * @param {Koatty} app - Koatty 应用实例
718
- */
719
- async function injectRedLock(_options, _app) {
720
- try {
721
- koatty_logger.DefaultLogger.Debug('Starting batch RedLock injection...');
722
- const componentList = koatty_container.IOCContainer.listClass("COMPONENT");
723
- for (const component of componentList) {
724
- const classMetadata = koatty_container.IOCContainer.getClassMetadata(COMPONENT_REDLOCK, DecoratorType.REDLOCK, component.target);
725
- if (!classMetadata) {
726
- continue;
727
- }
728
- let redlockCount = 0;
729
- for (const [className, metadata] of classMetadata) {
730
- try {
731
- const instance = koatty_container.IOCContainer.get(className);
732
- if (!instance) {
733
- continue;
734
- }
735
- // 查找所有RedLock方法的元数据
736
- for (const [key, value] of Object.entries(metadata)) {
737
- if (key.startsWith('REDLOCK')) {
738
- const redlockData = value;
739
- const targetMethod = instance[redlockData.method];
740
- if (!koatty_lib.Helper.isFunction(targetMethod)) {
741
- koatty_logger.DefaultLogger.Warn(`RedLock injection skipped: method ${redlockData.method} is not a function in ${className}`);
742
- continue;
743
- }
744
- // 生成有效的RedLock选项:方法级别 > 全局配置 > 默认值
745
- const effectiveOptions = getEffectiveRedLockOptions(redlockData.options);
746
- // 应用RedLock增强描述符
747
- const originalDescriptor = {
748
- value: targetMethod,
749
- writable: true,
750
- enumerable: false,
751
- configurable: true
752
- };
753
- const enhancedDescriptor = redLockerDescriptor(originalDescriptor, redlockData.name, // 使用装饰器中确定的锁名称
754
- redlockData.method, effectiveOptions);
755
- // 替换原方法
756
- Object.defineProperty(instance, redlockData.method, enhancedDescriptor);
757
- redlockCount++;
758
- koatty_logger.DefaultLogger.Debug(`RedLock applied to ${className}.${redlockData.method} with lock name: ${redlockData.name}`);
759
- }
760
- }
761
- }
762
- catch (error) {
763
- koatty_logger.DefaultLogger.Error(`Failed to process class ${className}:`, error);
764
- }
765
- }
766
- koatty_logger.DefaultLogger.Info(`Batch RedLock injection completed. ${redlockCount} locks applied.`);
767
- }
768
- }
769
- catch (error) {
770
- koatty_logger.DefaultLogger.Error('Failed to inject RedLocks:', error);
771
- }
772
- }
773
909
  /**
774
910
  * Create redLocker Descriptor with improved error handling and type safety
775
911
  * @param descriptor - Property descriptor
776
912
  * @param name - Lock name
777
913
  * @param method - Method name
778
- * @param options - RedLock options
914
+ * @param methodOptions - Method-level RedLock options
779
915
  * @returns Enhanced property descriptor
780
916
  */
781
917
  function redLockerDescriptor(descriptor, name, method, methodOptions) {
@@ -794,13 +930,6 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
794
930
  if (typeof value !== 'function') {
795
931
  throw new Error('Descriptor value must be a function');
796
932
  }
797
- // 设置默认选项,合并方法级别的选项
798
- const lockOptions = {
799
- lockTimeOut: methodOptions?.lockTimeOut,
800
- clockDriftFactor: methodOptions?.clockDriftFactor,
801
- maxRetries: methodOptions?.maxRetries,
802
- retryDelayMs: methodOptions?.retryDelayMs
803
- };
804
933
  /**
805
934
  * Enhanced function wrapper with proper lock renewal and safety
806
935
  */
@@ -811,15 +940,21 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
811
940
  let extensionCount = 0;
812
941
  try {
813
942
  while (remainingTime > 0 && extensionCount < maxExtensions) {
943
+ // 创建可取消的超时 Promise
944
+ const timeoutHandler = timeoutPromise(remainingTime);
814
945
  try {
815
946
  // 执行业务方法,与超时竞争
816
947
  const result = await Promise.race([
817
948
  value.apply(self, props),
818
- timeoutPromise(remainingTime)
949
+ timeoutHandler
819
950
  ]);
820
- return result; // 成功执行,返回业务结果
951
+ // 成功执行,取消超时定时器防止内存泄漏
952
+ timeoutHandler.cancel();
953
+ return result;
821
954
  }
822
955
  catch (error) {
956
+ // 无论什么错误,都要取消超时定时器
957
+ timeoutHandler.cancel();
823
958
  // 处理超时错误,尝试续期锁
824
959
  if (error instanceof Error && error.message === 'TIME_OUT_ERROR') {
825
960
  extensionCount++;
@@ -864,6 +999,7 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
864
999
  async value(...props) {
865
1000
  try {
866
1001
  const redlock = RedLocker.getInstance();
1002
+ const lockOptions = getEffectiveRedLockOptions(methodOptions);
867
1003
  // Acquire a lock.
868
1004
  const lockTime = lockOptions.lockTimeOut || 10000;
869
1005
  if (lockTime <= 200) {
@@ -881,6 +1017,169 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
881
1017
  },
882
1018
  };
883
1019
  }
1020
+ /**
1021
+ * Generate lock name for RedLock decorator
1022
+ */
1023
+ function generateLockName(configName, methodName, target) {
1024
+ if (configName) {
1025
+ return configName;
1026
+ }
1027
+ try {
1028
+ const targetObj = target;
1029
+ const identifier = koatty_container.IOCContainer.getIdentifier(targetObj);
1030
+ if (identifier) {
1031
+ return `${identifier}_${methodName}`;
1032
+ }
1033
+ }
1034
+ catch {
1035
+ // Fallback if IOC container is not available
1036
+ }
1037
+ const targetWithConstructor = target;
1038
+ const className = targetWithConstructor.constructor?.name || 'Unknown';
1039
+ return `${className}_${methodName}`;
1040
+ }
1041
+
1042
+ /*
1043
+ * @Description:
1044
+ * @Usage:
1045
+ * @Author: richen
1046
+ * @Date: 2025-06-09 16:00:00
1047
+ * @LastEditTime: 2025-06-09 16:00:00
1048
+ * @License: BSD (3-Clause)
1049
+ * @Copyright (c): <richenlin(at)gmail.com>
1050
+ */
1051
+ /**
1052
+ * Redis-based distributed lock decorator
1053
+ *
1054
+ * @export
1055
+ * @param {string} [name] - The locker name. If name is duplicated, lock sharing contention will result.
1056
+ * If not provided, a unique name will be auto-generated using method name + random suffix.
1057
+ * IMPORTANT: Auto-generated names are unique per method deployment and not predictable.
1058
+ * @param {RedLockMethodOptions} [options] - Lock configuration options for this method
1059
+ *
1060
+ * @returns {MethodDecorator}
1061
+ * @throws {Error} When decorator is used on wrong class type or invalid configuration
1062
+ *
1063
+ * @example
1064
+ * ```typescript
1065
+ * class UserService {
1066
+ * @RedLock('user_update_lock', { lockTimeOut: 5000, maxRetries: 2 })
1067
+ * async updateUser(id: string, data: any) {
1068
+ * // This method will be protected by a distributed lock with predictable name
1069
+ * }
1070
+ *
1071
+ * @RedLock() // Auto-generated unique name like "deleteUser_abc123_xyz789"
1072
+ * async deleteUser(id: string) {
1073
+ * // This method will be protected by a distributed lock with auto-generated unique name
1074
+ * }
1075
+ * }
1076
+ * ```
1077
+ */
1078
+ function RedLock(lockName, options) {
1079
+ return (target, propertyKey, descriptor) => {
1080
+ const methodName = propertyKey.toString();
1081
+ // 验证装饰器使用的类型(从原型对象获取类构造函数)
1082
+ const targetClass = target.constructor;
1083
+ const componentType = koatty_container.IOCContainer.getType(targetClass);
1084
+ if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
1085
+ throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes.");
1086
+ }
1087
+ // 验证方法名
1088
+ if (!methodName || typeof methodName !== 'string') {
1089
+ throw Error("Method name is required for @RedLock decorator");
1090
+ }
1091
+ // 验证方法描述符
1092
+ if (!descriptor || typeof descriptor.value !== 'function') {
1093
+ throw Error("@RedLock decorator can only be applied to methods");
1094
+ }
1095
+ // 生成锁名称:用户指定的 > 基于类名和方法名生成
1096
+ const finalLockName = lockName || generateLockName(lockName, methodName, target);
1097
+ // 验证选项
1098
+ if (options) {
1099
+ validateRedLockMethodOptions(options);
1100
+ }
1101
+ // 保存类到IOC容器
1102
+ koatty_container.IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
1103
+ try {
1104
+ // 直接在装饰器中包装方法,而不是延迟处理
1105
+ const enhancedDescriptor = redLockerDescriptor(descriptor, finalLockName, methodName, options);
1106
+ return enhancedDescriptor;
1107
+ }
1108
+ catch (error) {
1109
+ throw new Error(`Failed to apply RedLock to ${methodName}: ${error.message}`);
1110
+ }
1111
+ };
1112
+ }
1113
+
1114
+ /*
1115
+ * @Description:
1116
+ * @Usage:
1117
+ * @Author: richen
1118
+ * @Date: 2025-06-09 16:00:00
1119
+ * @LastEditTime: 2025-06-09 16:00:00
1120
+ * @License: BSD (3-Clause)
1121
+ * @Copyright (c): <richenlin(at)gmail.com>
1122
+ */
1123
+ /**
1124
+ * Schedule task decorator with optimized preprocessing
1125
+ *
1126
+ * @export
1127
+ * @param {string} cron - Cron expression for task scheduling
1128
+ * @param {string} [timezone='Asia/Beijing'] - Timezone for the schedule
1129
+ *
1130
+ * Cron expression format:
1131
+ * * Seconds: 0-59
1132
+ * * Minutes: 0-59
1133
+ * * Hours: 0-23
1134
+ * * Day of Month: 1-31
1135
+ * * Months: 1-12 (Jan-Dec)
1136
+ * * Day of Week: 1-7 (Sun-Sat)
1137
+ *
1138
+ * @returns {MethodDecorator}
1139
+ * @throws {Error} When cron expression is invalid or decorator is used on wrong class type
1140
+ */
1141
+ function Scheduled(cron, timezone = 'Asia/Beijing') {
1142
+ // 参数验证
1143
+ if (koatty_lib.Helper.isEmpty(cron)) {
1144
+ throw Error("Cron expression is required and cannot be empty");
1145
+ }
1146
+ // 验证cron表达式格式
1147
+ try {
1148
+ validateCronExpression(cron);
1149
+ }
1150
+ catch (error) {
1151
+ throw Error(`Invalid cron expression: ${error.message}`);
1152
+ }
1153
+ // 验证时区
1154
+ if (timezone && typeof timezone !== 'string') {
1155
+ throw Error("Timezone must be a string");
1156
+ }
1157
+ return (target, propertyKey, descriptor) => {
1158
+ // 验证装饰器使用的类型(从原型对象获取类构造函数)
1159
+ const targetClass = target.constructor;
1160
+ const componentType = koatty_container.IOCContainer.getType(targetClass);
1161
+ if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
1162
+ throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes.");
1163
+ }
1164
+ // 验证方法名
1165
+ const methodName = propertyKey.toString();
1166
+ if (!methodName || typeof methodName !== 'string') {
1167
+ throw Error("Method name is required for @Scheduled decorator");
1168
+ }
1169
+ // 验证方法描述符
1170
+ if (!descriptor || typeof descriptor.value !== 'function') {
1171
+ throw Error("@Scheduled decorator can only be applied to methods");
1172
+ }
1173
+ // 保存类到IOC容器
1174
+ koatty_container.IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
1175
+ // 保存调度元数据到 IOC 容器
1176
+ koatty_container.IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
1177
+ method: methodName,
1178
+ cron,
1179
+ timezone // 保存确定的时区值
1180
+ }, target, methodName);
1181
+ };
1182
+ }
884
1183
 
885
1184
  /**
886
1185
  * @ author: richen
@@ -888,6 +1187,29 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
888
1187
  * @ license: MIT
889
1188
  * @ version: 2020-07-06 10:29:20
890
1189
  */
1190
+ /**
1191
+ * 初始化调度任务系统
1192
+ * 在appReady时触发批量注入调度任务,确保所有初始化工作完成
1193
+ *
1194
+ * @param {Koatty} app - Koatty 应用实例
1195
+ * @param {any} options - 调度任务配置
1196
+ */
1197
+ async function initSchedule(options, app) {
1198
+ if (!app || !koatty_lib.Helper.isFunction(app.once)) {
1199
+ koatty_logger.DefaultLogger.Warn(`Schedule initialization skipped: Koatty app not available or not initialized`);
1200
+ return;
1201
+ }
1202
+ app.once("appReady", async function () {
1203
+ try {
1204
+ await injectSchedule(options);
1205
+ koatty_logger.DefaultLogger.Info('Schedule system initialized successfully');
1206
+ }
1207
+ catch (error) {
1208
+ koatty_logger.DefaultLogger.Error('Failed to initialize Schedule system:', error);
1209
+ throw error;
1210
+ }
1211
+ });
1212
+ }
891
1213
  /**
892
1214
  * Inject schedule job with enhanced error handling and validation
893
1215
  *
@@ -898,11 +1220,8 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
898
1220
  */
899
1221
  /**
900
1222
  * 批量注入调度任务 - 从IOC容器读取类元数据并创建所有CronJob
901
- *
902
- * @param {RedLockOptions} options - RedLock 配置选项
903
- * @param {Koatty} app - Koatty 应用实例
904
1223
  */
905
- async function injectSchedule(_options, _app) {
1224
+ async function injectSchedule(options) {
906
1225
  try {
907
1226
  koatty_logger.DefaultLogger.Debug('Starting batch schedule injection...');
908
1227
  const componentList = koatty_container.IOCContainer.listClass("COMPONENT");
@@ -928,7 +1247,7 @@ async function injectSchedule(_options, _app) {
928
1247
  continue;
929
1248
  }
930
1249
  const taskName = `${className}_${scheduleData.method}`;
931
- const tz = getEffectiveTimezone(scheduleData.timezone);
1250
+ const tz = getEffectiveTimezone(options, scheduleData.timezone);
932
1251
  new cron.CronJob(scheduleData.cron, () => {
933
1252
  koatty_logger.DefaultLogger.Debug(`The schedule job ${taskName} started.`);
934
1253
  Promise.resolve(targetMethod.call(instance))
@@ -979,6 +1298,7 @@ const defaultOptions = {
979
1298
  maxRetries: 3,
980
1299
  retryDelayMs: 200,
981
1300
  redisConfig: {
1301
+ mode: exports.RedisMode.STANDALONE,
982
1302
  host: "localhost",
983
1303
  port: 6379,
984
1304
  password: "",
@@ -992,14 +1312,16 @@ const defaultOptions = {
992
1312
  */
993
1313
  async function KoattyScheduled(options, app) {
994
1314
  options = { ...defaultOptions, ...options };
995
- // 保存全局配置
996
- setGlobalScheduledOptions(options);
1315
+ // 初始化RedLock(appReady时触发,确保所有依赖就绪)
997
1316
  await initRedLock(options, app);
998
- await injectRedLock();
999
- await injectSchedule();
1317
+ // 初始化调度任务系统(appReady时触发,确保所有组件都已初始化)
1318
+ await initSchedule(options, app);
1000
1319
  }
1001
1320
 
1002
1321
  exports.KoattyScheduled = KoattyScheduled;
1003
1322
  exports.RedLock = RedLock;
1323
+ exports.RedLocker = RedLocker;
1324
+ exports.RedisClientAdapter = RedisClientAdapter;
1325
+ exports.RedisFactory = RedisFactory;
1004
1326
  exports.Scheduled = Scheduled;
1005
1327
  exports.SchedulerLock = SchedulerLock;