koatty_schedule 3.3.2 → 3.3.4

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,14 +1,14 @@
1
1
  /*!
2
2
  * @Author: richen
3
- * @Date: 2025-06-23 00:55:12
3
+ * @Date: 2026-01-28 13:35:25
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
9
  import { Redlock } from '@sesamecare-oss/redlock';
10
- import { Redis } from 'ioredis';
11
10
  import { DefaultLogger } from 'koatty_logger';
11
+ import Redis, { Cluster } from 'ioredis';
12
12
  import { Helper } from 'koatty_lib';
13
13
  import { CronJob } from 'cron';
14
14
 
@@ -31,45 +31,110 @@ var DecoratorType;
31
31
  DecoratorType["REDLOCK"] = "REDLOCK";
32
32
  })(DecoratorType || (DecoratorType = {}));
33
33
  /**
34
- * 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
+ *
35
39
  * @param cron - Cron expression to validate
36
40
  * @throws {Error} When cron expression is invalid
37
41
  */
38
42
  function validateCronExpression(cron) {
39
43
  if (!cron || typeof cron !== 'string') {
40
- throw new Error('Cron expression must be a non-empty string');
44
+ throw new Error('Cron 表达式必须是非空字符串 (Cron expression must be a non-empty string)');
41
45
  }
42
46
  const cronParts = cron.trim().split(/\s+/);
43
47
  // Cron expressions should have 5 or 6 parts (with or without seconds)
44
48
  if (cronParts.length < 5 || cronParts.length > 6) {
45
- 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})`);
50
+ }
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;
46
97
  }
47
- // For 6-part cron (with seconds), validate each part
48
- if (cronParts.length === 6) {
49
- const [seconds, minutes, hours] = cronParts;
50
- // Basic validation for obvious invalid values
51
- if (!/^(\*|[0-9]|[0-5][0-9]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(seconds)) {
52
- throw new Error('Invalid seconds field in cron expression');
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})`);
53
104
  }
54
- if (!/^(\*|[0-9]|[0-5][0-9]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(minutes)) {
55
- throw new Error('Invalid minutes field in cron expression');
105
+ if (range !== '*') {
106
+ validateCronField(range, min, max, fieldName, fieldNameCN, allowedStrings);
56
107
  }
57
- if (!/^(\*|[0-9]|1[0-9]|2[0-3]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(hours)) {
58
- throw new Error('Invalid hours 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})`);
59
117
  }
60
- // Check for simple out-of-range values
61
- const secondsValue = parseInt(seconds);
62
- if (!isNaN(secondsValue) && (secondsValue < 0 || secondsValue > 59)) {
63
- throw new Error('Seconds value must be between 0 and 59');
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})`);
64
120
  }
121
+ if (startValue > endValue) {
122
+ throw new Error(`${fieldNameCN}字段的范围无效: ${start}-${end},起始值不能大于结束值 (Invalid range for ${fieldName}: ${start}-${end}, start cannot be greater than end)`);
123
+ }
124
+ return;
65
125
  }
66
- // Additional basic checks for common invalid patterns
67
- if (cron.includes('60')) {
68
- // Check if 60 appears as a standalone number (not part of a larger number)
69
- const parts = cron.split(/[\s,\-\/]/);
70
- if (parts.some(part => part === '60')) {
71
- 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);
72
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})`);
73
138
  }
74
139
  }
