koatty_schedule 4.0.7 → 4.1.0
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/.turbo/turbo-build.log +27 -18
- package/.turbo/turbo-clean.log +4 -0
- package/.turbo/turbo-lint.log +4 -0
- package/CHANGELOG.md +26 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +831 -656
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +828 -649
- package/dist/index.mjs.map +1 -1
- package/dist/package.json +11 -11
- package/package.json +11 -11
- package/tsconfig.tsbuildinfo +1 -0
package/dist/index.mjs
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
|
-
import { IOCContainer } from 'koatty_container';
|
|
2
|
-
import { Redlock } from '@sesamecare-oss/redlock';
|
|
3
|
-
import { DefaultLogger } from 'koatty_logger';
|
|
4
1
|
import Redis, { Cluster } from 'ioredis';
|
|
2
|
+
import { DefaultLogger } from 'koatty_logger';
|
|
3
|
+
import { Redlock } from '@sesamecare-oss/redlock';
|
|
4
|
+
import { IOCContainer } from 'koatty_container';
|
|
5
5
|
import { Helper } from 'koatty_lib';
|
|
6
6
|
import { CronJob } from 'cron';
|
|
7
7
|
|
|
8
8
|
/*!
|
|
9
9
|
* @Author: richen
|
|
10
|
-
* @Date: 2026-
|
|
10
|
+
* @Date: 2026-04-24 08:20:32
|
|
11
11
|
* @License: BSD (3-Clause)
|
|
12
12
|
* @Copyright (c) - <richenlin(at)gmail.com>
|
|
13
13
|
* @HomePage: https://koatty.org/
|
|
14
14
|
*/
|
|
15
15
|
var __defProp = Object.defineProperty;
|
|
16
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
16
17
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
18
|
+
var __esm = (fn, res) => function __init() {
|
|
19
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
20
|
+
};
|
|
21
|
+
var __export = (target, all) => {
|
|
22
|
+
for (var name in all)
|
|
23
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
24
|
+
};
|
|
17
25
|
|
|
18
26
|
// src/config/config.ts
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
var config_exports = {};
|
|
28
|
+
__export(config_exports, {
|
|
29
|
+
COMPONENT_REDLOCK: () => COMPONENT_REDLOCK,
|
|
30
|
+
COMPONENT_SCHEDULED: () => COMPONENT_SCHEDULED,
|
|
31
|
+
DecoratorType: () => DecoratorType,
|
|
32
|
+
getEffectiveRedLockOptions: () => getEffectiveRedLockOptions,
|
|
33
|
+
getEffectiveTimezone: () => getEffectiveTimezone,
|
|
34
|
+
getGlobalScheduledOptions: () => getGlobalScheduledOptions,
|
|
35
|
+
setGlobalScheduledOptions: () => setGlobalScheduledOptions,
|
|
36
|
+
validateCronExpression: () => validateCronExpression,
|
|
37
|
+
validateRedLockMethodOptions: () => validateRedLockMethodOptions,
|
|
38
|
+
validateRedLockOptions: () => validateRedLockOptions
|
|
39
|
+
});
|
|
25
40
|
function validateCronExpression(cron) {
|
|
26
41
|
if (!cron || typeof cron !== "string") {
|
|
27
|
-
throw new Error("Cron
|
|
42
|
+
throw new Error("Cron expression must be a non-empty string");
|
|
28
43
|
}
|
|
29
44
|
const cronParts = cron.trim().split(/\s+/);
|
|
30
45
|
if (cronParts.length < 5 || cronParts.length > 6) {
|
|
31
|
-
throw new Error(`
|
|
46
|
+
throw new Error(`Invalid cron format. Expected 5 or 6 parts, got ${cronParts.length}`);
|
|
32
47
|
}
|
|
33
48
|
const hasSecs = cronParts.length === 6;
|
|
34
49
|
const offset = hasSecs ? 0 : -1;
|
|
@@ -68,7 +83,6 @@ function validateCronExpression(cron) {
|
|
|
68
83
|
"SAT"
|
|
69
84
|
]);
|
|
70
85
|
}
|
|
71
|
-
__name(validateCronExpression, "validateCronExpression");
|
|
72
86
|
function validateCronField(field, min, max, fieldName, fieldNameCN, allowedStrings) {
|
|
73
87
|
if (field === "*") {
|
|
74
88
|
return;
|
|
@@ -83,7 +97,7 @@ function validateCronField(field, min, max, fieldName, fieldNameCN, allowedStrin
|
|
|
83
97
|
const [range, step] = field.split("/");
|
|
84
98
|
const stepValue = parseInt(step);
|
|
85
99
|
if (isNaN(stepValue) || stepValue <= 0) {
|
|
86
|
-
throw new Error(
|
|
100
|
+
throw new Error(`Invalid step value for ${fieldName}: ${step}`);
|
|
87
101
|
}
|
|
88
102
|
if (range !== "*") {
|
|
89
103
|
validateCronField(range, min, max, fieldName, fieldNameCN, allowedStrings);
|
|
@@ -95,13 +109,13 @@ function validateCronField(field, min, max, fieldName, fieldNameCN, allowedStrin
|
|
|
95
109
|
const startValue = parseInt(start);
|
|
96
110
|
const endValue = parseInt(end);
|
|
97
111
|
if (isNaN(startValue) || startValue < min || startValue > max) {
|
|
98
|
-
throw new Error(
|
|
112
|
+
throw new Error(`Invalid range start for ${fieldName}: ${start}, must be between ${min}-${max}`);
|
|
99
113
|
}
|
|
100
114
|
if (isNaN(endValue) || endValue < min || endValue > max) {
|
|
101
|
-
throw new Error(
|
|
115
|
+
throw new Error(`Invalid range end for ${fieldName}: ${end}, must be between ${min}-${max}`);
|
|
102
116
|
}
|
|
103
117
|
if (startValue > endValue) {
|
|
104
|
-
throw new Error(
|
|
118
|
+
throw new Error(`Invalid range for ${fieldName}: ${start}-${end}, start cannot be greater than end`);
|
|
105
119
|
}
|
|
106
120
|
return;
|
|
107
121
|
}
|
|
@@ -114,10 +128,9 @@ function validateCronField(field, min, max, fieldName, fieldNameCN, allowedStrin
|
|
|
114
128
|
}
|
|
115
129
|
const numValue = parseInt(field);
|
|
116
130
|
if (isNaN(numValue) || numValue < min || numValue > max) {
|
|
117
|
-
throw new Error(
|
|
131
|
+
throw new Error(`Invalid ${fieldName} value: ${field}, must be between ${min}-${max}`);
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
|
-
__name(validateCronField, "validateCronField");
|
|
121
134
|
function validateRedLockMethodOptions(options) {
|
|
122
135
|
if (!options || typeof options !== "object") {
|
|
123
136
|
throw new Error("RedLock method options must be an object");
|
|
@@ -143,16 +156,42 @@ function validateRedLockMethodOptions(options) {
|
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
function validateRedLockOptions(options) {
|
|
160
|
+
if (!options || typeof options !== "object") {
|
|
161
|
+
throw new Error("RedLock options must be an object");
|
|
162
|
+
}
|
|
163
|
+
if (options.lockTimeOut !== void 0) {
|
|
164
|
+
if (typeof options.lockTimeOut !== "number" || options.lockTimeOut <= 0) {
|
|
165
|
+
throw new Error("lockTimeOut must be a positive number");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (options.retryCount !== void 0) {
|
|
169
|
+
if (typeof options.retryCount !== "number" || options.retryCount < 0) {
|
|
170
|
+
throw new Error("retryCount must be a non-negative number");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (options.retryDelay !== void 0) {
|
|
174
|
+
if (typeof options.retryDelay !== "number" || options.retryDelay < 0) {
|
|
175
|
+
throw new Error("retryDelay must be a non-negative number");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (options.retryJitter !== void 0) {
|
|
179
|
+
if (typeof options.retryJitter !== "number" || options.retryJitter < 0) {
|
|
180
|
+
throw new Error("retryJitter must be a non-negative number");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function setGlobalScheduledOptions(options) {
|
|
185
|
+
globalScheduledOptions = {
|
|
186
|
+
...options
|
|
187
|
+
};
|
|
188
|
+
}
|
|
148
189
|
function getGlobalScheduledOptions() {
|
|
149
190
|
return globalScheduledOptions;
|
|
150
191
|
}
|
|
151
|
-
__name(getGlobalScheduledOptions, "getGlobalScheduledOptions");
|
|
152
192
|
function getEffectiveTimezone(options, userTimezone) {
|
|
153
193
|
return userTimezone || options.timezone || "Asia/Beijing";
|
|
154
194
|
}
|
|
155
|
-
__name(getEffectiveTimezone, "getEffectiveTimezone");
|
|
156
195
|
function getEffectiveRedLockOptions(methodOptions) {
|
|
157
196
|
const globalOptions = getGlobalScheduledOptions();
|
|
158
197
|
return {
|
|
@@ -162,580 +201,626 @@ function getEffectiveRedLockOptions(methodOptions) {
|
|
|
162
201
|
retryDelayMs: methodOptions?.retryDelayMs || globalOptions.retryDelayMs || 200
|
|
163
202
|
};
|
|
164
203
|
}
|
|
165
|
-
|
|
204
|
+
var COMPONENT_SCHEDULED, COMPONENT_REDLOCK, DecoratorType, globalScheduledOptions;
|
|
205
|
+
var init_config = __esm({
|
|
206
|
+
"src/config/config.ts"() {
|
|
207
|
+
COMPONENT_SCHEDULED = "COMPONENT_SCHEDULED";
|
|
208
|
+
COMPONENT_REDLOCK = "COMPONENT_REDLOCK";
|
|
209
|
+
DecoratorType = /* @__PURE__ */ (function(DecoratorType2) {
|
|
210
|
+
DecoratorType2["SCHEDULED"] = "SCHEDULED";
|
|
211
|
+
DecoratorType2["REDLOCK"] = "REDLOCK";
|
|
212
|
+
return DecoratorType2;
|
|
213
|
+
})({});
|
|
214
|
+
__name(validateCronExpression, "validateCronExpression");
|
|
215
|
+
__name(validateCronField, "validateCronField");
|
|
216
|
+
__name(validateRedLockMethodOptions, "validateRedLockMethodOptions");
|
|
217
|
+
__name(validateRedLockOptions, "validateRedLockOptions");
|
|
218
|
+
globalScheduledOptions = {};
|
|
219
|
+
__name(setGlobalScheduledOptions, "setGlobalScheduledOptions");
|
|
220
|
+
__name(getGlobalScheduledOptions, "getGlobalScheduledOptions");
|
|
221
|
+
__name(getEffectiveTimezone, "getEffectiveTimezone");
|
|
222
|
+
__name(getEffectiveRedLockOptions, "getEffectiveRedLockOptions");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
166
225
|
|
|
167
226
|
// src/locker/interface.ts
|
|
168
|
-
var RedisMode
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
*/
|
|
229
|
-
static createClient(config) {
|
|
230
|
-
const mode = config.mode || RedisMode.STANDALONE;
|
|
231
|
-
DefaultLogger.Debug(`Creating Redis client in ${mode} mode`);
|
|
232
|
-
switch (mode) {
|
|
233
|
-
case RedisMode.STANDALONE:
|
|
234
|
-
return this.createStandaloneClient(config);
|
|
235
|
-
case RedisMode.SENTINEL:
|
|
236
|
-
return this.createSentinelClient(config);
|
|
237
|
-
case RedisMode.CLUSTER:
|
|
238
|
-
return this.createClusterClient(config);
|
|
239
|
-
default:
|
|
240
|
-
throw new Error(`\u4E0D\u652F\u6301\u7684 Redis \u6A21\u5F0F: ${mode} (Unsupported Redis mode: ${mode})`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Create standalone Redis client
|
|
245
|
-
* @param config - Standalone configuration
|
|
246
|
-
*/
|
|
247
|
-
static createStandaloneClient(config) {
|
|
248
|
-
DefaultLogger.Debug(`Creating standalone Redis client: ${config.host}:${config.port}`);
|
|
249
|
-
const options = {
|
|
250
|
-
host: config.host,
|
|
251
|
-
port: config.port,
|
|
252
|
-
password: config.password || void 0,
|
|
253
|
-
db: config.db || 0,
|
|
254
|
-
keyPrefix: config.keyPrefix || "",
|
|
255
|
-
connectTimeout: config.connectTimeout || 1e4,
|
|
256
|
-
commandTimeout: config.commandTimeout || 5e3,
|
|
257
|
-
maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
|
|
258
|
-
retryStrategy: /* @__PURE__ */ __name((times) => {
|
|
259
|
-
const delay = Math.min(times * 50, 2e3);
|
|
260
|
-
DefaultLogger.Debug(`Redis reconnecting, attempt ${times}, delay ${delay}ms`);
|
|
261
|
-
return delay;
|
|
262
|
-
}, "retryStrategy"),
|
|
263
|
-
reconnectOnError: /* @__PURE__ */ __name((err) => {
|
|
264
|
-
DefaultLogger.Warn("Redis connection error, attempting reconnect:", err.message);
|
|
265
|
-
return true;
|
|
266
|
-
}, "reconnectOnError")
|
|
267
|
-
};
|
|
268
|
-
const client = new Redis(options);
|
|
269
|
-
client.on("connect", () => {
|
|
270
|
-
DefaultLogger.Info("Redis standalone client connected successfully");
|
|
271
|
-
});
|
|
272
|
-
client.on("error", (err) => {
|
|
273
|
-
DefaultLogger.Error("Redis standalone client error:", err);
|
|
274
|
-
});
|
|
275
|
-
return new RedisClientAdapter(client);
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Create sentinel Redis client
|
|
279
|
-
* @param config - Sentinel configuration
|
|
280
|
-
*/
|
|
281
|
-
static createSentinelClient(config) {
|
|
282
|
-
DefaultLogger.Debug(`Creating sentinel Redis client for master: ${config.name}`);
|
|
283
|
-
const options = {
|
|
284
|
-
sentinels: config.sentinels,
|
|
285
|
-
name: config.name,
|
|
286
|
-
password: config.password || void 0,
|
|
287
|
-
sentinelPassword: config.sentinelPassword || void 0,
|
|
288
|
-
db: config.db || 0,
|
|
289
|
-
keyPrefix: config.keyPrefix || "",
|
|
290
|
-
connectTimeout: config.connectTimeout || 1e4,
|
|
291
|
-
commandTimeout: config.commandTimeout || 5e3,
|
|
292
|
-
maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
|
|
293
|
-
retryStrategy: /* @__PURE__ */ __name((times) => {
|
|
294
|
-
const delay = Math.min(times * 50, 2e3);
|
|
295
|
-
DefaultLogger.Debug(`Sentinel Redis reconnecting, attempt ${times}, delay ${delay}ms`);
|
|
296
|
-
return delay;
|
|
297
|
-
}, "retryStrategy")
|
|
227
|
+
var RedisMode;
|
|
228
|
+
var init_interface = __esm({
|
|
229
|
+
"src/locker/interface.ts"() {
|
|
230
|
+
RedisMode = /* @__PURE__ */ (function(RedisMode2) {
|
|
231
|
+
RedisMode2["STANDALONE"] = "standalone";
|
|
232
|
+
RedisMode2["SENTINEL"] = "sentinel";
|
|
233
|
+
RedisMode2["CLUSTER"] = "cluster";
|
|
234
|
+
return RedisMode2;
|
|
235
|
+
})({});
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
var RedisClientAdapter, RedisFactory;
|
|
239
|
+
var init_redis_factory = __esm({
|
|
240
|
+
"src/locker/redis-factory.ts"() {
|
|
241
|
+
init_interface();
|
|
242
|
+
RedisClientAdapter = class RedisClientAdapter2 {
|
|
243
|
+
static {
|
|
244
|
+
__name(this, "RedisClientAdapter");
|
|
245
|
+
}
|
|
246
|
+
client;
|
|
247
|
+
constructor(client) {
|
|
248
|
+
this.client = client;
|
|
249
|
+
}
|
|
250
|
+
get status() {
|
|
251
|
+
return this.client.status;
|
|
252
|
+
}
|
|
253
|
+
async call(command, ...args) {
|
|
254
|
+
return this.client.call(command, ...args);
|
|
255
|
+
}
|
|
256
|
+
async set(key, value, mode, duration) {
|
|
257
|
+
if (mode && duration) {
|
|
258
|
+
return this.client.set(key, value, mode, duration);
|
|
259
|
+
}
|
|
260
|
+
return this.client.set(key, value);
|
|
261
|
+
}
|
|
262
|
+
async get(key) {
|
|
263
|
+
return this.client.get(key);
|
|
264
|
+
}
|
|
265
|
+
async del(...keys) {
|
|
266
|
+
return this.client.del(...keys);
|
|
267
|
+
}
|
|
268
|
+
async exists(key) {
|
|
269
|
+
return this.client.exists(key);
|
|
270
|
+
}
|
|
271
|
+
async eval(script, numKeys, ...args) {
|
|
272
|
+
return this.client.eval(script, numKeys, ...args);
|
|
273
|
+
}
|
|
274
|
+
async quit() {
|
|
275
|
+
return this.client.quit();
|
|
276
|
+
}
|
|
277
|
+
disconnect() {
|
|
278
|
+
this.client.disconnect();
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get underlying Redis/Cluster instance
|
|
282
|
+
* Used for RedLock initialization
|
|
283
|
+
*/
|
|
284
|
+
getClient() {
|
|
285
|
+
return this.client;
|
|
286
|
+
}
|
|
298
287
|
};
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
288
|
+
RedisFactory = class {
|
|
289
|
+
static {
|
|
290
|
+
__name(this, "RedisFactory");
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Create Redis client based on configuration mode
|
|
294
|
+
* @param config - Redis configuration
|
|
295
|
+
* @returns Redis client adapter
|
|
296
|
+
*/
|
|
297
|
+
static createClient(config) {
|
|
298
|
+
const mode = config.mode || RedisMode.STANDALONE;
|
|
299
|
+
DefaultLogger.Debug(`Creating Redis client in ${mode} mode`);
|
|
300
|
+
switch (mode) {
|
|
301
|
+
case RedisMode.STANDALONE:
|
|
302
|
+
return this.createStandaloneClient(config);
|
|
303
|
+
case RedisMode.SENTINEL:
|
|
304
|
+
return this.createSentinelClient(config);
|
|
305
|
+
case RedisMode.CLUSTER:
|
|
306
|
+
return this.createClusterClient(config);
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`Unsupported Redis mode: ${mode}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Create standalone Redis client
|
|
313
|
+
* @param config - Standalone configuration
|
|
314
|
+
*/
|
|
315
|
+
static createStandaloneClient(config) {
|
|
316
|
+
DefaultLogger.Debug(`Creating standalone Redis client: ${config.host}:${config.port}`);
|
|
317
|
+
const options = {
|
|
318
|
+
host: config.host,
|
|
319
|
+
port: config.port,
|
|
320
|
+
password: config.password || void 0,
|
|
321
|
+
db: config.db || 0,
|
|
322
|
+
keyPrefix: config.keyPrefix || "",
|
|
323
|
+
connectTimeout: config.connectTimeout || 1e4,
|
|
324
|
+
commandTimeout: config.commandTimeout || 5e3,
|
|
325
|
+
maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
|
|
326
|
+
retryStrategy: /* @__PURE__ */ __name((times) => {
|
|
327
|
+
const delay = Math.min(times * 50, 2e3);
|
|
328
|
+
DefaultLogger.Debug(`Redis reconnecting, attempt ${times}, delay ${delay}ms`);
|
|
329
|
+
return delay;
|
|
330
|
+
}, "retryStrategy"),
|
|
331
|
+
reconnectOnError: /* @__PURE__ */ __name((err) => {
|
|
332
|
+
DefaultLogger.Warn("Redis connection error, attempting reconnect:", err.message);
|
|
333
|
+
return true;
|
|
334
|
+
}, "reconnectOnError")
|
|
335
|
+
};
|
|
336
|
+
const client = new Redis(options);
|
|
337
|
+
client.on("connect", () => {
|
|
338
|
+
DefaultLogger.Info("Redis standalone client connected successfully");
|
|
339
|
+
});
|
|
340
|
+
client.on("error", (err) => {
|
|
341
|
+
DefaultLogger.Error("Redis standalone client error:", err);
|
|
342
|
+
});
|
|
343
|
+
return new RedisClientAdapter(client);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Create sentinel Redis client
|
|
347
|
+
* @param config - Sentinel configuration
|
|
348
|
+
*/
|
|
349
|
+
static createSentinelClient(config) {
|
|
350
|
+
DefaultLogger.Debug(`Creating sentinel Redis client for master: ${config.name}`);
|
|
351
|
+
const options = {
|
|
352
|
+
sentinels: config.sentinels,
|
|
353
|
+
name: config.name,
|
|
354
|
+
password: config.password || void 0,
|
|
355
|
+
sentinelPassword: config.sentinelPassword || void 0,
|
|
356
|
+
db: config.db || 0,
|
|
357
|
+
keyPrefix: config.keyPrefix || "",
|
|
358
|
+
connectTimeout: config.connectTimeout || 1e4,
|
|
359
|
+
commandTimeout: config.commandTimeout || 5e3,
|
|
360
|
+
maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
|
|
361
|
+
retryStrategy: /* @__PURE__ */ __name((times) => {
|
|
362
|
+
const delay = Math.min(times * 50, 2e3);
|
|
363
|
+
DefaultLogger.Debug(`Sentinel Redis reconnecting, attempt ${times}, delay ${delay}ms`);
|
|
364
|
+
return delay;
|
|
365
|
+
}, "retryStrategy")
|
|
366
|
+
};
|
|
367
|
+
const client = new Redis(options);
|
|
368
|
+
client.on("connect", () => {
|
|
369
|
+
DefaultLogger.Info(`Redis sentinel client connected to master: ${config.name}`);
|
|
370
|
+
});
|
|
371
|
+
client.on("error", (err) => {
|
|
372
|
+
DefaultLogger.Error("Redis sentinel client error:", err);
|
|
373
|
+
});
|
|
374
|
+
return new RedisClientAdapter(client);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Create cluster Redis client
|
|
378
|
+
* @param config - Cluster configuration
|
|
379
|
+
*/
|
|
380
|
+
static createClusterClient(config) {
|
|
381
|
+
DefaultLogger.Debug(`Creating cluster Redis client with ${config.nodes.length} nodes`);
|
|
382
|
+
const clusterOptions = {
|
|
383
|
+
redisOptions: {
|
|
384
|
+
password: config.redisOptions?.password || config.password || void 0,
|
|
385
|
+
db: config.redisOptions?.db || config.db || 0,
|
|
386
|
+
keyPrefix: config.keyPrefix || "",
|
|
387
|
+
connectTimeout: config.connectTimeout || 1e4,
|
|
388
|
+
commandTimeout: config.commandTimeout || 5e3,
|
|
389
|
+
maxRetriesPerRequest: config.maxRetriesPerRequest || 3
|
|
390
|
+
},
|
|
391
|
+
clusterRetryStrategy: /* @__PURE__ */ __name((times) => {
|
|
392
|
+
const delay = Math.min(times * 50, 2e3);
|
|
393
|
+
DefaultLogger.Debug(`Cluster Redis reconnecting, attempt ${times}, delay ${delay}ms`);
|
|
394
|
+
return delay;
|
|
395
|
+
}, "clusterRetryStrategy")
|
|
396
|
+
};
|
|
397
|
+
const cluster = new Cluster(config.nodes, clusterOptions);
|
|
398
|
+
cluster.on("connect", () => {
|
|
399
|
+
DefaultLogger.Info("Redis cluster client connected successfully");
|
|
400
|
+
});
|
|
401
|
+
cluster.on("error", (err) => {
|
|
402
|
+
DefaultLogger.Error("Redis cluster client error:", err);
|
|
403
|
+
});
|
|
404
|
+
cluster.on("node error", (err, address) => {
|
|
405
|
+
DefaultLogger.Error(`Redis cluster node error at ${address}:`, err);
|
|
406
|
+
});
|
|
407
|
+
return new RedisClientAdapter(cluster);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Validate Redis configuration
|
|
411
|
+
* @param config - Redis configuration to validate
|
|
412
|
+
*/
|
|
413
|
+
static validateConfig(config) {
|
|
414
|
+
if (!config) {
|
|
415
|
+
throw new Error("Redis configuration cannot be empty");
|
|
416
|
+
}
|
|
417
|
+
const mode = config.mode || RedisMode.STANDALONE;
|
|
418
|
+
switch (mode) {
|
|
419
|
+
case RedisMode.STANDALONE:
|
|
420
|
+
this.validateStandaloneConfig(config);
|
|
421
|
+
break;
|
|
422
|
+
case RedisMode.SENTINEL:
|
|
423
|
+
this.validateSentinelConfig(config);
|
|
424
|
+
break;
|
|
425
|
+
case RedisMode.CLUSTER:
|
|
426
|
+
this.validateClusterConfig(config);
|
|
427
|
+
break;
|
|
428
|
+
default:
|
|
429
|
+
throw new Error(`Unsupported Redis mode: ${mode}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
static validateStandaloneConfig(config) {
|
|
433
|
+
if (!config.host) {
|
|
434
|
+
throw new Error("Standalone mode requires host configuration");
|
|
435
|
+
}
|
|
436
|
+
if (!config.port) {
|
|
437
|
+
throw new Error("Standalone mode requires port configuration");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
static validateSentinelConfig(config) {
|
|
441
|
+
if (!config.sentinels || config.sentinels.length === 0) {
|
|
442
|
+
throw new Error("Sentinel mode requires at least one sentinel node");
|
|
443
|
+
}
|
|
444
|
+
if (!config.name) {
|
|
445
|
+
throw new Error("Sentinel mode requires master name");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
static validateClusterConfig(config) {
|
|
449
|
+
if (!config.nodes || config.nodes.length === 0) {
|
|
450
|
+
throw new Error("Cluster mode requires at least one node");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
328
453
|
};
|
|
329
|
-
const cluster = new Cluster(config.nodes, clusterOptions);
|
|
330
|
-
cluster.on("connect", () => {
|
|
331
|
-
DefaultLogger.Info("Redis cluster client connected successfully");
|
|
332
|
-
});
|
|
333
|
-
cluster.on("error", (err) => {
|
|
334
|
-
DefaultLogger.Error("Redis cluster client error:", err);
|
|
335
|
-
});
|
|
336
|
-
cluster.on("node error", (err, address) => {
|
|
337
|
-
DefaultLogger.Error(`Redis cluster node error at ${address}:`, err);
|
|
338
|
-
});
|
|
339
|
-
return new RedisClientAdapter(cluster);
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Validate Redis configuration
|
|
343
|
-
* @param config - Redis configuration to validate
|
|
344
|
-
*/
|
|
345
|
-
static validateConfig(config) {
|
|
346
|
-
if (!config) {
|
|
347
|
-
throw new Error("Redis \u914D\u7F6E\u4E0D\u80FD\u4E3A\u7A7A (Redis configuration cannot be empty)");
|
|
348
|
-
}
|
|
349
|
-
const mode = config.mode || RedisMode.STANDALONE;
|
|
350
|
-
switch (mode) {
|
|
351
|
-
case RedisMode.STANDALONE:
|
|
352
|
-
this.validateStandaloneConfig(config);
|
|
353
|
-
break;
|
|
354
|
-
case RedisMode.SENTINEL:
|
|
355
|
-
this.validateSentinelConfig(config);
|
|
356
|
-
break;
|
|
357
|
-
case RedisMode.CLUSTER:
|
|
358
|
-
this.validateClusterConfig(config);
|
|
359
|
-
break;
|
|
360
|
-
default:
|
|
361
|
-
throw new Error(`\u4E0D\u652F\u6301\u7684 Redis \u6A21\u5F0F: ${mode} (Unsupported Redis mode: ${mode})`);
|
|
362
|
-
}
|
|
363
454
|
}
|
|
364
|
-
|
|
365
|
-
if (!config.host) {
|
|
366
|
-
throw new Error("\u5355\u673A\u6A21\u5F0F\u9700\u8981 host \u914D\u7F6E (Standalone mode requires host configuration)");
|
|
367
|
-
}
|
|
368
|
-
if (!config.port) {
|
|
369
|
-
throw new Error("\u5355\u673A\u6A21\u5F0F\u9700\u8981 port \u914D\u7F6E (Standalone mode requires port configuration)");
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
static validateSentinelConfig(config) {
|
|
373
|
-
if (!config.sentinels || config.sentinels.length === 0) {
|
|
374
|
-
throw new Error("\u54E8\u5175\u6A21\u5F0F\u9700\u8981\u81F3\u5C11\u4E00\u4E2A\u54E8\u5175\u8282\u70B9\u914D\u7F6E (Sentinel mode requires at least one sentinel node)");
|
|
375
|
-
}
|
|
376
|
-
if (!config.name) {
|
|
377
|
-
throw new Error("\u54E8\u5175\u6A21\u5F0F\u9700\u8981 master name \u914D\u7F6E (Sentinel mode requires master name)");
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
static validateClusterConfig(config) {
|
|
381
|
-
if (!config.nodes || config.nodes.length === 0) {
|
|
382
|
-
throw new Error("\u96C6\u7FA4\u6A21\u5F0F\u9700\u8981\u81F3\u5C11\u4E00\u4E2A\u8282\u70B9\u914D\u7F6E (Cluster mode requires at least one node)");
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
};
|
|
455
|
+
});
|
|
386
456
|
|
|
387
457
|
// src/locker/redlock.ts
|
|
388
|
-
var
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
static {
|
|
411
|
-
__name(this, "RedLocker");
|
|
412
|
-
}
|
|
413
|
-
static instance = null;
|
|
414
|
-
static instanceLock = /* @__PURE__ */ Symbol("RedLocker.instanceLock");
|
|
415
|
-
redlock = null;
|
|
416
|
-
redisClient = null;
|
|
417
|
-
config;
|
|
418
|
-
isInitialized = false;
|
|
419
|
-
initializationPromise = null;
|
|
420
|
-
// 私有构造函数防止外部直接实例化
|
|
421
|
-
constructor(options) {
|
|
422
|
-
this.config = {
|
|
423
|
-
...defaultRedLockConfig,
|
|
424
|
-
...options
|
|
458
|
+
var redlock_exports = {};
|
|
459
|
+
__export(redlock_exports, {
|
|
460
|
+
RedLocker: () => RedLocker
|
|
461
|
+
});
|
|
462
|
+
var defaultRedLockConfig, defaultRedlockSettings, RedLocker;
|
|
463
|
+
var init_redlock = __esm({
|
|
464
|
+
"src/locker/redlock.ts"() {
|
|
465
|
+
init_interface();
|
|
466
|
+
init_redis_factory();
|
|
467
|
+
defaultRedLockConfig = {
|
|
468
|
+
lockTimeOut: 1e4,
|
|
469
|
+
clockDriftFactor: 0.01,
|
|
470
|
+
maxRetries: 3,
|
|
471
|
+
retryDelayMs: 200,
|
|
472
|
+
redisConfig: {
|
|
473
|
+
mode: RedisMode.STANDALONE,
|
|
474
|
+
host: "127.0.0.1",
|
|
475
|
+
port: 6379,
|
|
476
|
+
password: "",
|
|
477
|
+
db: 0,
|
|
478
|
+
keyPrefix: "redlock:"
|
|
479
|
+
}
|
|
425
480
|
};
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
481
|
+
defaultRedlockSettings = {
|
|
482
|
+
driftFactor: 0.01,
|
|
483
|
+
retryCount: 3,
|
|
484
|
+
retryDelay: 200,
|
|
485
|
+
retryJitter: 200,
|
|
486
|
+
automaticExtensionThreshold: 500
|
|
487
|
+
};
|
|
488
|
+
RedLocker = class _RedLocker {
|
|
489
|
+
static {
|
|
490
|
+
__name(this, "RedLocker");
|
|
491
|
+
}
|
|
492
|
+
static instance = null;
|
|
493
|
+
static instanceLock = /* @__PURE__ */ Symbol("RedLocker.instanceLock");
|
|
494
|
+
redlock = null;
|
|
495
|
+
redisClient = null;
|
|
496
|
+
config;
|
|
497
|
+
isInitialized = false;
|
|
498
|
+
initializationPromise = null;
|
|
499
|
+
// 私有构造函数防止外部直接实例化
|
|
500
|
+
constructor(options) {
|
|
501
|
+
this.config = {
|
|
502
|
+
...defaultRedLockConfig,
|
|
503
|
+
...options
|
|
504
|
+
};
|
|
505
|
+
this.registerInContainer();
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Register RedLocker in IOC container
|
|
509
|
+
* @private
|
|
510
|
+
*/
|
|
511
|
+
registerInContainer() {
|
|
452
512
|
try {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
513
|
+
const RedLockerClass = this.constructor;
|
|
514
|
+
IOCContainer.saveClass("COMPONENT", RedLockerClass, "RedLocker");
|
|
515
|
+
IOCContainer.setExistingInstance(RedLockerClass, this);
|
|
516
|
+
DefaultLogger.Debug("RedLocker registered in IOC container");
|
|
517
|
+
} catch (_error) {
|
|
518
|
+
DefaultLogger.Warn("Failed to register RedLocker in IOC container:", _error);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get RedLocker singleton instance with thread-safe initialization
|
|
523
|
+
* @static
|
|
524
|
+
* @param options - RedLock configuration options (only used for first initialization)
|
|
525
|
+
* @returns RedLocker singleton instance
|
|
526
|
+
*/
|
|
527
|
+
static getInstance(options) {
|
|
528
|
+
if (!_RedLocker.instance) {
|
|
529
|
+
if (_RedLocker.instance === null) {
|
|
530
|
+
try {
|
|
531
|
+
const containerInstance = IOCContainer.get("RedLocker", "COMPONENT");
|
|
532
|
+
if (containerInstance) {
|
|
533
|
+
_RedLocker.instance = containerInstance;
|
|
534
|
+
DefaultLogger.Debug("Retrieved existing RedLocker instance from IOC container");
|
|
535
|
+
} else {
|
|
536
|
+
_RedLocker.instance = new _RedLocker(options);
|
|
537
|
+
DefaultLogger.Debug("Created new RedLocker singleton instance");
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
_RedLocker.instance = new _RedLocker(options);
|
|
541
|
+
DefaultLogger.Debug("Created new RedLocker instance outside IOC container");
|
|
542
|
+
}
|
|
460
543
|
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
DefaultLogger.Debug("Created new RedLocker instance outside IOC container");
|
|
544
|
+
} else if (options) {
|
|
545
|
+
DefaultLogger.Warn("RedLocker instance already exists, ignoring new options. Use updateConfig() to change configuration.");
|
|
464
546
|
}
|
|
547
|
+
return _RedLocker.instance;
|
|
465
548
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
static resetInstance() {
|
|
476
|
-
if (_RedLocker.instance) {
|
|
477
|
-
_RedLocker.instance.close().catch((err) => DefaultLogger.Warn("Error while closing RedLocker instance during reset:", err));
|
|
478
|
-
_RedLocker.instance = null;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Initialize RedLock with Redis connection
|
|
483
|
-
* Uses cached promise to avoid duplicate initialization
|
|
484
|
-
* @private
|
|
485
|
-
*/
|
|
486
|
-
async initialize() {
|
|
487
|
-
if (this.isInitialized) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
if (this.initializationPromise) {
|
|
491
|
-
return this.initializationPromise;
|
|
492
|
-
}
|
|
493
|
-
this.initializationPromise = this.performInitialization();
|
|
494
|
-
try {
|
|
495
|
-
await this.initializationPromise;
|
|
496
|
-
} catch (error) {
|
|
497
|
-
this.initializationPromise = null;
|
|
498
|
-
this.isInitialized = false;
|
|
499
|
-
this.redlock = null;
|
|
500
|
-
DefaultLogger.Warn("RedLocker initialization failed, state has been reset for retry");
|
|
501
|
-
throw error;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* 执行实际的初始化操作
|
|
506
|
-
* @private
|
|
507
|
-
*/
|
|
508
|
-
async performInitialization() {
|
|
509
|
-
try {
|
|
510
|
-
if (this.config.redisConfig) {
|
|
511
|
-
RedisFactory.validateConfig(this.config.redisConfig);
|
|
549
|
+
/**
|
|
550
|
+
* Reset singleton instance (主要用于测试)
|
|
551
|
+
* @static
|
|
552
|
+
*/
|
|
553
|
+
static resetInstance() {
|
|
554
|
+
if (_RedLocker.instance) {
|
|
555
|
+
_RedLocker.instance.close().catch((err) => DefaultLogger.Warn("Error while closing RedLocker instance during reset:", err));
|
|
556
|
+
_RedLocker.instance = null;
|
|
557
|
+
}
|
|
512
558
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
559
|
+
/**
|
|
560
|
+
* Initialize RedLock with Redis connection
|
|
561
|
+
* Uses cached promise to avoid duplicate initialization
|
|
562
|
+
* @private
|
|
563
|
+
*/
|
|
564
|
+
async initialize() {
|
|
565
|
+
if (this.isInitialized) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (this.initializationPromise) {
|
|
569
|
+
return this.initializationPromise;
|
|
570
|
+
}
|
|
571
|
+
this.initializationPromise = this.performInitialization();
|
|
572
|
+
try {
|
|
573
|
+
await this.initializationPromise;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.initializationPromise = null;
|
|
576
|
+
this.isInitialized = false;
|
|
577
|
+
this.redlock = null;
|
|
578
|
+
DefaultLogger.Warn("RedLocker initialization failed, state has been reset for retry");
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* 执行实际的初始化操作
|
|
584
|
+
* @private
|
|
585
|
+
*/
|
|
586
|
+
async performInitialization() {
|
|
587
|
+
try {
|
|
588
|
+
if (this.config.redisConfig) {
|
|
589
|
+
RedisFactory.validateConfig(this.config.redisConfig);
|
|
520
590
|
}
|
|
521
|
-
|
|
591
|
+
try {
|
|
592
|
+
const existingRedis = IOCContainer.get("Redis", "COMPONENT");
|
|
593
|
+
if (existingRedis) {
|
|
594
|
+
if (existingRedis instanceof RedisClientAdapter) {
|
|
595
|
+
this.redisClient = existingRedis;
|
|
596
|
+
} else {
|
|
597
|
+
this.redisClient = new RedisClientAdapter(existingRedis);
|
|
598
|
+
}
|
|
599
|
+
DefaultLogger.Debug("Using Redis instance from IOC container");
|
|
600
|
+
}
|
|
601
|
+
} catch {
|
|
602
|
+
}
|
|
603
|
+
if (!this.redisClient && this.config.redisConfig) {
|
|
604
|
+
this.redisClient = RedisFactory.createClient(this.config.redisConfig);
|
|
605
|
+
DefaultLogger.Debug("Created new Redis connection for RedLocker");
|
|
606
|
+
}
|
|
607
|
+
if (!this.redisClient) {
|
|
608
|
+
throw new Error("Failed to initialize Redis connection: no configuration provided");
|
|
609
|
+
}
|
|
610
|
+
const underlyingClient = this.redisClient.getClient();
|
|
611
|
+
const userSettings = this.config;
|
|
612
|
+
const redlockSettings = {
|
|
613
|
+
...defaultRedlockSettings,
|
|
614
|
+
...userSettings.driftFactor !== void 0 && {
|
|
615
|
+
driftFactor: userSettings.driftFactor
|
|
616
|
+
},
|
|
617
|
+
...userSettings.retryCount !== void 0 && {
|
|
618
|
+
retryCount: userSettings.retryCount
|
|
619
|
+
},
|
|
620
|
+
...userSettings.retryDelay !== void 0 && {
|
|
621
|
+
retryDelay: userSettings.retryDelay
|
|
622
|
+
},
|
|
623
|
+
...userSettings.retryJitter !== void 0 && {
|
|
624
|
+
retryJitter: userSettings.retryJitter
|
|
625
|
+
},
|
|
626
|
+
...userSettings.automaticExtensionThreshold !== void 0 && {
|
|
627
|
+
automaticExtensionThreshold: userSettings.automaticExtensionThreshold
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
this.redlock = new Redlock([
|
|
631
|
+
underlyingClient
|
|
632
|
+
], redlockSettings);
|
|
633
|
+
this.redlock.on("clientError", (err) => {
|
|
634
|
+
DefaultLogger.Error("Redis client error in RedLock:", err);
|
|
635
|
+
});
|
|
636
|
+
this.isInitialized = true;
|
|
637
|
+
DefaultLogger.Info("RedLocker initialized successfully");
|
|
638
|
+
} catch (error) {
|
|
639
|
+
this.isInitialized = false;
|
|
640
|
+
DefaultLogger.Error("Failed to initialize RedLocker:", error);
|
|
641
|
+
throw new Error(`RedLocker initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
522
642
|
}
|
|
523
|
-
} catch {
|
|
524
643
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
644
|
+
/**
|
|
645
|
+
* Acquire a distributed lock
|
|
646
|
+
* @param resources - Resource identifiers to lock
|
|
647
|
+
* @param ttl - Time to live in milliseconds
|
|
648
|
+
* @returns Promise<Lock>
|
|
649
|
+
*/
|
|
650
|
+
async acquire(resources, ttl) {
|
|
651
|
+
if (!Array.isArray(resources) || resources.length === 0) {
|
|
652
|
+
throw new Error("Resources array cannot be empty");
|
|
653
|
+
}
|
|
654
|
+
const lockTtl = ttl || this.config.lockTimeOut;
|
|
655
|
+
if (lockTtl <= 0) {
|
|
656
|
+
throw new Error("Lock TTL must be positive");
|
|
657
|
+
}
|
|
658
|
+
await this.initialize();
|
|
659
|
+
if (!this.redlock) {
|
|
660
|
+
throw new Error("RedLock is not initialized");
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
const prefixedResources = resources.map((resource) => `${this.config.redisConfig.keyPrefix}${resource}`);
|
|
664
|
+
DefaultLogger.Debug(`Acquiring lock for resources: ${prefixedResources.join(", ")} with TTL: ${lockTtl}ms`);
|
|
665
|
+
const lock = await this.redlock.acquire(prefixedResources, lockTtl);
|
|
666
|
+
DefaultLogger.Debug(`Lock acquired successfully for resources: ${prefixedResources.join(", ")}`);
|
|
667
|
+
return lock;
|
|
668
|
+
} catch (error) {
|
|
669
|
+
DefaultLogger.Error(`Failed to acquire lock for resources: ${resources.join(", ")}`, error);
|
|
670
|
+
if (error instanceof Error) {
|
|
671
|
+
error.message = `Lock acquisition failed: ${error.message}`;
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
throw new Error(`Lock acquisition failed: Unknown error`);
|
|
675
|
+
}
|
|
528
676
|
}
|
|
529
|
-
|
|
530
|
-
|
|
677
|
+
/**
|
|
678
|
+
* Release a lock
|
|
679
|
+
* @param lock - Lock instance to release
|
|
680
|
+
*/
|
|
681
|
+
async release(lock) {
|
|
682
|
+
if (!lock) {
|
|
683
|
+
throw new Error("Lock instance is required");
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
await lock.release();
|
|
687
|
+
DefaultLogger.Debug("Lock released successfully");
|
|
688
|
+
} catch (error) {
|
|
689
|
+
DefaultLogger.Error("Failed to release lock:", error);
|
|
690
|
+
if (error instanceof Error) {
|
|
691
|
+
error.message = `Lock release failed: ${error.message}`;
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
throw new Error(`Lock release failed: Unknown error`);
|
|
695
|
+
}
|
|
531
696
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
697
|
+
/**
|
|
698
|
+
* Extend a lock's TTL
|
|
699
|
+
* @param lock - Lock instance to extend
|
|
700
|
+
* @param ttl - New TTL in milliseconds
|
|
701
|
+
* @returns Extended lock
|
|
702
|
+
*/
|
|
703
|
+
async extend(lock, ttl) {
|
|
704
|
+
if (!lock) {
|
|
705
|
+
throw new Error("Lock instance is required");
|
|
706
|
+
}
|
|
707
|
+
if (ttl <= 0) {
|
|
708
|
+
throw new Error("TTL must be positive");
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
const extendedLock = await lock.extend(ttl);
|
|
712
|
+
DefaultLogger.Debug(`Lock extended successfully with TTL: ${ttl}ms`);
|
|
713
|
+
return extendedLock;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
DefaultLogger.Error("Failed to extend lock:", error);
|
|
716
|
+
throw new Error(`Lock extension failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
550
717
|
}
|
|
551
|
-
};
|
|
552
|
-
this.redlock = new Redlock([
|
|
553
|
-
underlyingClient
|
|
554
|
-
], redlockSettings);
|
|
555
|
-
this.redlock.on("clientError", (err) => {
|
|
556
|
-
DefaultLogger.Error("Redis client error in RedLock:", err);
|
|
557
|
-
});
|
|
558
|
-
this.isInitialized = true;
|
|
559
|
-
DefaultLogger.Info("RedLocker initialized successfully");
|
|
560
|
-
} catch (error) {
|
|
561
|
-
this.isInitialized = false;
|
|
562
|
-
DefaultLogger.Error("Failed to initialize RedLocker:", error);
|
|
563
|
-
throw new Error(`RedLocker initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Acquire a distributed lock
|
|
568
|
-
* @param resources - Resource identifiers to lock
|
|
569
|
-
* @param ttl - Time to live in milliseconds
|
|
570
|
-
* @returns Promise<Lock>
|
|
571
|
-
*/
|
|
572
|
-
async acquire(resources, ttl) {
|
|
573
|
-
if (!Array.isArray(resources) || resources.length === 0) {
|
|
574
|
-
throw new Error("Resources array cannot be empty");
|
|
575
|
-
}
|
|
576
|
-
const lockTtl = ttl || this.config.lockTimeOut;
|
|
577
|
-
if (lockTtl <= 0) {
|
|
578
|
-
throw new Error("Lock TTL must be positive");
|
|
579
|
-
}
|
|
580
|
-
await this.initialize();
|
|
581
|
-
if (!this.redlock) {
|
|
582
|
-
throw new Error("RedLock is not initialized");
|
|
583
|
-
}
|
|
584
|
-
try {
|
|
585
|
-
const prefixedResources = resources.map((resource) => `${this.config.redisConfig.keyPrefix}${resource}`);
|
|
586
|
-
DefaultLogger.Debug(`Acquiring lock for resources: ${prefixedResources.join(", ")} with TTL: ${lockTtl}ms`);
|
|
587
|
-
const lock = await this.redlock.acquire(prefixedResources, lockTtl);
|
|
588
|
-
DefaultLogger.Debug(`Lock acquired successfully for resources: ${prefixedResources.join(", ")}`);
|
|
589
|
-
return lock;
|
|
590
|
-
} catch (error) {
|
|
591
|
-
DefaultLogger.Error(`Failed to acquire lock for resources: ${resources.join(", ")}`, error);
|
|
592
|
-
if (error instanceof Error) {
|
|
593
|
-
error.message = `Lock acquisition failed: ${error.message}`;
|
|
594
|
-
throw error;
|
|
595
718
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
*/
|
|
603
|
-
async release(lock) {
|
|
604
|
-
if (!lock) {
|
|
605
|
-
throw new Error("Lock instance is required");
|
|
606
|
-
}
|
|
607
|
-
try {
|
|
608
|
-
await lock.release();
|
|
609
|
-
DefaultLogger.Debug("Lock released successfully");
|
|
610
|
-
} catch (error) {
|
|
611
|
-
DefaultLogger.Error("Failed to release lock:", error);
|
|
612
|
-
if (error instanceof Error) {
|
|
613
|
-
error.message = `Lock release failed: ${error.message}`;
|
|
614
|
-
throw error;
|
|
719
|
+
/**
|
|
720
|
+
* Check if RedLocker is initialized
|
|
721
|
+
* @returns true if initialized, false otherwise
|
|
722
|
+
*/
|
|
723
|
+
isReady() {
|
|
724
|
+
return this.isInitialized && !!this.redlock && !!this.redisClient;
|
|
615
725
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
*/
|
|
625
|
-
async extend(lock, ttl) {
|
|
626
|
-
if (!lock) {
|
|
627
|
-
throw new Error("Lock instance is required");
|
|
628
|
-
}
|
|
629
|
-
if (ttl <= 0) {
|
|
630
|
-
throw new Error("TTL must be positive");
|
|
631
|
-
}
|
|
632
|
-
try {
|
|
633
|
-
const extendedLock = await lock.extend(ttl);
|
|
634
|
-
DefaultLogger.Debug(`Lock extended successfully with TTL: ${ttl}ms`);
|
|
635
|
-
return extendedLock;
|
|
636
|
-
} catch (error) {
|
|
637
|
-
DefaultLogger.Error("Failed to extend lock:", error);
|
|
638
|
-
throw new Error(`Lock extension failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Check if RedLocker is initialized
|
|
643
|
-
* @returns true if initialized, false otherwise
|
|
644
|
-
*/
|
|
645
|
-
isReady() {
|
|
646
|
-
return this.isInitialized && !!this.redlock && !!this.redisClient;
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Get current configuration
|
|
650
|
-
* @returns Current RedLock configuration
|
|
651
|
-
*/
|
|
652
|
-
getConfig() {
|
|
653
|
-
return {
|
|
654
|
-
...this.config
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Update configuration (requires reinitialization)
|
|
659
|
-
* @param options - New RedLock options
|
|
660
|
-
*/
|
|
661
|
-
updateConfig(options) {
|
|
662
|
-
if (options) {
|
|
663
|
-
this.config = {
|
|
664
|
-
...this.config,
|
|
665
|
-
...options
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
this.isInitialized = false;
|
|
669
|
-
this.initializationPromise = null;
|
|
670
|
-
this.redlock = null;
|
|
671
|
-
DefaultLogger.Debug("RedLocker configuration updated, will reinitialize on next use");
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Close Redis connection and cleanup
|
|
675
|
-
*/
|
|
676
|
-
async close() {
|
|
677
|
-
try {
|
|
678
|
-
if (this.redisClient && this.redisClient.status === "ready") {
|
|
679
|
-
await this.redisClient.quit();
|
|
680
|
-
DefaultLogger.Debug("Redis connection closed");
|
|
726
|
+
/**
|
|
727
|
+
* Get current configuration
|
|
728
|
+
* @returns Current RedLock configuration
|
|
729
|
+
*/
|
|
730
|
+
getConfig() {
|
|
731
|
+
return {
|
|
732
|
+
...this.config
|
|
733
|
+
};
|
|
681
734
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
*/
|
|
693
|
-
getContainerInfo() {
|
|
694
|
-
try {
|
|
695
|
-
const instance = IOCContainer.get("RedLocker", "COMPONENT");
|
|
696
|
-
return {
|
|
697
|
-
registered: !!instance,
|
|
698
|
-
identifier: "RedLocker"
|
|
699
|
-
};
|
|
700
|
-
} catch {
|
|
701
|
-
return {
|
|
702
|
-
registered: false,
|
|
703
|
-
identifier: "RedLocker"
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Health check for RedLocker
|
|
709
|
-
* @returns Health status
|
|
710
|
-
*/
|
|
711
|
-
async healthCheck() {
|
|
712
|
-
try {
|
|
713
|
-
await this.initialize();
|
|
714
|
-
const redisStatus = this.redisClient?.status || "unknown";
|
|
715
|
-
const isReady = this.isReady();
|
|
716
|
-
return {
|
|
717
|
-
status: isReady ? "healthy" : "unhealthy",
|
|
718
|
-
details: {
|
|
719
|
-
initialized: this.isInitialized,
|
|
720
|
-
redisStatus,
|
|
721
|
-
redisMode: this.config.redisConfig?.mode || "unknown",
|
|
722
|
-
redlockReady: !!this.redlock,
|
|
723
|
-
containerRegistered: this.getContainerInfo().registered
|
|
735
|
+
/**
|
|
736
|
+
* Update configuration (requires reinitialization)
|
|
737
|
+
* @param options - New RedLock options
|
|
738
|
+
*/
|
|
739
|
+
updateConfig(options) {
|
|
740
|
+
if (options) {
|
|
741
|
+
this.config = {
|
|
742
|
+
...this.config,
|
|
743
|
+
...options
|
|
744
|
+
};
|
|
724
745
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
746
|
+
this.isInitialized = false;
|
|
747
|
+
this.initializationPromise = null;
|
|
748
|
+
this.redlock = null;
|
|
749
|
+
DefaultLogger.Debug("RedLocker configuration updated, will reinitialize on next use");
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Close Redis connection and cleanup
|
|
753
|
+
*/
|
|
754
|
+
async close() {
|
|
755
|
+
try {
|
|
756
|
+
if (this.redisClient && this.redisClient.status === "ready") {
|
|
757
|
+
await this.redisClient.quit();
|
|
758
|
+
DefaultLogger.Debug("Redis connection closed");
|
|
759
|
+
}
|
|
760
|
+
this.redisClient = null;
|
|
761
|
+
this.redlock = null;
|
|
762
|
+
this.isInitialized = false;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
DefaultLogger.Error("Error closing RedLocker:", error);
|
|
732
765
|
}
|
|
733
|
-
}
|
|
734
|
-
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Get container registration status
|
|
769
|
+
* @returns Registration information
|
|
770
|
+
*/
|
|
771
|
+
getContainerInfo() {
|
|
772
|
+
try {
|
|
773
|
+
const instance = IOCContainer.get("RedLocker", "COMPONENT");
|
|
774
|
+
return {
|
|
775
|
+
registered: !!instance,
|
|
776
|
+
identifier: "RedLocker"
|
|
777
|
+
};
|
|
778
|
+
} catch {
|
|
779
|
+
return {
|
|
780
|
+
registered: false,
|
|
781
|
+
identifier: "RedLocker"
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Health check for RedLocker
|
|
787
|
+
* @returns Health status
|
|
788
|
+
*/
|
|
789
|
+
async healthCheck() {
|
|
790
|
+
try {
|
|
791
|
+
await this.initialize();
|
|
792
|
+
const redisStatus = this.redisClient?.status || "unknown";
|
|
793
|
+
const isReady = this.isReady();
|
|
794
|
+
return {
|
|
795
|
+
status: isReady ? "healthy" : "unhealthy",
|
|
796
|
+
details: {
|
|
797
|
+
initialized: this.isInitialized,
|
|
798
|
+
redisStatus,
|
|
799
|
+
redisMode: this.config.redisConfig?.mode || "unknown",
|
|
800
|
+
redlockReady: !!this.redlock,
|
|
801
|
+
containerRegistered: this.getContainerInfo().registered
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
} catch (error) {
|
|
805
|
+
return {
|
|
806
|
+
status: "unhealthy",
|
|
807
|
+
details: {
|
|
808
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
809
|
+
initialized: this.isInitialized
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
};
|
|
735
815
|
}
|
|
736
|
-
};
|
|
816
|
+
});
|
|
737
817
|
|
|
738
818
|
// src/utils/lib.ts
|
|
819
|
+
var lib_exports = {};
|
|
820
|
+
__export(lib_exports, {
|
|
821
|
+
timeoutPromise: () => timeoutPromise,
|
|
822
|
+
wrappedPromise: () => wrappedPromise
|
|
823
|
+
});
|
|
739
824
|
function timeoutPromise(ms) {
|
|
740
825
|
let timeoutId = null;
|
|
741
826
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -752,9 +837,30 @@ function timeoutPromise(ms) {
|
|
|
752
837
|
};
|
|
753
838
|
return promise;
|
|
754
839
|
}
|
|
755
|
-
|
|
840
|
+
function wrappedPromise(fn, args) {
|
|
841
|
+
return new Promise((resolve, reject) => {
|
|
842
|
+
try {
|
|
843
|
+
const result = fn(...args);
|
|
844
|
+
resolve(result);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
reject(error);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
var init_lib = __esm({
|
|
851
|
+
"src/utils/lib.ts"() {
|
|
852
|
+
__name(timeoutPromise, "timeoutPromise");
|
|
853
|
+
__name(wrappedPromise, "wrappedPromise");
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// src/decorator/redlock.ts
|
|
858
|
+
init_config();
|
|
756
859
|
|
|
757
860
|
// src/process/locker.ts
|
|
861
|
+
init_redlock();
|
|
862
|
+
init_lib();
|
|
863
|
+
init_config();
|
|
758
864
|
async function initRedLock(options, app) {
|
|
759
865
|
if (!app || !Helper.isFunction(app.once)) {
|
|
760
866
|
DefaultLogger.Warn(`RedLock initialization skipped: Koatty app not available or not initialized`);
|
|
@@ -878,33 +984,89 @@ __name(generateLockName, "generateLockName");
|
|
|
878
984
|
|
|
879
985
|
// src/decorator/redlock.ts
|
|
880
986
|
function RedLock(lockName, options) {
|
|
881
|
-
return (target,
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
987
|
+
return IOCContainer.createDecorator(({ target, methodName, descriptor, method, context }) => {
|
|
988
|
+
if (context) {
|
|
989
|
+
if (!methodName || typeof methodName !== "string") {
|
|
990
|
+
throw Error("Method name is required for @RedLock decorator");
|
|
991
|
+
}
|
|
992
|
+
if (options) {
|
|
993
|
+
validateRedLockMethodOptions(options);
|
|
994
|
+
}
|
|
995
|
+
context.addInitializer?.(function() {
|
|
996
|
+
const targetClass = this.constructor;
|
|
997
|
+
const componentType = IOCContainer.getType(targetClass);
|
|
998
|
+
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
|
|
999
|
+
throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes.");
|
|
1000
|
+
}
|
|
1001
|
+
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
|
|
1002
|
+
});
|
|
1003
|
+
const originalMethod = method;
|
|
1004
|
+
return async function(...props) {
|
|
1005
|
+
try {
|
|
1006
|
+
const { RedLocker: RedLocker2 } = await Promise.resolve().then(() => (init_redlock(), redlock_exports));
|
|
1007
|
+
const { getEffectiveRedLockOptions: getEffectiveRedLockOptions2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
1008
|
+
const { timeoutPromise: timeoutPromise2 } = await Promise.resolve().then(() => (init_lib(), lib_exports));
|
|
1009
|
+
const { Lock } = await import('@sesamecare-oss/redlock');
|
|
1010
|
+
const resolvedLockName = lockName || generateLockName(lockName, methodName, Object.getPrototypeOf(this));
|
|
1011
|
+
const redlock = RedLocker2.getInstance();
|
|
1012
|
+
const lockOptions = getEffectiveRedLockOptions2(options);
|
|
1013
|
+
const lockTime = lockOptions.lockTimeOut || 1e4;
|
|
1014
|
+
if (lockTime <= 200) {
|
|
1015
|
+
throw new Error("Lock timeout must be greater than 200ms to allow for proper execution");
|
|
1016
|
+
}
|
|
1017
|
+
const lock = await redlock.acquire([
|
|
1018
|
+
methodName,
|
|
1019
|
+
resolvedLockName
|
|
1020
|
+
], lockTime);
|
|
1021
|
+
const timeout = lockTime - 200;
|
|
1022
|
+
try {
|
|
1023
|
+
const result = await Promise.race([
|
|
1024
|
+
originalMethod.apply(this, props),
|
|
1025
|
+
timeoutPromise2(timeout)
|
|
1026
|
+
]);
|
|
1027
|
+
return result;
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
throw error;
|
|
1030
|
+
} finally {
|
|
1031
|
+
try {
|
|
1032
|
+
await lock.release();
|
|
1033
|
+
} catch (releaseError) {
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
} else {
|
|
1041
|
+
const targetClass = target.constructor;
|
|
1042
|
+
const componentType = IOCContainer.getType(targetClass);
|
|
1043
|
+
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
|
|
1044
|
+
throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes.");
|
|
1045
|
+
}
|
|
1046
|
+
if (!methodName || typeof methodName !== "string") {
|
|
1047
|
+
throw Error("Method name is required for @RedLock decorator");
|
|
1048
|
+
}
|
|
1049
|
+
if (!descriptor || typeof descriptor.value !== "function") {
|
|
1050
|
+
throw Error("@RedLock decorator can only be applied to methods");
|
|
1051
|
+
}
|
|
1052
|
+
const finalLockName = lockName || generateLockName(lockName, methodName, target);
|
|
1053
|
+
if (options) {
|
|
1054
|
+
validateRedLockMethodOptions(options);
|
|
1055
|
+
}
|
|
1056
|
+
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
|
|
1057
|
+
try {
|
|
1058
|
+
const enhancedDescriptor = redLockerDescriptor(descriptor, finalLockName, methodName, options);
|
|
1059
|
+
return enhancedDescriptor;
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
throw new Error(`Failed to apply RedLock to ${methodName}: ${error.message}`);
|
|
1062
|
+
}
|
|
904
1063
|
}
|
|
905
|
-
};
|
|
1064
|
+
}, "method");
|
|
906
1065
|
}
|
|
907
1066
|
__name(RedLock, "RedLock");
|
|
1067
|
+
|
|
1068
|
+
// src/decorator/scheduled.ts
|
|
1069
|
+
init_config();
|
|
908
1070
|
function Scheduled(cron, timezone = "Asia/Beijing") {
|
|
909
1071
|
if (Helper.isEmpty(cron)) {
|
|
910
1072
|
throw Error("Cron expression is required and cannot be empty");
|
|
@@ -917,28 +1079,50 @@ function Scheduled(cron, timezone = "Asia/Beijing") {
|
|
|
917
1079
|
if (timezone && typeof timezone !== "string") {
|
|
918
1080
|
throw Error("Timezone must be a string");
|
|
919
1081
|
}
|
|
920
|
-
return (target,
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1082
|
+
return IOCContainer.createDecorator(({ target, methodName, descriptor, method, context }) => {
|
|
1083
|
+
if (context) {
|
|
1084
|
+
context.addInitializer?.(function() {
|
|
1085
|
+
const targetClass = this.constructor;
|
|
1086
|
+
const componentType = IOCContainer.getType(targetClass);
|
|
1087
|
+
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
|
|
1088
|
+
throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes.");
|
|
1089
|
+
}
|
|
1090
|
+
if (!methodName || typeof methodName !== "string") {
|
|
1091
|
+
throw Error("Method name is required for @Scheduled decorator");
|
|
1092
|
+
}
|
|
1093
|
+
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
|
|
1094
|
+
IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
|
|
1095
|
+
method: methodName,
|
|
1096
|
+
cron,
|
|
1097
|
+
timezone
|
|
1098
|
+
}, this, methodName);
|
|
1099
|
+
});
|
|
1100
|
+
return method;
|
|
1101
|
+
} else {
|
|
1102
|
+
const targetClass = target.constructor;
|
|
1103
|
+
const componentType = IOCContainer.getType(targetClass);
|
|
1104
|
+
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
|
|
1105
|
+
throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes.");
|
|
1106
|
+
}
|
|
1107
|
+
if (!methodName || typeof methodName !== "string") {
|
|
1108
|
+
throw Error("Method name is required for @Scheduled decorator");
|
|
1109
|
+
}
|
|
1110
|
+
if (!descriptor || typeof descriptor.value !== "function") {
|
|
1111
|
+
throw Error("@Scheduled decorator can only be applied to methods");
|
|
1112
|
+
}
|
|
1113
|
+
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
|
|
1114
|
+
IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
|
|
1115
|
+
method: methodName,
|
|
1116
|
+
cron,
|
|
1117
|
+
timezone
|
|
1118
|
+
}, target, methodName);
|
|
932
1119
|
}
|
|
933
|
-
|
|
934
|
-
IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
|
|
935
|
-
method: methodName,
|
|
936
|
-
cron,
|
|
937
|
-
timezone
|
|
938
|
-
}, target, methodName);
|
|
939
|
-
};
|
|
1120
|
+
}, "method");
|
|
940
1121
|
}
|
|
941
1122
|
__name(Scheduled, "Scheduled");
|
|
1123
|
+
|
|
1124
|
+
// src/process/schedule.ts
|
|
1125
|
+
init_config();
|
|
942
1126
|
async function initSchedule(options, app) {
|
|
943
1127
|
if (!app || !Helper.isFunction(app.once)) {
|
|
944
1128
|
DefaultLogger.Warn(`Schedule initialization skipped: Koatty app not available or not initialized`);
|
|
@@ -956,54 +1140,45 @@ __name(initSchedule, "initSchedule");
|
|
|
956
1140
|
async function injectSchedule(options) {
|
|
957
1141
|
try {
|
|
958
1142
|
DefaultLogger.Debug("Starting batch schedule injection...");
|
|
1143
|
+
let totalScheduled = 0;
|
|
959
1144
|
const componentList = IOCContainer.listClass("COMPONENT");
|
|
960
1145
|
for (const component of componentList) {
|
|
961
1146
|
const classMetadata = IOCContainer.getClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, component.target);
|
|
962
|
-
if (!classMetadata) {
|
|
1147
|
+
if (!classMetadata || !Array.isArray(classMetadata)) {
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const instance = IOCContainer.get(component.id);
|
|
1151
|
+
if (!instance) {
|
|
963
1152
|
continue;
|
|
964
1153
|
}
|
|
965
|
-
|
|
966
|
-
for (const [className, metadata] of classMetadata) {
|
|
1154
|
+
for (const scheduleData of classMetadata) {
|
|
967
1155
|
try {
|
|
968
|
-
|
|
969
|
-
if (!instance) {
|
|
1156
|
+
if (!scheduleData || !scheduleData.method) {
|
|
970
1157
|
continue;
|
|
971
1158
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
if (!Helper.isFunction(targetMethod)) {
|
|
977
|
-
DefaultLogger.Warn(`Schedule injection skipped: method ${scheduleData.method} is not a function in ${className}`);
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
const taskName = `${className}_${scheduleData.method}`;
|
|
981
|
-
const tz = getEffectiveTimezone(options, scheduleData.timezone);
|
|
982
|
-
new CronJob(
|
|
983
|
-
scheduleData.cron,
|
|
984
|
-
() => {
|
|
985
|
-
DefaultLogger.Debug(`The schedule job ${taskName} started.`);
|
|
986
|
-
Promise.resolve(targetMethod.call(instance)).then(() => {
|
|
987
|
-
DefaultLogger.Debug(`The schedule job ${taskName} completed.`);
|
|
988
|
-
}).catch((error) => {
|
|
989
|
-
DefaultLogger.Error(`The schedule job ${taskName} failed:`, error);
|
|
990
|
-
});
|
|
991
|
-
},
|
|
992
|
-
null,
|
|
993
|
-
true,
|
|
994
|
-
tz
|
|
995
|
-
// timeZone
|
|
996
|
-
);
|
|
997
|
-
scheduledCount++;
|
|
998
|
-
DefaultLogger.Debug(`Schedule job ${taskName} registered with cron: ${scheduleData.cron}`);
|
|
999
|
-
}
|
|
1159
|
+
const targetMethod = instance[scheduleData.method];
|
|
1160
|
+
if (!Helper.isFunction(targetMethod)) {
|
|
1161
|
+
DefaultLogger.Warn(`Schedule injection skipped: method ${scheduleData.method} is not a function in ${component.id}`);
|
|
1162
|
+
continue;
|
|
1000
1163
|
}
|
|
1164
|
+
const taskName = `${component.id}_${scheduleData.method}`;
|
|
1165
|
+
const tz = getEffectiveTimezone(options, scheduleData.timezone);
|
|
1166
|
+
new CronJob(scheduleData.cron, () => {
|
|
1167
|
+
DefaultLogger.Debug(`The schedule job ${taskName} started.`);
|
|
1168
|
+
Promise.resolve(targetMethod.call(instance)).then(() => {
|
|
1169
|
+
DefaultLogger.Debug(`The schedule job ${taskName} completed.`);
|
|
1170
|
+
}).catch((error) => {
|
|
1171
|
+
DefaultLogger.Error(`The schedule job ${taskName} failed:`, error);
|
|
1172
|
+
});
|
|
1173
|
+
}, null, true, tz);
|
|
1174
|
+
totalScheduled++;
|
|
1175
|
+
DefaultLogger.Debug(`Schedule job ${taskName} registered with cron: ${scheduleData.cron}`);
|
|
1001
1176
|
} catch (error) {
|
|
1002
|
-
DefaultLogger.Error(`Failed to process
|
|
1177
|
+
DefaultLogger.Error(`Failed to process schedule for ${component.id}:`, error);
|
|
1003
1178
|
}
|
|
1004
1179
|
}
|
|
1005
|
-
DefaultLogger.Info(`Batch schedule injection completed. ${scheduledCount} jobs registered.`);
|
|
1006
1180
|
}
|
|
1181
|
+
DefaultLogger.Info(`Batch schedule injection completed. ${totalScheduled} jobs registered.`);
|
|
1007
1182
|
} catch (error) {
|
|
1008
1183
|
DefaultLogger.Error("Failed to inject schedules:", error);
|
|
1009
1184
|
}
|
|
@@ -1011,6 +1186,10 @@ async function injectSchedule(options) {
|
|
|
1011
1186
|
__name(injectSchedule, "injectSchedule");
|
|
1012
1187
|
|
|
1013
1188
|
// src/index.ts
|
|
1189
|
+
init_interface();
|
|
1190
|
+
init_redlock();
|
|
1191
|
+
init_interface();
|
|
1192
|
+
init_redis_factory();
|
|
1014
1193
|
var SchedulerLock = RedLock;
|
|
1015
1194
|
var defaultOptions = {
|
|
1016
1195
|
timezone: "Asia/Beijing",
|