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