75
140
  /**
@@ -130,13 +195,257 @@ function getEffectiveTimezone(options, userTimezone) {
130
195
  function getEffectiveRedLockOptions(methodOptions) {
131
196
  const globalOptions = getGlobalScheduledOptions();
132
197
  return {
133
- lockTimeOut: methodOptions?.lockTimeOut || globalOptions.lockTimeOut || 10000,
134
- clockDriftFactor: methodOptions?.clockDriftFactor || globalOptions.clockDriftFactor || 0.01,
135
- maxRetries: methodOptions?.maxRetries || globalOptions.maxRetries || 3,
136
- retryDelayMs: methodOptions?.retryDelayMs || globalOptions.retryDelayMs || 200
198
+ lockTimeOut: (methodOptions === null || methodOptions === void 0 ? void 0 : methodOptions.lockTimeOut) || globalOptions.lockTimeOut || 10000,
199
+ clockDriftFactor: (methodOptions === null || methodOptions === void 0 ? void 0 : methodOptions.clockDriftFactor) || globalOptions.clockDriftFactor || 0.01,
200
+ maxRetries: (methodOptions === null || methodOptions === void 0 ? void 0 : methodOptions.maxRetries) || globalOptions.maxRetries || 3,
201
+ retryDelayMs: (methodOptions === null || methodOptions === void 0 ? void 0 : methodOptions.retryDelayMs) || globalOptions.retryDelayMs || 200
137
202
  };
138
203
  }
139
204
 
205
+ /*
206
+ * @Description: Abstract interfaces for Redis client and distributed lock
207
+ * @Usage:
208
+ * @Author: richen
209
+ * @Date: 2025-10-30 12:00:00
210
+ * @LastEditTime: 2025-10-30 12:00:00
211
+ * @License: BSD (3-Clause)
212
+ * @Copyright (c): <richenlin(at)gmail.com>
213
+ */
214
+ /**
215
+ * Redis connection mode
216
+ */
217
+ var RedisMode;
218
+ (function (RedisMode) {
219
+ RedisMode["STANDALONE"] = "standalone";
220
+ RedisMode["SENTINEL"] = "sentinel";
221
+ RedisMode["CLUSTER"] = "cluster"; // 集群模式
222
+ })(RedisMode || (RedisMode = {}));
223
+
224
+ /*
225
+ * @Description: Redis client factory supporting multiple modes
226
+ * @Usage:
227
+ * @Author: richen
228
+ * @Date: 2025-10-30 12:00:00
229
+ * @LastEditTime: 2025-10-30 12:00:00
230
+ * @License: BSD (3-Clause)
231
+ * @Copyright (c): <richenlin(at)gmail.com>
232
+ */
233
+ /**
234
+ * Redis client wrapper that implements IRedisClient interface
235
+ * Wraps ioredis client to provide unified interface
236
+ */
237
+ class RedisClientAdapter {
238
+ constructor(client) {
239
+ this.client = client;
240
+ }
241
+ get status() {
242
+ return this.client.status;
243
+ }
244
+ async call(command, ...args) {
245
+ return this.client.call(command, ...args);
246
+ }
247
+ async set(key, value, mode, duration) {
248
+ if (mode && duration) {
249
+ // Use type assertion for mode parameter due to ioredis strict typing
250
+ return this.client.set(key, value, mode, duration);
251
+ }
252
+ return this.client.set(key, value);
253
+ }
254
+ async get(key) {
255
+ return this.client.get(key);
256
+ }
257
+ async del(...keys) {
258
+ return this.client.del(...keys);
259
+ }
260
+ async exists(key) {
261
+ return this.client.exists(key);
262
+ }
263
+ async eval(script, numKeys, ...args) {
264
+ return this.client.eval(script, numKeys, ...args);
265
+ }
266
+ async quit() {
267
+ return this.client.quit();
268
+ }
269
+ disconnect() {
270
+ this.client.disconnect();
271
+ }
272
+ /**
273
+ * Get underlying Redis/Cluster instance
274
+ * Used for RedLock initialization
275
+ */
276
+ getClient() {
277
+ return this.client;
278
+ }
279
+ }
280
+ /**
281
+ * Redis client factory
282
+ * Creates appropriate Redis client based on configuration
283
+ */
284
+ class RedisFactory {
285
+ /**
286
+ * Create Redis client based on configuration mode
287
+ * @param config - Redis configuration
288
+ * @returns Redis client adapter
289
+ */
290
+ static createClient(config) {
291
+ const mode = config.mode || RedisMode.STANDALONE;
292
+ DefaultLogger.Debug(`Creating Redis client in ${mode} mode`);
293
+ switch (mode) {
294
+ case RedisMode.STANDALONE:
295
+ return this.createStandaloneClient(config);
296
+ case RedisMode.SENTINEL:
297
+ return this.createSentinelClient(config);
298
+ case RedisMode.CLUSTER:
299
+ return this.createClusterClient(config);
300
+ default:
301
+ throw new Error(`不支持的 Redis 模式: ${mode} (Unsupported Redis mode: ${mode})`);
302
+ }
303
+ }
304
+ /**
305
+ * Create standalone Redis client
306
+ * @param config - Standalone configuration
307
+ */
308
+ static createStandaloneClient(config) {
309
+ DefaultLogger.Debug(`Creating standalone Redis client: ${config.host}:${config.port}`);
310
+ const options = {
311
+ host: config.host,
312
+ port: config.port,
313
+ password: config.password || undefined,
314
+ db: config.db || 0,
315
+ keyPrefix: config.keyPrefix || '',
316
+ connectTimeout: config.connectTimeout || 10000,
317
+ commandTimeout: config.commandTimeout || 5000,
318
+ maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
319
+ retryStrategy: (times) => {
320
+ const delay = Math.min(times * 50, 2000);
321
+ DefaultLogger.Debug(`Redis reconnecting, attempt ${times}, delay ${delay}ms`);
322
+ return delay;
323
+ },
324
+ reconnectOnError: (err) => {
325
+ DefaultLogger.Warn('Redis connection error, attempting reconnect:', err.message);
326
+ return true;
327
+ }
328
+ };
329
+ const client = new Redis(options);
330
+ client.on('connect', () => {
331
+ DefaultLogger.Info('Redis standalone client connected successfully');
332
+ });
333
+ client.on('error', (err) => {
334
+ DefaultLogger.Error('Redis standalone client error:', err);
335
+ });
336
+ return new RedisClientAdapter(client);
337
+ }
338
+ /**
339
+ * Create sentinel Redis client
340
+ * @param config - Sentinel configuration
341
+ */
342
+ static createSentinelClient(config) {
343
+ DefaultLogger.Debug(`Creating sentinel Redis client for master: ${config.name}`);
344
+ const options = {
345
+ sentinels: config.sentinels,
346
+ name: config.name,
347
+ password: config.password || undefined,
348
+ sentinelPassword: config.sentinelPassword || undefined,
349
+ db: config.db || 0,
350
+ keyPrefix: config.keyPrefix || '',
351
+ connectTimeout: config.connectTimeout || 10000,
352
+ commandTimeout: config.commandTimeout || 5000,
353
+ maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
354
+ retryStrategy: (times) => {
355
+ const delay = Math.min(times * 50, 2000);
356
+ DefaultLogger.Debug(`Sentinel Redis reconnecting, attempt ${times}, delay ${delay}ms`);
357
+ return delay;
358
+ }
359
+ };
360
+ const client = new Redis(options);
361
+ client.on('connect', () => {
362
+ DefaultLogger.Info(`Redis sentinel client connected to master: ${config.name}`);
363
+ });
364
+ client.on('error', (err) => {
365
+ DefaultLogger.Error('Redis sentinel client error:', err);
366
+ });
367
+ return new RedisClientAdapter(client);
368
+ }
369
+ /**
370
+ * Create cluster Redis client
371
+ * @param config - Cluster configuration
372
+ */
373
+ static createClusterClient(config) {
374
+ var _a, _b;
375
+ DefaultLogger.Debug(`Creating cluster Redis client with ${config.nodes.length} nodes`);
376
+ const clusterOptions = {
377
+ redisOptions: {
378
+ password: ((_a = config.redisOptions) === null || _a === void 0 ? void 0 : _a.password) || config.password || undefined,
379
+ db: ((_b = config.redisOptions) === null || _b === void 0 ? void 0 : _b.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)');
410
+ }
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})`);
424
+ }
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
+ }
447
+ }
448
+
140
449
  /*
141
450
  * @Description: RedLock utility for distributed locks
142
451
  * @Usage:
@@ -154,12 +463,8 @@ const defaultRedLockConfig = {
154
463
  clockDriftFactor: 0.01,
155
464
  maxRetries: 3,
156
465
  retryDelayMs: 200,
157
- driftFactor: 0.01,
158
- retryCount: 3,
159
- retryDelay: 200,
160
- retryJitter: 200,
161
- automaticExtensionThreshold: 500,
162
466
  redisConfig: {
467
+ mode: RedisMode.STANDALONE,
163
468
  host: '127.0.0.1',
164
469
  port: 6379,
165
470
  password: '',
@@ -167,21 +472,29 @@ const defaultRedLockConfig = {
167
472
  keyPrefix: 'redlock:'
168
473
  }
169
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
+ };
170
485
  /**
171
486
  * RedLock distributed lock manager
172
487
  * Integrated with koatty IOC container
173
488
  * Implements singleton pattern for safe instance management
489
+ * Implements IDistributedLock interface for abstraction
174
490
  */
175
491
  class RedLocker {
176
- static instance = null;
177
- static instanceLock = Symbol('RedLocker.instanceLock');
178
- redlock = null;
179
- redis = null;
180
- config;
181
- isInitialized = false;
182
- initializationPromise = null;
183
492
  // 私有构造函数防止外部直接实例化
184
493
  constructor(options) {
494
+ this.redlock = null;
495
+ this.redisClient = null;
496
+ this.isInitialized = false;
497
+ this.initializationPromise = null;
185
498
  this.config = { ...defaultRedLockConfig, ...options };
186
499
  // Register this instance in IOC container
187
500
  this.registerInContainer();
@@ -270,8 +583,12 @@ class RedLocker {
270
583
  await this.initializationPromise;
271
584
  }
272
585
  catch (error) {
273
- // 初始化失败时清理缓存,允许重试
586
+ // 初始化失败时完整清理状态,允许重试
274
587
  this.initializationPromise = null;
588
+ this.isInitialized = false;
589
+ this.redlock = null;
590
+ // 注意:不清理 redis 连接,因为它可能来自 IOC 容器
591
+ DefaultLogger.Warn('RedLocker initialization failed, state has been reset for retry');
275
592
  throw error;
276
593
  }
277
594
  }
@@ -281,34 +598,54 @@ class RedLocker {
281
598
  */
282
599
  async performInitialization() {
283
600
  try {
601
+ // Validate Redis configuration
602
+ if (this.config.redisConfig) {
603
+ RedisFactory.validateConfig(this.config.redisConfig);
604
+ }
284
605
  // Try to get Redis instance from IOC container first
285
606
  try {
286
- this.redis = IOCContainer.get('Redis', 'COMPONENT');
287
- DefaultLogger.Debug('Using Redis instance from IOC container');
607
+ const existingRedis = IOCContainer.get('Redis', 'COMPONENT');
608
+ // If Redis instance exists in container, wrap it
609
+ if (existingRedis) {
610
+ // Check if it's already a RedisClientAdapter
611
+ if (existingRedis instanceof RedisClientAdapter) {
612
+ this.redisClient = existingRedis;
613
+ }
614
+ else {
615
+ // Wrap raw Redis/Cluster instance (type assertion needed)
616
+ this.redisClient = new RedisClientAdapter(existingRedis);
617
+ }
618
+ DefaultLogger.Debug('Using Redis instance from IOC container');
619
+ }
288
620
  }
289
621
  catch {
290
- // Create new Redis connection if not available in container
291
- this.redis = new Redis({
292
- host: this.config.redisConfig.host,
293
- port: this.config.redisConfig.port,
294
- password: this.config.redisConfig.password || undefined,
295
- db: this.config.redisConfig.db || 0,
296
- keyPrefix: this.config.redisConfig.keyPrefix,
297
- maxRetriesPerRequest: 3
298
- });
622
+ // IOC container doesn't have Redis, create new connection
623
+ }
624
+ // Create new Redis connection if not available in container
625
+ if (!this.redisClient && this.config.redisConfig) {
626
+ this.redisClient = RedisFactory.createClient(this.config.redisConfig);
299
627
  DefaultLogger.Debug('Created new Redis connection for RedLocker');
300
628
  }
301
- if (!this.redis) {
302
- throw new Error('Failed to initialize Redis connection');
629
+ if (!this.redisClient) {
630
+ throw new Error('Failed to initialize Redis connection: no configuration provided');
303
631
  }
632
+ // Get underlying client for Redlock
633
+ const underlyingClient = this.redisClient.getClient();
634
+ // Merge default settings with user configuration
635
+ // Extract Settings properties from config (which extends Partial<Settings>)
636
+ const userSettings = this.config;
637
+ const redlockSettings = {
638
+ ...defaultRedlockSettings,
639
+ ...(userSettings.driftFactor !== undefined && { driftFactor: userSettings.driftFactor }),
640
+ ...(userSettings.retryCount !== undefined && { retryCount: userSettings.retryCount }),
641
+ ...(userSettings.retryDelay !== undefined && { retryDelay: userSettings.retryDelay }),
642
+ ...(userSettings.retryJitter !== undefined && { retryJitter: userSettings.retryJitter }),
643
+ ...(userSettings.automaticExtensionThreshold !== undefined && {
644
+ automaticExtensionThreshold: userSettings.automaticExtensionThreshold
645
+ })
646
+ };
304
647
  // Initialize Redlock with the Redis instance
305
- this.redlock = new Redlock([this.redis], {
306
- driftFactor: this.config.driftFactor,
307
- retryCount: this.config.retryCount,
308
- retryDelay: this.config.retryDelay,
309
- retryJitter: this.config.retryJitter,
310
- automaticExtensionThreshold: this.config.automaticExtensionThreshold
311
- });
648
+ this.redlock = new Redlock([underlyingClient], redlockSettings);
312
649
  // Set up error handlers
313
650
  this.redlock.on('clientError', (err) => {
314
651
  DefaultLogger.Error('Redis client error in RedLock:', err);
@@ -409,7 +746,7 @@ class RedLocker {
409
746
  * @returns true if initialized, false otherwise
410
747
  */
411
748
  isReady() {
412
- return this.isInitialized && !!this.redlock && !!this.redis;
749
+ return this.isInitialized && !!this.redlock && !!this.redisClient;
413
750
  }
414
751
  /**
415
752
  * Get current configuration
@@ -437,11 +774,11 @@ class RedLocker {
437
774
  */
438
775
  async close() {
439
776
  try {
440
- if (this.redis && this.redis.status === 'ready') {
441
- await this.redis.quit();
777
+ if (this.redisClient && this.redisClient.status === 'ready') {
778
+ await this.redisClient.quit();
442
779
  DefaultLogger.Debug('Redis connection closed');
443
780
  }
444
- this.redis = null;
781
+ this.redisClient = null;
445
782
  this.redlock = null;
446
783
  this.isInitialized = false;
447
784
  }
@@ -473,15 +810,17 @@ class RedLocker {
473
810
  * @returns Health status
474
811
  */
475
812
  async healthCheck() {
813
+ var _a, _b;
476
814
  try {
477
815
  await this.initialize();
478
- const redisStatus = this.redis?.status || 'unknown';
816
+ const redisStatus = ((_a = this.redisClient) === null || _a === void 0 ? void 0 : _a.status) || 'unknown';
479
817
  const isReady = this.isReady();
480
818
  return {
481
819
  status: isReady ? 'healthy' : 'unhealthy',
482
820
  details: {
483
821
  initialized: this.isInitialized,
484
822
  redisStatus,
823
+ redisMode: ((_b = this.config.redisConfig) === null || _b === void 0 ? void 0 : _b.mode) || 'unknown',
485
824
  redlockReady: !!this.redlock,
486
825
  containerRegistered: this.getContainerInfo().registered
487
826
  }
@@ -498,6 +837,8 @@ class RedLocker {
498
837
  }
499
838
  }
500
839
  }
840
+ RedLocker.instance = null;
841
+ RedLocker.instanceLock = Symbol('RedLocker.instanceLock');
501
842
 
502
843
  /*
503
844
  * @Description:
@@ -508,18 +849,22 @@ class RedLocker {
508
849
  * @License: BSD (3-Clause)
509
850
  * @Copyright (c): <richenlin(at)gmail.com>
510
851
  */
511
- /**
512
- * 返回一个 Promise,在指定时间后 reject
513
- * @param ms
514
- * @returns
515
- */
516
852
  function timeoutPromise(ms) {
517
- return new Promise((resolve, reject) => {
518
- const timeoutId = setTimeout(() => {
519
- clearTimeout(timeoutId);
853
+ let timeoutId = null;
854
+ const promise = new Promise((resolve, reject) => {
855
+ timeoutId = setTimeout(() => {
856
+ timeoutId = null;
520
857
  reject(new Error('TIME_OUT_ERROR'));
521
858
  }, ms);
522
859
  });
860
+ // 添加取消方法,防止内存泄漏
861
+ promise.cancel = () => {
862
+ if (timeoutId !== null) {
863
+ clearTimeout(timeoutId);
864
+ timeoutId = null;
865
+ }
866
+ };
867
+ return promise;
523
868
  }
524
869
 
525
870
  /*
@@ -543,21 +888,19 @@ async function initRedLock(options, app) {
543
888
  DefaultLogger.Warn(`RedLock initialization skipped: Koatty app not available or not initialized`);
544
889
  return;
545
890
  }
546
- app.once("appReady", async function () {
547
- try {
548
- if (Helper.isEmpty(options)) {
549
- throw Error(`Missing RedLock configuration. Please write a configuration item with the key name 'RedLock' in the db.ts file.`);
550
- }
551
- // 获取RedLocker实例,在首次使用时自动初始化
552
- const redLocker = RedLocker.getInstance(options);
553
- await redLocker.initialize();
554
- DefaultLogger.Info('RedLock initialized successfully');
555
- }
556
- catch (error) {
557
- DefaultLogger.Error('Failed to initialize RedLock:', error);
558
- throw error;
891
+ try {
892
+ if (Helper.isEmpty(options)) {
893
+ throw Error(`Missing RedLock configuration. Please write a configuration item with the key name 'RedLock' in the db.ts file.`);
559
894
  }
560
- });
895
+ // 获取RedLocker实例,在首次使用时自动初始化
896
+ const redLocker = RedLocker.getInstance(options);
897
+ await redLocker.initialize();
898
+ DefaultLogger.Info('RedLock initialized successfully');
899
+ }
900
+ catch (error) {
901
+ DefaultLogger.Error('Failed to initialize RedLock:', error);
902
+ throw error;
903
+ }
561
904
  }
562
905
  /**
563
906
  * Create redLocker Descriptor with improved error handling and type safety
@@ -593,15 +936,21 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
593
936
  let extensionCount = 0;
594
937
  try {
595
938
  while (remainingTime > 0 && extensionCount < maxExtensions) {
939
+ // 创建可取消的超时 Promise
940
+ const timeoutHandler = timeoutPromise(remainingTime);
596
941
  try {
597
942
  // 执行业务方法,与超时竞争
598
943
  const result = await Promise.race([
599
944
  value.apply(self, props),
600
- timeoutPromise(remainingTime)
945
+ timeoutHandler
601
946
  ]);
602
- return result; // 成功执行,返回业务结果
947
+ // 成功执行,取消超时定时器防止内存泄漏
948
+ timeoutHandler.cancel();
949
+ return result;
603
950
  }
604
951
  catch (error) {
952
+ // 无论什么错误,都要取消超时定时器
953
+ timeoutHandler.cancel();
605
954
  // 处理超时错误,尝试续期锁
606
955
  if (error instanceof Error && error.message === 'TIME_OUT_ERROR') {
607
956
  extensionCount++;
@@ -668,6 +1017,7 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
668
1017
  * Generate lock name for RedLock decorator
669
1018
  */
670
1019
  function generateLockName(configName, methodName, target) {
1020
+ var _a;
671
1021
  if (configName) {
672
1022
  return configName;
673
1023
  }
@@ -682,7 +1032,7 @@ function generateLockName(configName, methodName, target) {
682
1032
  // Fallback if IOC container is not available
683
1033
  }
684
1034
  const targetWithConstructor = target;
685
- const className = targetWithConstructor.constructor?.name || 'Unknown';
1035
+ const className = ((_a = targetWithConstructor.constructor) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown';
686
1036
  return `${className}_${methodName}`;
687
1037
  }
688
1038
 
@@ -846,16 +1196,14 @@ async function initSchedule(options, app) {
846
1196
  DefaultLogger.Warn(`Schedule initialization skipped: Koatty app not available or not initialized`);
847
1197
  return;
848
1198
  }
849
- app.once("appReady", async function () {
850
- try {
851
- await injectSchedule(options);
852
- DefaultLogger.Info('Schedule system initialized successfully');
853
- }
854
- catch (error) {
855
- DefaultLogger.Error('Failed to initialize Schedule system:', error);
856
- throw error;
857
- }
858
- });
1199
+ try {
1200
+ await injectSchedule(options);
1201
+ DefaultLogger.Info('Schedule system initialized successfully');
1202
+ }
1203
+ catch (error) {
1204
+ DefaultLogger.Error('Failed to initialize Schedule system:', error);
1205
+ throw error;
1206
+ }
859
1207
  }
860
1208
  /**
861
1209
  * Inject schedule job with enhanced error handling and validation
@@ -945,6 +1293,7 @@ const defaultOptions = {
945
1293
  maxRetries: 3,
946
1294
  retryDelayMs: 200,
947
1295
  redisConfig: {
1296
+ mode: RedisMode.STANDALONE,
948
1297
  host: "localhost",
949
1298
  port: 6379,
950
1299
  password: "",
@@ -958,10 +1307,12 @@ const defaultOptions = {
958
1307
  */
959
1308
  async function KoattyScheduled(options, app) {
960
1309
  options = { ...defaultOptions, ...options };
961
- // 初始化RedLock(appReady时触发,确保所有依赖就绪)
962
- await initRedLock(options, app);
963
- // 初始化调度任务系统(appReady时触发,确保所有组件都已初始化)
964
- await initSchedule(options, app);
1310
+ app.once("appReady", async function () {
1311
+ // 初始化RedLock
1312
+ await initRedLock(options, app);
1313
+ // 初始化调度任务系统
1314
+ await initSchedule(options, app);
1315
+ });
965
1316
  }
966
1317
 
967
- export { KoattyScheduled, RedLock, Scheduled, SchedulerLock };
1318
+ export { KoattyScheduled, RedLock, RedLocker, RedisClientAdapter, RedisFactory, RedisMode, Scheduled, SchedulerLock };