koatty_schedule 3.3.0 → 3.3.2
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/CHANGELOG.md +9 -0
- package/README.md +3 -1
- package/dist/README.md +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +202 -235
- package/dist/index.mjs +202 -235
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [3.3.2](https://github.com/thinkkoa/koatty_schedule/compare/v3.3.1...v3.3.2) (2025-06-22)
|
|
6
|
+
|
|
7
|
+
### [3.3.1](https://github.com/thinkkoa/koatty_schedule/compare/v3.3.0...v3.3.1) (2025-06-22)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* unify component type constant to 'COMPONENT' string literal in IOCContainer registration ([7098c7e](https://github.com/thinkkoa/koatty_schedule/commit/7098c7e2c326a6461b6b8b5c84d07a3ced75de5f))
|
|
13
|
+
|
|
5
14
|
## [3.3.0](https://github.com/thinkkoa/koatty_schedule/compare/v3.2.0...v3.3.0) (2025-06-22)
|
|
6
15
|
|
|
7
16
|
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Powerful scheduled tasks and distributed locking solution for Koatty framework.
|
|
|
16
16
|
- 🌍 **Timezone Smart**: Three-tier priority system for timezone configuration
|
|
17
17
|
- 📊 **Health Monitoring**: Built-in health checks and detailed status reporting
|
|
18
18
|
- 🔧 **Easy Configuration**: Method-level and global configuration options
|
|
19
|
+
- 🚀 **Smart Initialization**: Unified initialization timing for optimal dependency resolution
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -391,7 +392,8 @@ DEBUG=koatty_schedule* npm start
|
|
|
391
392
|
|
|
392
393
|
#### `@Scheduled(cron: string, timezone?: string)`
|
|
393
394
|
- `cron`: Cron expression (6-part format with seconds)
|
|
394
|
-
- `timezone`: Optional timezone override
|
|
395
|
+
- `timezone`: Optional timezone override (defaults to 'Asia/Beijing')
|
|
396
|
+
- **Processing**: Records metadata in IOC container, CronJob created at `appReady`
|
|
395
397
|
|
|
396
398
|
#### `@RedLock(lockName?: string, options?: RedLockMethodOptions)`
|
|
397
399
|
- `lockName`: Unique lock identifier (auto-generated if not provided)
|
package/dist/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Powerful scheduled tasks and distributed locking solution for Koatty framework.
|
|
|
16
16
|
- 🌍 **Timezone Smart**: Three-tier priority system for timezone configuration
|
|
17
17
|
- 📊 **Health Monitoring**: Built-in health checks and detailed status reporting
|
|
18
18
|
- 🔧 **Easy Configuration**: Method-level and global configuration options
|
|
19
|
+
- 🚀 **Smart Initialization**: Unified initialization timing for optimal dependency resolution
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -391,7 +392,8 @@ DEBUG=koatty_schedule* npm start
|
|
|
391
392
|
|
|
392
393
|
#### `@Scheduled(cron: string, timezone?: string)`
|
|
393
394
|
- `cron`: Cron expression (6-part format with seconds)
|
|
394
|
-
- `timezone`: Optional timezone override
|
|
395
|
+
- `timezone`: Optional timezone override (defaults to 'Asia/Beijing')
|
|
396
|
+
- **Processing**: Records metadata in IOC container, CronJob created at `appReady`
|
|
395
397
|
|
|
396
398
|
#### `@RedLock(lockName?: string, options?: RedLockMethodOptions)`
|
|
397
399
|
- `lockName`: Unique lock identifier (auto-generated if not provided)
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* @Author: richen
|
|
3
|
-
* @Date: 2025-06-
|
|
3
|
+
* @Date: 2025-06-23 00:55:12
|
|
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
12
|
var ioredis = require('ioredis');
|
|
14
13
|
var koatty_logger = require('koatty_logger');
|
|
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
|
*/
|
|
@@ -110,13 +109,6 @@ function validateRedLockMethodOptions(options) {
|
|
|
110
109
|
* Global configuration storage
|
|
111
110
|
*/
|
|
112
111
|
let globalScheduledOptions = {};
|
|
113
|
-
/**
|
|
114
|
-
* Set global scheduled options
|
|
115
|
-
* @param options - Global scheduled options
|
|
116
|
-
*/
|
|
117
|
-
function setGlobalScheduledOptions(options) {
|
|
118
|
-
globalScheduledOptions = { ...options };
|
|
119
|
-
}
|
|
120
112
|
/**
|
|
121
113
|
* Get global scheduled options
|
|
122
114
|
* @returns Global scheduled options
|
|
@@ -129,8 +121,8 @@ function getGlobalScheduledOptions() {
|
|
|
129
121
|
* @param userTimezone - User specified timezone
|
|
130
122
|
* @returns Effective timezone
|
|
131
123
|
*/
|
|
132
|
-
function getEffectiveTimezone(userTimezone) {
|
|
133
|
-
return userTimezone ||
|
|
124
|
+
function getEffectiveTimezone(options, userTimezone) {
|
|
125
|
+
return userTimezone || options.timezone || 'Asia/Beijing';
|
|
134
126
|
}
|
|
135
127
|
/**
|
|
136
128
|
* Get effective RedLock method options with priority: method options > global options > defaults
|
|
@@ -147,150 +139,6 @@ function getEffectiveRedLockOptions(methodOptions) {
|
|
|
147
139
|
};
|
|
148
140
|
}
|
|
149
141
|
|
|
150
|
-
/*
|
|
151
|
-
* @Description:
|
|
152
|
-
* @Usage:
|
|
153
|
-
* @Author: richen
|
|
154
|
-
* @Date: 2025-06-09 16:00:00
|
|
155
|
-
* @LastEditTime: 2025-06-09 16:00:00
|
|
156
|
-
* @License: BSD (3-Clause)
|
|
157
|
-
* @Copyright (c): <richenlin(at)gmail.com>
|
|
158
|
-
*/
|
|
159
|
-
/**
|
|
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
|
-
* ```
|
|
185
|
-
*/
|
|
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(componentType, 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
|
-
}
|
|
223
|
-
|
|
224
|
-
/*
|
|
225
|
-
* @Description:
|
|
226
|
-
* @Usage:
|
|
227
|
-
* @Author: richen
|
|
228
|
-
* @Date: 2025-06-09 16:00:00
|
|
229
|
-
* @LastEditTime: 2025-06-09 16:00:00
|
|
230
|
-
* @License: BSD (3-Clause)
|
|
231
|
-
* @Copyright (c): <richenlin(at)gmail.com>
|
|
232
|
-
*/
|
|
233
|
-
/**
|
|
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
|
|
250
|
-
*/
|
|
251
|
-
function Scheduled(cron, timezone) {
|
|
252
|
-
// 参数验证
|
|
253
|
-
if (koatty_lib.Helper.isEmpty(cron)) {
|
|
254
|
-
throw Error("Cron expression is required and cannot be empty");
|
|
255
|
-
}
|
|
256
|
-
// 验证cron表达式格式
|
|
257
|
-
try {
|
|
258
|
-
validateCronExpression(cron);
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
throw Error(`Invalid cron expression: ${error.message}`);
|
|
262
|
-
}
|
|
263
|
-
// 验证时区
|
|
264
|
-
if (timezone && typeof timezone !== 'string') {
|
|
265
|
-
throw Error("Timezone must be a string");
|
|
266
|
-
}
|
|
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.");
|
|
273
|
-
}
|
|
274
|
-
// 验证方法名
|
|
275
|
-
const methodName = propertyKey.toString();
|
|
276
|
-
if (!methodName || typeof methodName !== 'string') {
|
|
277
|
-
throw Error("Method name is required for @Scheduled decorator");
|
|
278
|
-
}
|
|
279
|
-
// 验证方法描述符
|
|
280
|
-
if (!descriptor || typeof descriptor.value !== 'function') {
|
|
281
|
-
throw Error("@Scheduled decorator can only be applied to methods");
|
|
282
|
-
}
|
|
283
|
-
// 保存类到IOC容器
|
|
284
|
-
koatty_container.IOCContainer.saveClass(componentType, 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
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
142
|
/*
|
|
295
143
|
* @Description: RedLock utility for distributed locks
|
|
296
144
|
* @Usage:
|
|
@@ -688,6 +536,8 @@ function timeoutPromise(ms) {
|
|
|
688
536
|
/**
|
|
689
537
|
* Initiation schedule locker client.
|
|
690
538
|
*
|
|
539
|
+
* @param {RedLockOptions} options - RedLock 配置选项
|
|
540
|
+
* @param {Koatty} app - Koatty 应用实例
|
|
691
541
|
* @returns {Promise<void>}
|
|
692
542
|
*/
|
|
693
543
|
async function initRedLock(options, app) {
|
|
@@ -695,11 +545,12 @@ async function initRedLock(options, app) {
|
|
|
695
545
|
koatty_logger.DefaultLogger.Warn(`RedLock initialization skipped: Koatty app not available or not initialized`);
|
|
696
546
|
return;
|
|
697
547
|
}
|
|
698
|
-
app.once("
|
|
548
|
+
app.once("appReady", async function () {
|
|
699
549
|
try {
|
|
700
550
|
if (koatty_lib.Helper.isEmpty(options)) {
|
|
701
551
|
throw Error(`Missing RedLock configuration. Please write a configuration item with the key name 'RedLock' in the db.ts file.`);
|
|
702
552
|
}
|
|
553
|
+
// 获取RedLocker实例,在首次使用时自动初始化
|
|
703
554
|
const redLocker = RedLocker.getInstance(options);
|
|
704
555
|
await redLocker.initialize();
|
|
705
556
|
koatty_logger.DefaultLogger.Info('RedLock initialized successfully');
|
|
@@ -710,72 +561,12 @@ async function initRedLock(options, app) {
|
|
|
710
561
|
}
|
|
711
562
|
});
|
|
712
563
|
}
|
|
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_REDLOCK);
|
|
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
564
|
/**
|
|
774
565
|
* Create redLocker Descriptor with improved error handling and type safety
|
|
775
566
|
* @param descriptor - Property descriptor
|
|
776
567
|
* @param name - Lock name
|
|
777
568
|
* @param method - Method name
|
|
778
|
-
* @param
|
|
569
|
+
* @param methodOptions - Method-level RedLock options
|
|
779
570
|
* @returns Enhanced property descriptor
|
|
780
571
|
*/
|
|
781
572
|
function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
@@ -794,13 +585,6 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
794
585
|
if (typeof value !== 'function') {
|
|
795
586
|
throw new Error('Descriptor value must be a function');
|
|
796
587
|
}
|
|
797
|
-
// 设置默认选项,合并方法级别的选项
|
|
798
|
-
const lockOptions = {
|
|
799
|
-
lockTimeOut: methodOptions?.lockTimeOut,
|
|
800
|
-
clockDriftFactor: methodOptions?.clockDriftFactor,
|
|
801
|
-
maxRetries: methodOptions?.maxRetries,
|
|
802
|
-
retryDelayMs: methodOptions?.retryDelayMs
|
|
803
|
-
};
|
|
804
588
|
/**
|
|
805
589
|
* Enhanced function wrapper with proper lock renewal and safety
|
|
806
590
|
*/
|
|
@@ -864,6 +648,7 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
864
648
|
async value(...props) {
|
|
865
649
|
try {
|
|
866
650
|
const redlock = RedLocker.getInstance();
|
|
651
|
+
const lockOptions = getEffectiveRedLockOptions(methodOptions);
|
|
867
652
|
// Acquire a lock.
|
|
868
653
|
const lockTime = lockOptions.lockTimeOut || 10000;
|
|
869
654
|
if (lockTime <= 200) {
|
|
@@ -881,6 +666,169 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
881
666
|
},
|
|
882
667
|
};
|
|
883
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* Generate lock name for RedLock decorator
|
|
671
|
+
*/
|
|
672
|
+
function generateLockName(configName, methodName, target) {
|
|
673
|
+
if (configName) {
|
|
674
|
+
return configName;
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
const targetObj = target;
|
|
678
|
+
const identifier = koatty_container.IOCContainer.getIdentifier(targetObj);
|
|
679
|
+
if (identifier) {
|
|
680
|
+
return `${identifier}_${methodName}`;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
// Fallback if IOC container is not available
|
|
685
|
+
}
|
|
686
|
+
const targetWithConstructor = target;
|
|
687
|
+
const className = targetWithConstructor.constructor?.name || 'Unknown';
|
|
688
|
+
return `${className}_${methodName}`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/*
|
|
692
|
+
* @Description:
|
|
693
|
+
* @Usage:
|
|
694
|
+
* @Author: richen
|
|
695
|
+
* @Date: 2025-06-09 16:00:00
|
|
696
|
+
* @LastEditTime: 2025-06-09 16:00:00
|
|
697
|
+
* @License: BSD (3-Clause)
|
|
698
|
+
* @Copyright (c): <richenlin(at)gmail.com>
|
|
699
|
+
*/
|
|
700
|
+
/**
|
|
701
|
+
* Redis-based distributed lock decorator
|
|
702
|
+
*
|
|
703
|
+
* @export
|
|
704
|
+
* @param {string} [name] - The locker name. If name is duplicated, lock sharing contention will result.
|
|
705
|
+
* If not provided, a unique name will be auto-generated using method name + random suffix.
|
|
706
|
+
* IMPORTANT: Auto-generated names are unique per method deployment and not predictable.
|
|
707
|
+
* @param {RedLockMethodOptions} [options] - Lock configuration options for this method
|
|
708
|
+
*
|
|
709
|
+
* @returns {MethodDecorator}
|
|
710
|
+
* @throws {Error} When decorator is used on wrong class type or invalid configuration
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* ```typescript
|
|
714
|
+
* class UserService {
|
|
715
|
+
* @RedLock('user_update_lock', { lockTimeOut: 5000, maxRetries: 2 })
|
|
716
|
+
* async updateUser(id: string, data: any) {
|
|
717
|
+
* // This method will be protected by a distributed lock with predictable name
|
|
718
|
+
* }
|
|
719
|
+
*
|
|
720
|
+
* @RedLock() // Auto-generated unique name like "deleteUser_abc123_xyz789"
|
|
721
|
+
* async deleteUser(id: string) {
|
|
722
|
+
* // This method will be protected by a distributed lock with auto-generated unique name
|
|
723
|
+
* }
|
|
724
|
+
* }
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
function RedLock(lockName, options) {
|
|
728
|
+
return (target, propertyKey, descriptor) => {
|
|
729
|
+
const methodName = propertyKey.toString();
|
|
730
|
+
// 验证装饰器使用的类型(从原型对象获取类构造函数)
|
|
731
|
+
const targetClass = target.constructor;
|
|
732
|
+
const componentType = koatty_container.IOCContainer.getType(targetClass);
|
|
733
|
+
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
|
|
734
|
+
throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes.");
|
|
735
|
+
}
|
|
736
|
+
// 验证方法名
|
|
737
|
+
if (!methodName || typeof methodName !== 'string') {
|
|
738
|
+
throw Error("Method name is required for @RedLock decorator");
|
|
739
|
+
}
|
|
740
|
+
// 验证方法描述符
|
|
741
|
+
if (!descriptor || typeof descriptor.value !== 'function') {
|
|
742
|
+
throw Error("@RedLock decorator can only be applied to methods");
|
|
743
|
+
}
|
|
744
|
+
// 生成锁名称:用户指定的 > 基于类名和方法名生成
|
|
745
|
+
const finalLockName = lockName || generateLockName(lockName, methodName, target);
|
|
746
|
+
// 验证选项
|
|
747
|
+
if (options) {
|
|
748
|
+
validateRedLockMethodOptions(options);
|
|
749
|
+
}
|
|
750
|
+
// 保存类到IOC容器
|
|
751
|
+
koatty_container.IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
|
|
752
|
+
try {
|
|
753
|
+
// 直接在装饰器中包装方法,而不是延迟处理
|
|
754
|
+
const enhancedDescriptor = redLockerDescriptor(descriptor, finalLockName, methodName, options);
|
|
755
|
+
return enhancedDescriptor;
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
throw new Error(`Failed to apply RedLock to ${methodName}: ${error.message}`);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/*
|
|
764
|
+
* @Description:
|
|
765
|
+
* @Usage:
|
|
766
|
+
* @Author: richen
|
|
767
|
+
* @Date: 2025-06-09 16:00:00
|
|
768
|
+
* @LastEditTime: 2025-06-09 16:00:00
|
|
769
|
+
* @License: BSD (3-Clause)
|
|
770
|
+
* @Copyright (c): <richenlin(at)gmail.com>
|
|
771
|
+
*/
|
|
772
|
+
/**
|
|
773
|
+
* Schedule task decorator with optimized preprocessing
|
|
774
|
+
*
|
|
775
|
+
* @export
|
|
776
|
+
* @param {string} cron - Cron expression for task scheduling
|
|
777
|
+
* @param {string} [timezone='Asia/Beijing'] - Timezone for the schedule
|
|
778
|
+
*
|
|
779
|
+
* Cron expression format:
|
|
780
|
+
* * Seconds: 0-59
|
|
781
|
+
* * Minutes: 0-59
|
|
782
|
+
* * Hours: 0-23
|
|
783
|
+
* * Day of Month: 1-31
|
|
784
|
+
* * Months: 1-12 (Jan-Dec)
|
|
785
|
+
* * Day of Week: 1-7 (Sun-Sat)
|
|
786
|
+
*
|
|
787
|
+
* @returns {MethodDecorator}
|
|
788
|
+
* @throws {Error} When cron expression is invalid or decorator is used on wrong class type
|
|
789
|
+
*/
|
|
790
|
+
function Scheduled(cron, timezone = 'Asia/Beijing') {
|
|
791
|
+
// 参数验证
|
|
792
|
+
if (koatty_lib.Helper.isEmpty(cron)) {
|
|
793
|
+
throw Error("Cron expression is required and cannot be empty");
|
|
794
|
+
}
|
|
795
|
+
// 验证cron表达式格式
|
|
796
|
+
try {
|
|
797
|
+
validateCronExpression(cron);
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
throw Error(`Invalid cron expression: ${error.message}`);
|
|
801
|
+
}
|
|
802
|
+
// 验证时区
|
|
803
|
+
if (timezone && typeof timezone !== 'string') {
|
|
804
|
+
throw Error("Timezone must be a string");
|
|
805
|
+
}
|
|
806
|
+
return (target, propertyKey, descriptor) => {
|
|
807
|
+
// 验证装饰器使用的类型(从原型对象获取类构造函数)
|
|
808
|
+
const targetClass = target.constructor;
|
|
809
|
+
const componentType = koatty_container.IOCContainer.getType(targetClass);
|
|
810
|
+
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
|
|
811
|
+
throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes.");
|
|
812
|
+
}
|
|
813
|
+
// 验证方法名
|
|
814
|
+
const methodName = propertyKey.toString();
|
|
815
|
+
if (!methodName || typeof methodName !== 'string') {
|
|
816
|
+
throw Error("Method name is required for @Scheduled decorator");
|
|
817
|
+
}
|
|
818
|
+
// 验证方法描述符
|
|
819
|
+
if (!descriptor || typeof descriptor.value !== 'function') {
|
|
820
|
+
throw Error("@Scheduled decorator can only be applied to methods");
|
|
821
|
+
}
|
|
822
|
+
// 保存类到IOC容器
|
|
823
|
+
koatty_container.IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
|
|
824
|
+
// 保存调度元数据到 IOC 容器
|
|
825
|
+
koatty_container.IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
|
|
826
|
+
method: methodName,
|
|
827
|
+
cron,
|
|
828
|
+
timezone // 保存确定的时区值
|
|
829
|
+
}, target, methodName);
|
|
830
|
+
};
|
|
831
|
+
}
|
|
884
832
|
|
|
885
833
|
/**
|
|
886
834
|
* @ author: richen
|
|
@@ -888,6 +836,29 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
888
836
|
* @ license: MIT
|
|
889
837
|
* @ version: 2020-07-06 10:29:20
|
|
890
838
|
*/
|
|
839
|
+
/**
|
|
840
|
+
* 初始化调度任务系统
|
|
841
|
+
* 在appReady时触发批量注入调度任务,确保所有初始化工作完成
|
|
842
|
+
*
|
|
843
|
+
* @param {Koatty} app - Koatty 应用实例
|
|
844
|
+
* @param {any} options - 调度任务配置
|
|
845
|
+
*/
|
|
846
|
+
async function initSchedule(options, app) {
|
|
847
|
+
if (!app || !koatty_lib.Helper.isFunction(app.once)) {
|
|
848
|
+
koatty_logger.DefaultLogger.Warn(`Schedule initialization skipped: Koatty app not available or not initialized`);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
app.once("appReady", async function () {
|
|
852
|
+
try {
|
|
853
|
+
await injectSchedule(options);
|
|
854
|
+
koatty_logger.DefaultLogger.Info('Schedule system initialized successfully');
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
koatty_logger.DefaultLogger.Error('Failed to initialize Schedule system:', error);
|
|
858
|
+
throw error;
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
891
862
|
/**
|
|
892
863
|
* Inject schedule job with enhanced error handling and validation
|
|
893
864
|
*
|
|
@@ -898,14 +869,11 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
898
869
|
*/
|
|
899
870
|
/**
|
|
900
871
|
* 批量注入调度任务 - 从IOC容器读取类元数据并创建所有CronJob
|
|
901
|
-
*
|
|
902
|
-
* @param {RedLockOptions} options - RedLock 配置选项
|
|
903
|
-
* @param {Koatty} app - Koatty 应用实例
|
|
904
872
|
*/
|
|
905
|
-
async function injectSchedule(
|
|
873
|
+
async function injectSchedule(options) {
|
|
906
874
|
try {
|
|
907
875
|
koatty_logger.DefaultLogger.Debug('Starting batch schedule injection...');
|
|
908
|
-
const componentList = koatty_container.IOCContainer.listClass(
|
|
876
|
+
const componentList = koatty_container.IOCContainer.listClass("COMPONENT");
|
|
909
877
|
for (const component of componentList) {
|
|
910
878
|
const classMetadata = koatty_container.IOCContainer.getClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, component.target);
|
|
911
879
|
if (!classMetadata) {
|
|
@@ -928,7 +896,7 @@ async function injectSchedule(_options, _app) {
|
|
|
928
896
|
continue;
|
|
929
897
|
}
|
|
930
898
|
const taskName = `${className}_${scheduleData.method}`;
|
|
931
|
-
const tz = getEffectiveTimezone(scheduleData.timezone);
|
|
899
|
+
const tz = getEffectiveTimezone(options, scheduleData.timezone);
|
|
932
900
|
new cron.CronJob(scheduleData.cron, () => {
|
|
933
901
|
koatty_logger.DefaultLogger.Debug(`The schedule job ${taskName} started.`);
|
|
934
902
|
Promise.resolve(targetMethod.call(instance))
|
|
@@ -992,11 +960,10 @@ const defaultOptions = {
|
|
|
992
960
|
*/
|
|
993
961
|
async function KoattyScheduled(options, app) {
|
|
994
962
|
options = { ...defaultOptions, ...options };
|
|
995
|
-
//
|
|
996
|
-
setGlobalScheduledOptions(options);
|
|
963
|
+
// 初始化RedLock(appReady时触发,确保所有依赖就绪)
|
|
997
964
|
await initRedLock(options, app);
|
|
998
|
-
|
|
999
|
-
await
|
|
965
|
+
// 初始化调度任务系统(appReady时触发,确保所有组件都已初始化)
|
|
966
|
+
await initSchedule(options, app);
|
|
1000
967
|
}
|
|
1001
968
|
|
|
1002
969
|
exports.KoattyScheduled = KoattyScheduled;
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* @Author: richen
|
|
3
|
-
* @Date: 2025-06-
|
|
3
|
+
* @Date: 2025-06-23 00:55:12
|
|
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
10
|
import { Redis } from 'ioredis';
|
|
12
11
|
import { DefaultLogger } from 'koatty_logger';
|
|
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
|
*/
|
|
@@ -108,13 +107,6 @@ function validateRedLockMethodOptions(options) {
|
|
|
108
107
|
* Global configuration storage
|
|
109
108
|
*/
|
|
110
109
|
let globalScheduledOptions = {};
|
|
111
|
-
/**
|
|
112
|
-
* Set global scheduled options
|
|
113
|
-
* @param options - Global scheduled options
|
|
114
|
-
*/
|
|
115
|
-
function setGlobalScheduledOptions(options) {
|
|
116
|
-
globalScheduledOptions = { ...options };
|
|
117
|
-
}
|
|
118
110
|
/**
|
|
119
111
|
* Get global scheduled options
|
|
120
112
|
* @returns Global scheduled options
|
|
@@ -127,8 +119,8 @@ function getGlobalScheduledOptions() {
|
|
|
127
119
|
* @param userTimezone - User specified timezone
|
|
128
120
|
* @returns Effective timezone
|
|
129
121
|
*/
|
|
130
|
-
function getEffectiveTimezone(userTimezone) {
|
|
131
|
-
return userTimezone ||
|
|
122
|
+
function getEffectiveTimezone(options, userTimezone) {
|
|
123
|
+
return userTimezone || options.timezone || 'Asia/Beijing';
|
|
132
124
|
}
|
|
133
125
|
/**
|
|
134
126
|
* Get effective RedLock method options with priority: method options > global options > defaults
|
|
@@ -145,150 +137,6 @@ function getEffectiveRedLockOptions(methodOptions) {
|
|
|
145
137
|
};
|
|
146
138
|
}
|
|
147
139
|
|
|
148
|
-
/*
|
|
149
|
-
* @Description:
|
|
150
|
-
* @Usage:
|
|
151
|
-
* @Author: richen
|
|
152
|
-
* @Date: 2025-06-09 16:00:00
|
|
153
|
-
* @LastEditTime: 2025-06-09 16:00:00
|
|
154
|
-
* @License: BSD (3-Clause)
|
|
155
|
-
* @Copyright (c): <richenlin(at)gmail.com>
|
|
156
|
-
*/
|
|
157
|
-
/**
|
|
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
|
-
* ```
|
|
183
|
-
*/
|
|
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(componentType, 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
|
-
}
|
|
221
|
-
|
|
222
|
-
/*
|
|
223
|
-
* @Description:
|
|
224
|
-
* @Usage:
|
|
225
|
-
* @Author: richen
|
|
226
|
-
* @Date: 2025-06-09 16:00:00
|
|
227
|
-
* @LastEditTime: 2025-06-09 16:00:00
|
|
228
|
-
* @License: BSD (3-Clause)
|
|
229
|
-
* @Copyright (c): <richenlin(at)gmail.com>
|
|
230
|
-
*/
|
|
231
|
-
/**
|
|
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
|
|
248
|
-
*/
|
|
249
|
-
function Scheduled(cron, timezone) {
|
|
250
|
-
// 参数验证
|
|
251
|
-
if (Helper.isEmpty(cron)) {
|
|
252
|
-
throw Error("Cron expression is required and cannot be empty");
|
|
253
|
-
}
|
|
254
|
-
// 验证cron表达式格式
|
|
255
|
-
try {
|
|
256
|
-
validateCronExpression(cron);
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
throw Error(`Invalid cron expression: ${error.message}`);
|
|
260
|
-
}
|
|
261
|
-
// 验证时区
|
|
262
|
-
if (timezone && typeof timezone !== 'string') {
|
|
263
|
-
throw Error("Timezone must be a string");
|
|
264
|
-
}
|
|
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.");
|
|
271
|
-
}
|
|
272
|
-
// 验证方法名
|
|
273
|
-
const methodName = propertyKey.toString();
|
|
274
|
-
if (!methodName || typeof methodName !== 'string') {
|
|
275
|
-
throw Error("Method name is required for @Scheduled decorator");
|
|
276
|
-
}
|
|
277
|
-
// 验证方法描述符
|
|
278
|
-
if (!descriptor || typeof descriptor.value !== 'function') {
|
|
279
|
-
throw Error("@Scheduled decorator can only be applied to methods");
|
|
280
|
-
}
|
|
281
|
-
// 保存类到IOC容器
|
|
282
|
-
IOCContainer.saveClass(componentType, targetClass, targetClass.name);
|
|
283
|
-
// 保存调度元数据到 IOC 容器
|
|
284
|
-
IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
|
|
285
|
-
method: methodName,
|
|
286
|
-
cron,
|
|
287
|
-
timezone // 保存用户指定的值,可能为undefined
|
|
288
|
-
}, target, methodName);
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
140
|
/*
|
|
293
141
|
* @Description: RedLock utility for distributed locks
|
|
294
142
|
* @Usage:
|
|
@@ -686,6 +534,8 @@ function timeoutPromise(ms) {
|
|
|
686
534
|
/**
|
|
687
535
|
* Initiation schedule locker client.
|
|
688
536
|
*
|
|
537
|
+
* @param {RedLockOptions} options - RedLock 配置选项
|
|
538
|
+
* @param {Koatty} app - Koatty 应用实例
|
|
689
539
|
* @returns {Promise<void>}
|
|
690
540
|
*/
|
|
691
541
|
async function initRedLock(options, app) {
|
|
@@ -693,11 +543,12 @@ async function initRedLock(options, app) {
|
|
|
693
543
|
DefaultLogger.Warn(`RedLock initialization skipped: Koatty app not available or not initialized`);
|
|
694
544
|
return;
|
|
695
545
|
}
|
|
696
|
-
app.once("
|
|
546
|
+
app.once("appReady", async function () {
|
|
697
547
|
try {
|
|
698
548
|
if (Helper.isEmpty(options)) {
|
|
699
549
|
throw Error(`Missing RedLock configuration. Please write a configuration item with the key name 'RedLock' in the db.ts file.`);
|
|
700
550
|
}
|
|
551
|
+
// 获取RedLocker实例,在首次使用时自动初始化
|
|
701
552
|
const redLocker = RedLocker.getInstance(options);
|
|
702
553
|
await redLocker.initialize();
|
|
703
554
|
DefaultLogger.Info('RedLock initialized successfully');
|
|
@@ -708,72 +559,12 @@ async function initRedLock(options, app) {
|
|
|
708
559
|
}
|
|
709
560
|
});
|
|
710
561
|
}
|
|
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_REDLOCK);
|
|
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
562
|
/**
|
|
772
563
|
* Create redLocker Descriptor with improved error handling and type safety
|
|
773
564
|
* @param descriptor - Property descriptor
|
|
774
565
|
* @param name - Lock name
|
|
775
566
|
* @param method - Method name
|
|
776
|
-
* @param
|
|
567
|
+
* @param methodOptions - Method-level RedLock options
|
|
777
568
|
* @returns Enhanced property descriptor
|
|
778
569
|
*/
|
|
779
570
|
function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
@@ -792,13 +583,6 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
792
583
|
if (typeof value !== 'function') {
|
|
793
584
|
throw new Error('Descriptor value must be a function');
|
|
794
585
|
}
|
|
795
|
-
// 设置默认选项,合并方法级别的选项
|
|
796
|
-
const lockOptions = {
|
|
797
|
-
lockTimeOut: methodOptions?.lockTimeOut,
|
|
798
|
-
clockDriftFactor: methodOptions?.clockDriftFactor,
|
|
799
|
-
maxRetries: methodOptions?.maxRetries,
|
|
800
|
-
retryDelayMs: methodOptions?.retryDelayMs
|
|
801
|
-
};
|
|
802
586
|
/**
|
|
803
587
|
* Enhanced function wrapper with proper lock renewal and safety
|
|
804
588
|
*/
|
|
@@ -862,6 +646,7 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
862
646
|
async value(...props) {
|
|
863
647
|
try {
|
|
864
648
|
const redlock = RedLocker.getInstance();
|
|
649
|
+
const lockOptions = getEffectiveRedLockOptions(methodOptions);
|
|
865
650
|
// Acquire a lock.
|
|
866
651
|
const lockTime = lockOptions.lockTimeOut || 10000;
|
|
867
652
|
if (lockTime <= 200) {
|
|
@@ -879,6 +664,169 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
879
664
|
},
|
|
880
665
|
};
|
|
881
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* Generate lock name for RedLock decorator
|
|
669
|
+
*/
|
|
670
|
+
function generateLockName(configName, methodName, target) {
|
|
671
|
+
if (configName) {
|
|
672
|
+
return configName;
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
const targetObj = target;
|
|
676
|
+
const identifier = IOCContainer.getIdentifier(targetObj);
|
|
677
|
+
if (identifier) {
|
|
678
|
+
return `${identifier}_${methodName}`;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
// Fallback if IOC container is not available
|
|
683
|
+
}
|
|
684
|
+
const targetWithConstructor = target;
|
|
685
|
+
const className = targetWithConstructor.constructor?.name || 'Unknown';
|
|
686
|
+
return `${className}_${methodName}`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/*
|
|
690
|
+
* @Description:
|
|
691
|
+
* @Usage:
|
|
692
|
+
* @Author: richen
|
|
693
|
+
* @Date: 2025-06-09 16:00:00
|
|
694
|
+
* @LastEditTime: 2025-06-09 16:00:00
|
|
695
|
+
* @License: BSD (3-Clause)
|
|
696
|
+
* @Copyright (c): <richenlin(at)gmail.com>
|
|
697
|
+
*/
|
|
698
|
+
/**
|
|
699
|
+
* Redis-based distributed lock decorator
|
|
700
|
+
*
|
|
701
|
+
* @export
|
|
702
|
+
* @param {string} [name] - The locker name. If name is duplicated, lock sharing contention will result.
|
|
703
|
+
* If not provided, a unique name will be auto-generated using method name + random suffix.
|
|
704
|
+
* IMPORTANT: Auto-generated names are unique per method deployment and not predictable.
|
|
705
|
+
* @param {RedLockMethodOptions} [options] - Lock configuration options for this method
|
|
706
|
+
*
|
|
707
|
+
* @returns {MethodDecorator}
|
|
708
|
+
* @throws {Error} When decorator is used on wrong class type or invalid configuration
|
|
709
|
+
*
|
|
710
|
+
* @example
|
|
711
|
+
* ```typescript
|
|
712
|
+
* class UserService {
|
|
713
|
+
* @RedLock('user_update_lock', { lockTimeOut: 5000, maxRetries: 2 })
|
|
714
|
+
* async updateUser(id: string, data: any) {
|
|
715
|
+
* // This method will be protected by a distributed lock with predictable name
|
|
716
|
+
* }
|
|
717
|
+
*
|
|
718
|
+
* @RedLock() // Auto-generated unique name like "deleteUser_abc123_xyz789"
|
|
719
|
+
* async deleteUser(id: string) {
|
|
720
|
+
* // This method will be protected by a distributed lock with auto-generated unique name
|
|
721
|
+
* }
|
|
722
|
+
* }
|
|
723
|
+
* ```
|
|
724
|
+
*/
|
|
725
|
+
function RedLock(lockName, options) {
|
|
726
|
+
return (target, propertyKey, descriptor) => {
|
|
727
|
+
const methodName = propertyKey.toString();
|
|
728
|
+
// 验证装饰器使用的类型(从原型对象获取类构造函数)
|
|
729
|
+
const targetClass = target.constructor;
|
|
730
|
+
const componentType = IOCContainer.getType(targetClass);
|
|
731
|
+
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
|
|
732
|
+
throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes.");
|
|
733
|
+
}
|
|
734
|
+
// 验证方法名
|
|
735
|
+
if (!methodName || typeof methodName !== 'string') {
|
|
736
|
+
throw Error("Method name is required for @RedLock decorator");
|
|
737
|
+
}
|
|
738
|
+
// 验证方法描述符
|
|
739
|
+
if (!descriptor || typeof descriptor.value !== 'function') {
|
|
740
|
+
throw Error("@RedLock decorator can only be applied to methods");
|
|
741
|
+
}
|
|
742
|
+
// 生成锁名称:用户指定的 > 基于类名和方法名生成
|
|
743
|
+
const finalLockName = lockName || generateLockName(lockName, methodName, target);
|
|
744
|
+
// 验证选项
|
|
745
|
+
if (options) {
|
|
746
|
+
validateRedLockMethodOptions(options);
|
|
747
|
+
}
|
|
748
|
+
// 保存类到IOC容器
|
|
749
|
+
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
|
|
750
|
+
try {
|
|
751
|
+
// 直接在装饰器中包装方法,而不是延迟处理
|
|
752
|
+
const enhancedDescriptor = redLockerDescriptor(descriptor, finalLockName, methodName, options);
|
|
753
|
+
return enhancedDescriptor;
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
throw new Error(`Failed to apply RedLock to ${methodName}: ${error.message}`);
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/*
|
|
762
|
+
* @Description:
|
|
763
|
+
* @Usage:
|
|
764
|
+
* @Author: richen
|
|
765
|
+
* @Date: 2025-06-09 16:00:00
|
|
766
|
+
* @LastEditTime: 2025-06-09 16:00:00
|
|
767
|
+
* @License: BSD (3-Clause)
|
|
768
|
+
* @Copyright (c): <richenlin(at)gmail.com>
|
|
769
|
+
*/
|
|
770
|
+
/**
|
|
771
|
+
* Schedule task decorator with optimized preprocessing
|
|
772
|
+
*
|
|
773
|
+
* @export
|
|
774
|
+
* @param {string} cron - Cron expression for task scheduling
|
|
775
|
+
* @param {string} [timezone='Asia/Beijing'] - Timezone for the schedule
|
|
776
|
+
*
|
|
777
|
+
* Cron expression format:
|
|
778
|
+
* * Seconds: 0-59
|
|
779
|
+
* * Minutes: 0-59
|
|
780
|
+
* * Hours: 0-23
|
|
781
|
+
* * Day of Month: 1-31
|
|
782
|
+
* * Months: 1-12 (Jan-Dec)
|
|
783
|
+
* * Day of Week: 1-7 (Sun-Sat)
|
|
784
|
+
*
|
|
785
|
+
* @returns {MethodDecorator}
|
|
786
|
+
* @throws {Error} When cron expression is invalid or decorator is used on wrong class type
|
|
787
|
+
*/
|
|
788
|
+
function Scheduled(cron, timezone = 'Asia/Beijing') {
|
|
789
|
+
// 参数验证
|
|
790
|
+
if (Helper.isEmpty(cron)) {
|
|
791
|
+
throw Error("Cron expression is required and cannot be empty");
|
|
792
|
+
}
|
|
793
|
+
// 验证cron表达式格式
|
|
794
|
+
try {
|
|
795
|
+
validateCronExpression(cron);
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
throw Error(`Invalid cron expression: ${error.message}`);
|
|
799
|
+
}
|
|
800
|
+
// 验证时区
|
|
801
|
+
if (timezone && typeof timezone !== 'string') {
|
|
802
|
+
throw Error("Timezone must be a string");
|
|
803
|
+
}
|
|
804
|
+
return (target, propertyKey, descriptor) => {
|
|
805
|
+
// 验证装饰器使用的类型(从原型对象获取类构造函数)
|
|
806
|
+
const targetClass = target.constructor;
|
|
807
|
+
const componentType = IOCContainer.getType(targetClass);
|
|
808
|
+
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
|
|
809
|
+
throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes.");
|
|
810
|
+
}
|
|
811
|
+
// 验证方法名
|
|
812
|
+
const methodName = propertyKey.toString();
|
|
813
|
+
if (!methodName || typeof methodName !== 'string') {
|
|
814
|
+
throw Error("Method name is required for @Scheduled decorator");
|
|
815
|
+
}
|
|
816
|
+
// 验证方法描述符
|
|
817
|
+
if (!descriptor || typeof descriptor.value !== 'function') {
|
|
818
|
+
throw Error("@Scheduled decorator can only be applied to methods");
|
|
819
|
+
}
|
|
820
|
+
// 保存类到IOC容器
|
|
821
|
+
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
|
|
822
|
+
// 保存调度元数据到 IOC 容器
|
|
823
|
+
IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
|
|
824
|
+
method: methodName,
|
|
825
|
+
cron,
|
|
826
|
+
timezone // 保存确定的时区值
|
|
827
|
+
}, target, methodName);
|
|
828
|
+
};
|
|
829
|
+
}
|
|
882
830
|
|
|
883
831
|
/**
|
|
884
832
|
* @ author: richen
|
|
@@ -886,6 +834,29 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
886
834
|
* @ license: MIT
|
|
887
835
|
* @ version: 2020-07-06 10:29:20
|
|
888
836
|
*/
|
|
837
|
+
/**
|
|
838
|
+
* 初始化调度任务系统
|
|
839
|
+
* 在appReady时触发批量注入调度任务,确保所有初始化工作完成
|
|
840
|
+
*
|
|
841
|
+
* @param {Koatty} app - Koatty 应用实例
|
|
842
|
+
* @param {any} options - 调度任务配置
|
|
843
|
+
*/
|
|
844
|
+
async function initSchedule(options, app) {
|
|
845
|
+
if (!app || !Helper.isFunction(app.once)) {
|
|
846
|
+
DefaultLogger.Warn(`Schedule initialization skipped: Koatty app not available or not initialized`);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
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
|
+
});
|
|
859
|
+
}
|
|
889
860
|
/**
|
|
890
861
|
* Inject schedule job with enhanced error handling and validation
|
|
891
862
|
*
|
|
@@ -896,14 +867,11 @@ function redLockerDescriptor(descriptor, name, method, methodOptions) {
|
|
|
896
867
|
*/
|
|
897
868
|
/**
|
|
898
869
|
* 批量注入调度任务 - 从IOC容器读取类元数据并创建所有CronJob
|
|
899
|
-
*
|
|
900
|
-
* @param {RedLockOptions} options - RedLock 配置选项
|
|
901
|
-
* @param {Koatty} app - Koatty 应用实例
|
|
902
870
|
*/
|
|
903
|
-
async function injectSchedule(
|
|
871
|
+
async function injectSchedule(options) {
|
|
904
872
|
try {
|
|
905
873
|
DefaultLogger.Debug('Starting batch schedule injection...');
|
|
906
|
-
const componentList = IOCContainer.listClass(
|
|
874
|
+
const componentList = IOCContainer.listClass("COMPONENT");
|
|
907
875
|
for (const component of componentList) {
|
|
908
876
|
const classMetadata = IOCContainer.getClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, component.target);
|
|
909
877
|
if (!classMetadata) {
|
|
@@ -926,7 +894,7 @@ async function injectSchedule(_options, _app) {
|
|
|
926
894
|
continue;
|
|
927
895
|
}
|
|
928
896
|
const taskName = `${className}_${scheduleData.method}`;
|
|
929
|
-
const tz = getEffectiveTimezone(scheduleData.timezone);
|
|
897
|
+
const tz = getEffectiveTimezone(options, scheduleData.timezone);
|
|
930
898
|
new CronJob(scheduleData.cron, () => {
|
|
931
899
|
DefaultLogger.Debug(`The schedule job ${taskName} started.`);
|
|
932
900
|
Promise.resolve(targetMethod.call(instance))
|
|
@@ -990,11 +958,10 @@ const defaultOptions = {
|
|
|
990
958
|
*/
|
|
991
959
|
async function KoattyScheduled(options, app) {
|
|
992
960
|
options = { ...defaultOptions, ...options };
|
|
993
|
-
//
|
|
994
|
-
setGlobalScheduledOptions(options);
|
|
961
|
+
// 初始化RedLock(appReady时触发,确保所有依赖就绪)
|
|
995
962
|
await initRedLock(options, app);
|
|
996
|
-
|
|
997
|
-
await
|
|
963
|
+
// 初始化调度任务系统(appReady时触发,确保所有组件都已初始化)
|
|
964
|
+
await initSchedule(options, app);
|
|
998
965
|
}
|
|
999
966
|
|
|
1000
967
|
export { KoattyScheduled, RedLock, Scheduled, SchedulerLock };
|
package/dist/package.json
CHANGED