koatty_schedule
Version:
Schedule for koatty.
1,223 lines (1,211 loc) • 43.6 kB
JavaScript
import Redis, { Cluster } from 'ioredis';
import { DefaultLogger } from 'koatty_logger';
import { Redlock } from '@sesamecare-oss/redlock';
import { IOCContainer } from 'koatty_container';
import { Helper } from 'koatty_lib';
import { CronJob } from 'cron';
/*!
* @Author: richen
* @Date: 2026-04-24 08:20:32
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
* @HomePage: https://koatty.org/
*/
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/config/config.ts
var config_exports = {};
__export(config_exports, {
COMPONENT_REDLOCK: () => COMPONENT_REDLOCK,
COMPONENT_SCHEDULED: () => COMPONENT_SCHEDULED,
DecoratorType: () => DecoratorType,
getEffectiveRedLockOptions: () => getEffectiveRedLockOptions,
getEffectiveTimezone: () => getEffectiveTimezone,
getGlobalScheduledOptions: () => getGlobalScheduledOptions,
setGlobalScheduledOptions: () => setGlobalScheduledOptions,
validateCronExpression: () => validateCronExpression,
validateRedLockMethodOptions: () => validateRedLockMethodOptions,
validateRedLockOptions: () => validateRedLockOptions
});
function validateCronExpression(cron) {
if (!cron || typeof cron !== "string") {
throw new Error("Cron expression must be a non-empty string");
}
const cronParts = cron.trim().split(/\s+/);
if (cronParts.length < 5 || cronParts.length > 6) {
throw new Error(`Invalid cron format. Expected 5 or 6 parts, got ${cronParts.length}`);
}
const hasSecs = cronParts.length === 6;
const offset = hasSecs ? 0 : -1;
const seconds = hasSecs ? cronParts[0] : null;
const minutes = cronParts[offset + 1];
const hours = cronParts[offset + 2];
const dayOfMonth = cronParts[offset + 3];
const month = cronParts[offset + 4];
const dayOfWeek = cronParts[offset + 5];
if (seconds !== null) {
validateCronField(seconds, 0, 59, "seconds", "\u79D2");
}
validateCronField(minutes, 0, 59, "minutes", "\u5206\u949F");
validateCronField(hours, 0, 23, "hours", "\u5C0F\u65F6");
validateCronField(dayOfMonth, 1, 31, "day of month", "\u65E5\u671F");
validateCronField(month, 1, 12, "month", "\u6708\u4EFD", [
"JAN",
"FEB",
"MAR",
"APR",
"MAY",
"JUN",
"JUL",
"AUG",
"SEP",
"OCT",
"NOV",
"DEC"
]);
validateCronField(dayOfWeek, 0, 7, "day of week", "\u661F\u671F", [
"SUN",
"MON",
"TUE",
"WED",
"THU",
"FRI",
"SAT"
]);
}
function validateCronField(field, min, max, fieldName, fieldNameCN, allowedStrings) {
if (field === "*") {
return;
}
if (field === "?") {
return;
}
if (allowedStrings && allowedStrings.some((str) => field.toUpperCase().includes(str))) {
return;
}
if (field.includes("/")) {
const [range, step] = field.split("/");
const stepValue = parseInt(step);
if (isNaN(stepValue) || stepValue <= 0) {
throw new Error(`Invalid step value for ${fieldName}: ${step}`);
}
if (range !== "*") {
validateCronField(range, min, max, fieldName, fieldNameCN, allowedStrings);
}
return;
}
if (field.includes("-")) {
const [start, end] = field.split("-");
const startValue = parseInt(start);
const endValue = parseInt(end);
if (isNaN(startValue) || startValue < min || startValue > max) {
throw new Error(`Invalid range start for ${fieldName}: ${start}, must be between ${min}-${max}`);
}
if (isNaN(endValue) || endValue < min || endValue > max) {
throw new Error(`Invalid range end for ${fieldName}: ${end}, must be between ${min}-${max}`);
}
if (startValue > endValue) {
throw new Error(`Invalid range for ${fieldName}: ${start}-${end}, start cannot be greater than end`);
}
return;
}
if (field.includes(",")) {
const values = field.split(",");
for (const value of values) {
validateCronField(value.trim(), min, max, fieldName, fieldNameCN, allowedStrings);
}
return;
}
const numValue = parseInt(field);
if (isNaN(numValue) || numValue < min || numValue > max) {
throw new Error(`Invalid ${fieldName} value: ${field}, must be between ${min}-${max}`);
}
}
function validateRedLockMethodOptions(options) {
if (!options || typeof options !== "object") {
throw new Error("RedLock method options must be an object");
}
if (options.lockTimeOut !== void 0) {
if (typeof options.lockTimeOut !== "number" || options.lockTimeOut <= 0) {
throw new Error("lockTimeOut must be a positive number");
}
}
if (options.clockDriftFactor !== void 0) {
if (typeof options.clockDriftFactor !== "number" || options.clockDriftFactor < 0 || options.clockDriftFactor > 1) {
throw new Error("clockDriftFactor must be a number between 0 and 1");
}
}
if (options.maxRetries !== void 0) {
if (typeof options.maxRetries !== "number" || options.maxRetries < 0) {
throw new Error("maxRetries must be a non-negative number");
}
}
if (options.retryDelayMs !== void 0) {
if (typeof options.retryDelayMs !== "number" || options.retryDelayMs < 0) {
throw new Error("retryDelayMs must be a non-negative number");
}
}
}
function validateRedLockOptions(options) {
if (!options || typeof options !== "object") {
throw new Error("RedLock options must be an object");
}
if (options.lockTimeOut !== void 0) {
if (typeof options.lockTimeOut !== "number" || options.lockTimeOut <= 0) {
throw new Error("lockTimeOut must be a positive number");
}
}
if (options.retryCount !== void 0) {
if (typeof options.retryCount !== "number" || options.retryCount < 0) {
throw new Error("retryCount must be a non-negative number");
}
}
if (options.retryDelay !== void 0) {
if (typeof options.retryDelay !== "number" || options.retryDelay < 0) {
throw new Error("retryDelay must be a non-negative number");
}
}
if (options.retryJitter !== void 0) {
if (typeof options.retryJitter !== "number" || options.retryJitter < 0) {
throw new Error("retryJitter must be a non-negative number");
}
}
}
function setGlobalScheduledOptions(options) {
globalScheduledOptions = {
...options
};
}
function getGlobalScheduledOptions() {
return globalScheduledOptions;
}
function getEffectiveTimezone(options, userTimezone) {
return userTimezone || options.timezone || "Asia/Beijing";
}
function getEffectiveRedLockOptions(methodOptions) {
const globalOptions = getGlobalScheduledOptions();
return {
lockTimeOut: methodOptions?.lockTimeOut || globalOptions.lockTimeOut || 1e4,
clockDriftFactor: methodOptions?.clockDriftFactor || globalOptions.clockDriftFactor || 0.01,
maxRetries: methodOptions?.maxRetries || globalOptions.maxRetries || 3,
retryDelayMs: methodOptions?.retryDelayMs || globalOptions.retryDelayMs || 200
};
}
var COMPONENT_SCHEDULED, COMPONENT_REDLOCK, DecoratorType, globalScheduledOptions;
var init_config = __esm({
"src/config/config.ts"() {
COMPONENT_SCHEDULED = "COMPONENT_SCHEDULED";
COMPONENT_REDLOCK = "COMPONENT_REDLOCK";
DecoratorType = /* @__PURE__ */ (function(DecoratorType2) {
DecoratorType2["SCHEDULED"] = "SCHEDULED";
DecoratorType2["REDLOCK"] = "REDLOCK";
return DecoratorType2;
})({});
__name(validateCronExpression, "validateCronExpression");
__name(validateCronField, "validateCronField");
__name(validateRedLockMethodOptions, "validateRedLockMethodOptions");
__name(validateRedLockOptions, "validateRedLockOptions");
globalScheduledOptions = {};
__name(setGlobalScheduledOptions, "setGlobalScheduledOptions");
__name(getGlobalScheduledOptions, "getGlobalScheduledOptions");
__name(getEffectiveTimezone, "getEffectiveTimezone");
__name(getEffectiveRedLockOptions, "getEffectiveRedLockOptions");
}
});
// src/locker/interface.ts
var RedisMode;
var init_interface = __esm({
"src/locker/interface.ts"() {
RedisMode = /* @__PURE__ */ (function(RedisMode2) {
RedisMode2["STANDALONE"] = "standalone";
RedisMode2["SENTINEL"] = "sentinel";
RedisMode2["CLUSTER"] = "cluster";
return RedisMode2;
})({});
}
});
var RedisClientAdapter, RedisFactory;
var init_redis_factory = __esm({
"src/locker/redis-factory.ts"() {
init_interface();
RedisClientAdapter = class RedisClientAdapter2 {
static {
__name(this, "RedisClientAdapter");
}
client;
constructor(client) {
this.client = client;
}
get status() {
return this.client.status;
}
async call(command, ...args) {
return this.client.call(command, ...args);
}
async set(key, value, mode, duration) {
if (mode && duration) {
return this.client.set(key, value, mode, duration);
}
return this.client.set(key, value);
}
async get(key) {
return this.client.get(key);
}
async del(...keys) {
return this.client.del(...keys);
}
async exists(key) {
return this.client.exists(key);
}
async eval(script, numKeys, ...args) {
return this.client.eval(script, numKeys, ...args);
}
async quit() {
return this.client.quit();
}
disconnect() {
this.client.disconnect();
}
/**
* Get underlying Redis/Cluster instance
* Used for RedLock initialization
*/
getClient() {
return this.client;
}
};
RedisFactory = class {
static {
__name(this, "RedisFactory");
}
/**
* Create Redis client based on configuration mode
* @param config - Redis configuration
* @returns Redis client adapter
*/
static createClient(config) {
const mode = config.mode || RedisMode.STANDALONE;
DefaultLogger.Debug(`Creating Redis client in ${mode} mode`);
switch (mode) {
case RedisMode.STANDALONE:
return this.createStandaloneClient(config);
case RedisMode.SENTINEL:
return this.createSentinelClient(config);
case RedisMode.CLUSTER:
return this.createClusterClient(config);
default:
throw new Error(`Unsupported Redis mode: ${mode}`);
}
}
/**
* Create standalone Redis client
* @param config - Standalone configuration
*/
static createStandaloneClient(config) {
DefaultLogger.Debug(`Creating standalone Redis client: ${config.host}:${config.port}`);
const options = {
host: config.host,
port: config.port,
password: config.password || void 0,
db: config.db || 0,
keyPrefix: config.keyPrefix || "",
connectTimeout: config.connectTimeout || 1e4,
commandTimeout: config.commandTimeout || 5e3,
maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
retryStrategy: /* @__PURE__ */ __name((times) => {
const delay = Math.min(times * 50, 2e3);
DefaultLogger.Debug(`Redis reconnecting, attempt ${times}, delay ${delay}ms`);
return delay;
}, "retryStrategy"),
reconnectOnError: /* @__PURE__ */ __name((err) => {
DefaultLogger.Warn("Redis connection error, attempting reconnect:", err.message);
return true;
}, "reconnectOnError")
};
const client = new Redis(options);
client.on("connect", () => {
DefaultLogger.Info("Redis standalone client connected successfully");
});
client.on("error", (err) => {
DefaultLogger.Error("Redis standalone client error:", err);
});
return new RedisClientAdapter(client);
}
/**
* Create sentinel Redis client
* @param config - Sentinel configuration
*/
static createSentinelClient(config) {
DefaultLogger.Debug(`Creating sentinel Redis client for master: ${config.name}`);
const options = {
sentinels: config.sentinels,
name: config.name,
password: config.password || void 0,
sentinelPassword: config.sentinelPassword || void 0,
db: config.db || 0,
keyPrefix: config.keyPrefix || "",
connectTimeout: config.connectTimeout || 1e4,
commandTimeout: config.commandTimeout || 5e3,
maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
retryStrategy: /* @__PURE__ */ __name((times) => {
const delay = Math.min(times * 50, 2e3);
DefaultLogger.Debug(`Sentinel Redis reconnecting, attempt ${times}, delay ${delay}ms`);
return delay;
}, "retryStrategy")
};
const client = new Redis(options);
client.on("connect", () => {
DefaultLogger.Info(`Redis sentinel client connected to master: ${config.name}`);
});
client.on("error", (err) => {
DefaultLogger.Error("Redis sentinel client error:", err);
});
return new RedisClientAdapter(client);
}
/**
* Create cluster Redis client
* @param config - Cluster configuration
*/
static createClusterClient(config) {
DefaultLogger.Debug(`Creating cluster Redis client with ${config.nodes.length} nodes`);
const clusterOptions = {
redisOptions: {
password: config.redisOptions?.password || config.password || void 0,
db: config.redisOptions?.db || config.db || 0,
keyPrefix: config.keyPrefix || "",
connectTimeout: config.connectTimeout || 1e4,
commandTimeout: config.commandTimeout || 5e3,
maxRetriesPerRequest: config.maxRetriesPerRequest || 3
},
clusterRetryStrategy: /* @__PURE__ */ __name((times) => {
const delay = Math.min(times * 50, 2e3);
DefaultLogger.Debug(`Cluster Redis reconnecting, attempt ${times}, delay ${delay}ms`);
return delay;
}, "clusterRetryStrategy")
};
const cluster = new Cluster(config.nodes, clusterOptions);
cluster.on("connect", () => {
DefaultLogger.Info("Redis cluster client connected successfully");
});
cluster.on("error", (err) => {
DefaultLogger.Error("Redis cluster client error:", err);
});
cluster.on("node error", (err, address) => {
DefaultLogger.Error(`Redis cluster node error at ${address}:`, err);
});
return new RedisClientAdapter(cluster);
}
/**
* Validate Redis configuration
* @param config - Redis configuration to validate
*/
static validateConfig(config) {
if (!config) {
throw new Error("Redis configuration cannot be empty");
}
const mode = config.mode || RedisMode.STANDALONE;
switch (mode) {
case RedisMode.STANDALONE:
this.validateStandaloneConfig(config);
break;
case RedisMode.SENTINEL:
this.validateSentinelConfig(config);
break;
case RedisMode.CLUSTER:
this.validateClusterConfig(config);
break;
default:
throw new Error(`Unsupported Redis mode: ${mode}`);
}
}
static validateStandaloneConfig(config) {
if (!config.host) {
throw new Error("Standalone mode requires host configuration");
}
if (!config.port) {
throw new Error("Standalone mode requires port configuration");
}
}
static validateSentinelConfig(config) {
if (!config.sentinels || config.sentinels.length === 0) {
throw new Error("Sentinel mode requires at least one sentinel node");
}
if (!config.name) {
throw new Error("Sentinel mode requires master name");
}
}
static validateClusterConfig(config) {
if (!config.nodes || config.nodes.length === 0) {
throw new Error("Cluster mode requires at least one node");
}
}
};
}
});
// src/locker/redlock.ts
var redlock_exports = {};
__export(redlock_exports, {
RedLocker: () => RedLocker
});
var defaultRedLockConfig, defaultRedlockSettings, RedLocker;
var init_redlock = __esm({
"src/locker/redlock.ts"() {
init_interface();
init_redis_factory();
defaultRedLockConfig = {
lockTimeOut: 1e4,
clockDriftFactor: 0.01,
maxRetries: 3,
retryDelayMs: 200,
redisConfig: {
mode: RedisMode.STANDALONE,
host: "127.0.0.1",
port: 6379,
password: "",
db: 0,
keyPrefix: "redlock:"
}
};
defaultRedlockSettings = {
driftFactor: 0.01,
retryCount: 3,
retryDelay: 200,
retryJitter: 200,
automaticExtensionThreshold: 500
};
RedLocker = class _RedLocker {
static {
__name(this, "RedLocker");
}
static instance = null;
static instanceLock = /* @__PURE__ */ Symbol("RedLocker.instanceLock");
redlock = null;
redisClient = null;
config;
isInitialized = false;
initializationPromise = null;
// 私有构造函数防止外部直接实例化
constructor(options) {
this.config = {
...defaultRedLockConfig,
...options
};
this.registerInContainer();
}
/**
* Register RedLocker in IOC container
* @private
*/
registerInContainer() {
try {
const RedLockerClass = this.constructor;
IOCContainer.saveClass("COMPONENT", RedLockerClass, "RedLocker");
IOCContainer.setExistingInstance(RedLockerClass, this);
DefaultLogger.Debug("RedLocker registered in IOC container");
} catch (_error) {
DefaultLogger.Warn("Failed to register RedLocker in IOC container:", _error);
}
}
/**
* Get RedLocker singleton instance with thread-safe initialization
* @static
* @param options - RedLock configuration options (only used for first initialization)
* @returns RedLocker singleton instance
*/
static getInstance(options) {
if (!_RedLocker.instance) {
if (_RedLocker.instance === null) {
try {
const containerInstance = IOCContainer.get("RedLocker", "COMPONENT");
if (containerInstance) {
_RedLocker.instance = containerInstance;
DefaultLogger.Debug("Retrieved existing RedLocker instance from IOC container");
} else {
_RedLocker.instance = new _RedLocker(options);
DefaultLogger.Debug("Created new RedLocker singleton instance");
}
} catch {
_RedLocker.instance = new _RedLocker(options);
DefaultLogger.Debug("Created new RedLocker instance outside IOC container");
}
}
} else if (options) {
DefaultLogger.Warn("RedLocker instance already exists, ignoring new options. Use updateConfig() to change configuration.");
}
return _RedLocker.instance;
}
/**
* Reset singleton instance (主要用于测试)
* @static
*/
static resetInstance() {
if (_RedLocker.instance) {
_RedLocker.instance.close().catch((err) => DefaultLogger.Warn("Error while closing RedLocker instance during reset:", err));
_RedLocker.instance = null;
}
}
/**
* Initialize RedLock with Redis connection
* Uses cached promise to avoid duplicate initialization
* @private
*/
async initialize() {
if (this.isInitialized) {
return;
}
if (this.initializationPromise) {
return this.initializationPromise;
}
this.initializationPromise = this.performInitialization();
try {
await this.initializationPromise;
} catch (error) {
this.initializationPromise = null;
this.isInitialized = false;
this.redlock = null;
DefaultLogger.Warn("RedLocker initialization failed, state has been reset for retry");
throw error;
}
}
/**
* 执行实际的初始化操作
* @private
*/
async performInitialization() {
try {
if (this.config.redisConfig) {
RedisFactory.validateConfig(this.config.redisConfig);
}
try {
const existingRedis = IOCContainer.get("Redis", "COMPONENT");
if (existingRedis) {
if (existingRedis instanceof RedisClientAdapter) {
this.redisClient = existingRedis;
} else {
this.redisClient = new RedisClientAdapter(existingRedis);
}
DefaultLogger.Debug("Using Redis instance from IOC container");
}
} catch {
}
if (!this.redisClient && this.config.redisConfig) {
this.redisClient = RedisFactory.createClient(this.config.redisConfig);
DefaultLogger.Debug("Created new Redis connection for RedLocker");
}
if (!this.redisClient) {
throw new Error("Failed to initialize Redis connection: no configuration provided");
}
const underlyingClient = this.redisClient.getClient();
const userSettings = this.config;
const redlockSettings = {
...defaultRedlockSettings,
...userSettings.driftFactor !== void 0 && {
driftFactor: userSettings.driftFactor
},
...userSettings.retryCount !== void 0 && {
retryCount: userSettings.retryCount
},
...userSettings.retryDelay !== void 0 && {
retryDelay: userSettings.retryDelay
},
...userSettings.retryJitter !== void 0 && {
retryJitter: userSettings.retryJitter
},
...userSettings.automaticExtensionThreshold !== void 0 && {
automaticExtensionThreshold: userSettings.automaticExtensionThreshold
}
};
this.redlock = new Redlock([
underlyingClient
], redlockSettings);
this.redlock.on("clientError", (err) => {
DefaultLogger.Error("Redis client error in RedLock:", err);
});
this.isInitialized = true;
DefaultLogger.Info("RedLocker initialized successfully");
} catch (error) {
this.isInitialized = false;
DefaultLogger.Error("Failed to initialize RedLocker:", error);
throw new Error(`RedLocker initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Acquire a distributed lock
* @param resources - Resource identifiers to lock
* @param ttl - Time to live in milliseconds
* @returns Promise<Lock>
*/
async acquire(resources, ttl) {
if (!Array.isArray(resources) || resources.length === 0) {
throw new Error("Resources array cannot be empty");
}
const lockTtl = ttl || this.config.lockTimeOut;
if (lockTtl <= 0) {
throw new Error("Lock TTL must be positive");
}
await this.initialize();
if (!this.redlock) {
throw new Error("RedLock is not initialized");
}
try {
const prefixedResources = resources.map((resource) => `${this.config.redisConfig.keyPrefix}${resource}`);
DefaultLogger.Debug(`Acquiring lock for resources: ${prefixedResources.join(", ")} with TTL: ${lockTtl}ms`);
const lock = await this.redlock.acquire(prefixedResources, lockTtl);
DefaultLogger.Debug(`Lock acquired successfully for resources: ${prefixedResources.join(", ")}`);
return lock;
} catch (error) {
DefaultLogger.Error(`Failed to acquire lock for resources: ${resources.join(", ")}`, error);
if (error instanceof Error) {
error.message = `Lock acquisition failed: ${error.message}`;
throw error;
}
throw new Error(`Lock acquisition failed: Unknown error`);
}
}
/**
* Release a lock
* @param lock - Lock instance to release
*/
async release(lock) {
if (!lock) {
throw new Error("Lock instance is required");
}
try {
await lock.release();
DefaultLogger.Debug("Lock released successfully");
} catch (error) {
DefaultLogger.Error("Failed to release lock:", error);
if (error instanceof Error) {
error.message = `Lock release failed: ${error.message}`;
throw error;
}
throw new Error(`Lock release failed: Unknown error`);
}
}
/**
* Extend a lock's TTL
* @param lock - Lock instance to extend
* @param ttl - New TTL in milliseconds
* @returns Extended lock
*/
async extend(lock, ttl) {
if (!lock) {
throw new Error("Lock instance is required");
}
if (ttl <= 0) {
throw new Error("TTL must be positive");
}
try {
const extendedLock = await lock.extend(ttl);
DefaultLogger.Debug(`Lock extended successfully with TTL: ${ttl}ms`);
return extendedLock;
} catch (error) {
DefaultLogger.Error("Failed to extend lock:", error);
throw new Error(`Lock extension failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Check if RedLocker is initialized
* @returns true if initialized, false otherwise
*/
isReady() {
return this.isInitialized && !!this.redlock && !!this.redisClient;
}
/**
* Get current configuration
* @returns Current RedLock configuration
*/
getConfig() {
return {
...this.config
};
}
/**
* Update configuration (requires reinitialization)
* @param options - New RedLock options
*/
updateConfig(options) {
if (options) {
this.config = {
...this.config,
...options
};
}
this.isInitialized = false;
this.initializationPromise = null;
this.redlock = null;
DefaultLogger.Debug("RedLocker configuration updated, will reinitialize on next use");
}
/**
* Close Redis connection and cleanup
*/
async close() {
try {
if (this.redisClient && this.redisClient.status === "ready") {
await this.redisClient.quit();
DefaultLogger.Debug("Redis connection closed");
}
this.redisClient = null;
this.redlock = null;
this.isInitialized = false;
} catch (error) {
DefaultLogger.Error("Error closing RedLocker:", error);
}
}
/**
* Get container registration status
* @returns Registration information
*/
getContainerInfo() {
try {
const instance = IOCContainer.get("RedLocker", "COMPONENT");
return {
registered: !!instance,
identifier: "RedLocker"
};
} catch {
return {
registered: false,
identifier: "RedLocker"
};
}
}
/**
* Health check for RedLocker
* @returns Health status
*/
async healthCheck() {
try {
await this.initialize();
const redisStatus = this.redisClient?.status || "unknown";
const isReady = this.isReady();
return {
status: isReady ? "healthy" : "unhealthy",
details: {
initialized: this.isInitialized,
redisStatus,
redisMode: this.config.redisConfig?.mode || "unknown",
redlockReady: !!this.redlock,
containerRegistered: this.getContainerInfo().registered
}
};
} catch (error) {
return {
status: "unhealthy",
details: {
error: error instanceof Error ? error.message : "Unknown error",
initialized: this.isInitialized
}
};
}
}
};
}
});
// src/utils/lib.ts
var lib_exports = {};
__export(lib_exports, {
timeoutPromise: () => timeoutPromise,
wrappedPromise: () => wrappedPromise
});
function timeoutPromise(ms) {
let timeoutId = null;
const promise = new Promise((resolve, reject) => {
timeoutId = setTimeout(() => {
timeoutId = null;
reject(new Error("TIME_OUT_ERROR"));
}, ms);
});
promise.cancel = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
return promise;
}
function wrappedPromise(fn, args) {
return new Promise((resolve, reject) => {
try {
const result = fn(...args);
resolve(result);
} catch (error) {
reject(error);
}
});
}
var init_lib = __esm({
"src/utils/lib.ts"() {
__name(timeoutPromise, "timeoutPromise");
__name(wrappedPromise, "wrappedPromise");
}
});
// src/decorator/redlock.ts
init_config();
// src/process/locker.ts
init_redlock();
init_lib();
init_config();
async function initRedLock(options, app) {
if (!app || !Helper.isFunction(app.once)) {
DefaultLogger.Warn(`RedLock initialization skipped: Koatty app not available or not initialized`);
return;
}
try {
if (Helper.isEmpty(options)) {
throw Error(`Missing RedLock configuration. Please write a configuration item with the key name 'RedLock' in the db.ts file.`);
}
const redLocker = RedLocker.getInstance(options);
await redLocker.initialize();
DefaultLogger.Info("RedLock initialized successfully");
} catch (error) {
DefaultLogger.Error("Failed to initialize RedLock:", error);
throw error;
}
}
__name(initRedLock, "initRedLock");
function redLockerDescriptor(descriptor, name, method, methodOptions) {
if (!descriptor) {
throw new Error("Property descriptor is required");
}
if (!name || typeof name !== "string") {
throw new Error("Lock name must be a non-empty string");
}
if (!method || typeof method !== "string") {
throw new Error("Method name must be a non-empty string");
}
const { value, configurable, enumerable } = descriptor;
if (typeof value !== "function") {
throw new Error("Descriptor value must be a function");
}
const valueFunction = /* @__PURE__ */ __name(async (self, initialLock, lockTime, timeout, props) => {
let currentLock = initialLock;
let remainingTime = timeout;
const maxExtensions = 3;
let extensionCount = 0;
try {
while (remainingTime > 0 && extensionCount < maxExtensions) {
const timeoutHandler = timeoutPromise(remainingTime);
try {
const result = await Promise.race([
value.apply(self, props),
timeoutHandler
]);
timeoutHandler.cancel();
return result;
} catch (error) {
timeoutHandler.cancel();
if (error instanceof Error && error.message === "TIME_OUT_ERROR") {
extensionCount++;
DefaultLogger.Debug(`Method ${method} execution timeout, attempting lock extension ${extensionCount}/${maxExtensions}`);
try {
currentLock = await currentLock.extend(lockTime);
remainingTime = lockTime - 200;
DefaultLogger.Debug(`Lock extended for method: ${method}, remaining time: ${remainingTime}ms`);
continue;
} catch (extendError) {
DefaultLogger.Error(`Failed to extend lock for method: ${method}`, extendError);
throw new Error(`Lock extension failed: ${extendError instanceof Error ? extendError.message : "Unknown error"}`);
}
} else {
throw error;
}
}
}
throw new Error(`Method ${method} execution timeout after ${extensionCount} lock extensions`);
} finally {
try {
await currentLock.release();
DefaultLogger.Debug(`Lock released for method: ${method}`);
} catch (releaseError) {
DefaultLogger.Warn(`Failed to release lock for method: ${method}`, releaseError);
}
}
}, "valueFunction");
return {
configurable,
enumerable,
writable: true,
async value(...props) {
try {
const redlock = RedLocker.getInstance();
const lockOptions = getEffectiveRedLockOptions(methodOptions);
const lockTime = lockOptions.lockTimeOut || 1e4;
if (lockTime <= 200) {
throw new Error("Lock timeout must be greater than 200ms to allow for proper execution");
}
const lock = await redlock.acquire([
method,
name
], lockTime);
const timeout = lockTime - 200;
DefaultLogger.Debug(`Lock acquired for method: ${method}, timeout: ${timeout}ms`);
return await valueFunction(this, lock, lockTime, timeout, props);
} catch (error) {
DefaultLogger.Error(`RedLock operation failed for method: ${method}`, error);
throw error;
}
}
};
}
__name(redLockerDescriptor, "redLockerDescriptor");
function generateLockName(configName, methodName, target) {
if (configName) {
return configName;
}
try {
const targetObj = target;
const identifier = IOCContainer.getIdentifier(targetObj);
if (identifier) {
return `${identifier}_${methodName}`;
}
} catch {
}
const targetWithConstructor = target;
const className = targetWithConstructor.constructor?.name || "Unknown";
return `${className}_${methodName}`;
}
__name(generateLockName, "generateLockName");
// src/decorator/redlock.ts
function RedLock(lockName, options) {
return IOCContainer.createDecorator(({ target, methodName, descriptor, method, context }) => {
if (context) {
if (!methodName || typeof methodName !== "string") {
throw Error("Method name is required for @RedLock decorator");
}
if (options) {
validateRedLockMethodOptions(options);
}
context.addInitializer?.(function() {
const targetClass = this.constructor;
const componentType = IOCContainer.getType(targetClass);
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes.");
}
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
});
const originalMethod = method;
return async function(...props) {
try {
const { RedLocker: RedLocker2 } = await Promise.resolve().then(() => (init_redlock(), redlock_exports));
const { getEffectiveRedLockOptions: getEffectiveRedLockOptions2 } = await Promise.resolve().then(() => (init_config(), config_exports));
const { timeoutPromise: timeoutPromise2 } = await Promise.resolve().then(() => (init_lib(), lib_exports));
const { Lock } = await import('@sesamecare-oss/redlock');
const resolvedLockName = lockName || generateLockName(lockName, methodName, Object.getPrototypeOf(this));
const redlock = RedLocker2.getInstance();
const lockOptions = getEffectiveRedLockOptions2(options);
const lockTime = lockOptions.lockTimeOut || 1e4;
if (lockTime <= 200) {
throw new Error("Lock timeout must be greater than 200ms to allow for proper execution");
}
const lock = await redlock.acquire([
methodName,
resolvedLockName
], lockTime);
const timeout = lockTime - 200;
try {
const result = await Promise.race([
originalMethod.apply(this, props),
timeoutPromise2(timeout)
]);
return result;
} catch (error) {
throw error;
} finally {
try {
await lock.release();
} catch (releaseError) {
}
}
} catch (error) {
throw error;
}
};
} else {
const targetClass = target.constructor;
const componentType = IOCContainer.getType(targetClass);
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes.");
}
if (!methodName || typeof methodName !== "string") {
throw Error("Method name is required for @RedLock decorator");
}
if (!descriptor || typeof descriptor.value !== "function") {
throw Error("@RedLock decorator can only be applied to methods");
}
const finalLockName = lockName || generateLockName(lockName, methodName, target);
if (options) {
validateRedLockMethodOptions(options);
}
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
try {
const enhancedDescriptor = redLockerDescriptor(descriptor, finalLockName, methodName, options);
return enhancedDescriptor;
} catch (error) {
throw new Error(`Failed to apply RedLock to ${methodName}: ${error.message}`);
}
}
}, "method");
}
__name(RedLock, "RedLock");
// src/decorator/scheduled.ts
init_config();
function Scheduled(cron, timezone = "Asia/Beijing") {
if (Helper.isEmpty(cron)) {
throw Error("Cron expression is required and cannot be empty");
}
try {
validateCronExpression(cron);
} catch (error) {
throw Error(`Invalid cron expression: ${error.message}`);
}
if (timezone && typeof timezone !== "string") {
throw Error("Timezone must be a string");
}
return IOCContainer.createDecorator(({ target, methodName, descriptor, method, context }) => {
if (context) {
context.addInitializer?.(function() {
const targetClass = this.constructor;
const componentType = IOCContainer.getType(targetClass);
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes.");
}
if (!methodName || typeof methodName !== "string") {
throw Error("Method name is required for @Scheduled decorator");
}
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
method: methodName,
cron,
timezone
}, this, methodName);
});
return method;
} else {
const targetClass = target.constructor;
const componentType = IOCContainer.getType(targetClass);
if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes.");
}
if (!methodName || typeof methodName !== "string") {
throw Error("Method name is required for @Scheduled decorator");
}
if (!descriptor || typeof descriptor.value !== "function") {
throw Error("@Scheduled decorator can only be applied to methods");
}
IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name);
IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, {
method: methodName,
cron,
timezone
}, target, methodName);
}
}, "method");
}
__name(Scheduled, "Scheduled");
// src/process/schedule.ts
init_config();
async function initSchedule(options, app) {
if (!app || !Helper.isFunction(app.once)) {
DefaultLogger.Warn(`Schedule initialization skipped: Koatty app not available or not initialized`);
return;
}
try {
await injectSchedule(options);
DefaultLogger.Info("Schedule system initialized successfully");
} catch (error) {
DefaultLogger.Error("Failed to initialize Schedule system:", error);
throw error;
}
}
__name(initSchedule, "initSchedule");
async function injectSchedule(options) {
try {
DefaultLogger.Debug("Starting batch schedule injection...");
let totalScheduled = 0;
const componentList = IOCContainer.listClass("COMPONENT");
for (const component of componentList) {
const classMetadata = IOCContainer.getClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, component.target);
if (!classMetadata || !Array.isArray(classMetadata)) {
continue;
}
const instance = IOCContainer.get(component.id);
if (!instance) {
continue;
}
for (const scheduleData of classMetadata) {
try {
if (!scheduleData || !scheduleData.method) {
continue;
}
const targetMethod = instance[scheduleData.method];
if (!Helper.isFunction(targetMethod)) {
DefaultLogger.Warn(`Schedule injection skipped: method ${scheduleData.method} is not a function in ${component.id}`);
continue;
}
const taskName = `${component.id}_${scheduleData.method}`;
const tz = getEffectiveTimezone(options, scheduleData.timezone);
new CronJob(scheduleData.cron, () => {
DefaultLogger.Debug(`The schedule job ${taskName} started.`);
Promise.resolve(targetMethod.call(instance)).then(() => {
DefaultLogger.Debug(`The schedule job ${taskName} completed.`);
}).catch((error) => {
DefaultLogger.Error(`The schedule job ${taskName} failed:`, error);
});
}, null, true, tz);
totalScheduled++;
DefaultLogger.Debug(`Schedule job ${taskName} registered with cron: ${scheduleData.cron}`);
} catch (error) {
DefaultLogger.Error(`Failed to process schedule for ${component.id}:`, error);
}
}
}
DefaultLogger.Info(`Batch schedule injection completed. ${totalScheduled} jobs registered.`);
} catch (error) {
DefaultLogger.Error("Failed to inject schedules:", error);
}
}
__name(injectSchedule, "injectSchedule");
// src/index.ts
init_interface();
init_redlock();
init_interface();
init_redis_factory();
var SchedulerLock = RedLock;
var defaultOptions = {
timezone: "Asia/Beijing",
lockTimeOut: 1e4,
clockDriftFactor: 0.01,
maxRetries: 3,
retryDelayMs: 200,
redisConfig: {
mode: RedisMode.STANDALONE,
host: "localhost",
port: 6379,
password: "",
db: 0,
keyPrefix: "redlock:"
}
};
async function KoattyScheduled(options, app) {
options = {
...defaultOptions,
...options
};
app.once("appReady", async function() {
await initRedLock(options, app);
await initSchedule(options, app);
});
}
__name(KoattyScheduled, "KoattyScheduled");
export { KoattyScheduled, RedLock, RedLocker, RedisClientAdapter, RedisFactory, RedisMode, Scheduled, SchedulerLock };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map