UNPKG

koatty_schedule

Version:
1,223 lines (1,211 loc) 43.6 kB
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