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