@gravito/horizon 3.0.1 → 3.2.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/README.md +93 -80
- package/README.zh-TW.md +102 -8
- package/dist/index.cjs +603 -140
- package/dist/index.d.cts +563 -188
- package/dist/index.d.ts +563 -188
- package/dist/index.js +603 -140
- package/package.json +7 -5
package/dist/index.cjs
CHANGED
|
@@ -7824,7 +7824,18 @@ module.exports = __toCommonJS(index_exports);
|
|
|
7824
7824
|
// src/SimpleCronParser.ts
|
|
7825
7825
|
var SimpleCronParser = class {
|
|
7826
7826
|
/**
|
|
7827
|
-
*
|
|
7827
|
+
* Evaluates if a cron expression matches the specified date and timezone.
|
|
7828
|
+
*
|
|
7829
|
+
* @param expression - Standard 5-part cron expression.
|
|
7830
|
+
* @param timezone - Target timezone for comparison (default: "UTC").
|
|
7831
|
+
* @param date - Reference date to check (default: now).
|
|
7832
|
+
* @returns True if the expression is due at the given minute.
|
|
7833
|
+
* @throws {Error} If the expression is malformed or the timezone is invalid.
|
|
7834
|
+
*
|
|
7835
|
+
* @example
|
|
7836
|
+
* ```typescript
|
|
7837
|
+
* const isDue = SimpleCronParser.isDue('0 * * * *', 'Asia/Taipei');
|
|
7838
|
+
* ```
|
|
7828
7839
|
*/
|
|
7829
7840
|
static isDue(expression, timezone = "UTC", date = /* @__PURE__ */ new Date()) {
|
|
7830
7841
|
const parts = expression.trim().split(/\s+/);
|
|
@@ -7839,8 +7850,22 @@ var SimpleCronParser = class {
|
|
|
7839
7850
|
const dayOfWeek = targetDate.getDay();
|
|
7840
7851
|
return this.match(parts[0], minutes, 0, 59) && this.match(parts[1], hours, 0, 23) && this.match(parts[2], dayOfMonth, 1, 31) && this.match(parts[3], month, 1, 12) && this.match(parts[4], dayOfWeek, 0, 6, true);
|
|
7841
7852
|
}
|
|
7853
|
+
/**
|
|
7854
|
+
* Internal pattern matching logic for individual cron fields.
|
|
7855
|
+
*
|
|
7856
|
+
* @param pattern - Cron sub-expression (e.g., "1-5").
|
|
7857
|
+
* @param value - Extracted date component value.
|
|
7858
|
+
* @param _min - Field minimum boundary.
|
|
7859
|
+
* @param _max - Field maximum boundary.
|
|
7860
|
+
* @param isDayOfWeek - Special handling for Sunday (0 and 7).
|
|
7861
|
+
* @returns Boolean indicating a match.
|
|
7862
|
+
*
|
|
7863
|
+
* @internal
|
|
7864
|
+
*/
|
|
7842
7865
|
static match(pattern, value, _min, _max, isDayOfWeek = false) {
|
|
7843
|
-
if (pattern === "*")
|
|
7866
|
+
if (pattern === "*") {
|
|
7867
|
+
return true;
|
|
7868
|
+
}
|
|
7844
7869
|
if (pattern.includes(",")) {
|
|
7845
7870
|
return pattern.split(",").some((p) => this.match(p, value, _min, _max, isDayOfWeek));
|
|
7846
7871
|
}
|
|
@@ -7848,7 +7873,9 @@ var SimpleCronParser = class {
|
|
|
7848
7873
|
if (stepMatch) {
|
|
7849
7874
|
const range = stepMatch[1];
|
|
7850
7875
|
const step = parseInt(stepMatch[3], 10);
|
|
7851
|
-
if (range === "*")
|
|
7876
|
+
if (range === "*") {
|
|
7877
|
+
return value % step === 0;
|
|
7878
|
+
}
|
|
7852
7879
|
const [rMin, rMax] = range.split("-").map((n) => parseInt(n, 10));
|
|
7853
7880
|
return value >= rMin && value <= rMax && (value - rMin) % step === 0;
|
|
7854
7881
|
}
|
|
@@ -7857,13 +7884,27 @@ var SimpleCronParser = class {
|
|
|
7857
7884
|
return value >= rMin && value <= rMax;
|
|
7858
7885
|
}
|
|
7859
7886
|
const patternVal = parseInt(pattern, 10);
|
|
7860
|
-
if (isDayOfWeek && patternVal === 7 && value === 0)
|
|
7887
|
+
if (isDayOfWeek && patternVal === 7 && value === 0) {
|
|
7888
|
+
return true;
|
|
7889
|
+
}
|
|
7861
7890
|
return patternVal === value;
|
|
7862
7891
|
}
|
|
7892
|
+
/**
|
|
7893
|
+
* Resolves a Date object to the specified timezone.
|
|
7894
|
+
*
|
|
7895
|
+
* @param date - Source UTC date.
|
|
7896
|
+
* @param timezone - Target timezone string.
|
|
7897
|
+
* @returns Localized Date object.
|
|
7898
|
+
* @throws {Error} If the timezone is invalid.
|
|
7899
|
+
*
|
|
7900
|
+
* @internal
|
|
7901
|
+
*/
|
|
7863
7902
|
static getDateInTimezone(date, timezone) {
|
|
7864
7903
|
try {
|
|
7865
7904
|
const tzDate = new Date(date.toLocaleString("en-US", { timeZone: timezone }));
|
|
7866
|
-
if (isNaN(tzDate.getTime()))
|
|
7905
|
+
if (Number.isNaN(tzDate.getTime())) {
|
|
7906
|
+
throw new Error();
|
|
7907
|
+
}
|
|
7867
7908
|
return tzDate;
|
|
7868
7909
|
} catch {
|
|
7869
7910
|
throw new Error(`Invalid timezone: ${timezone}`);
|
|
@@ -7873,8 +7914,26 @@ var SimpleCronParser = class {
|
|
|
7873
7914
|
|
|
7874
7915
|
// src/CronParser.ts
|
|
7875
7916
|
var CronParser = class {
|
|
7917
|
+
static cache = /* @__PURE__ */ new Map();
|
|
7918
|
+
/** Cache duration in milliseconds (1 minute). */
|
|
7919
|
+
static CACHE_TTL = 6e4;
|
|
7920
|
+
/** Maximum number of unique expression/timezone combinations to store. */
|
|
7921
|
+
static MAX_CACHE_SIZE = 500;
|
|
7876
7922
|
/**
|
|
7877
|
-
*
|
|
7923
|
+
* Calculates the next occurrence of a cron expression.
|
|
7924
|
+
*
|
|
7925
|
+
* Dynamically loads `cron-parser` to minimize initial bundle size and memory footprint.
|
|
7926
|
+
*
|
|
7927
|
+
* @param expression - Valid 5-part cron expression.
|
|
7928
|
+
* @param timezone - Target timezone for evaluation (default: "UTC").
|
|
7929
|
+
* @param currentDate - Reference time to calculate from (default: now).
|
|
7930
|
+
* @returns Resolves to the next execution Date object.
|
|
7931
|
+
* @throws {Error} If the expression format is invalid.
|
|
7932
|
+
*
|
|
7933
|
+
* @example
|
|
7934
|
+
* ```typescript
|
|
7935
|
+
* const next = await CronParser.nextDate('0 0 * * *');
|
|
7936
|
+
* ```
|
|
7878
7937
|
*/
|
|
7879
7938
|
static async nextDate(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
|
|
7880
7939
|
try {
|
|
@@ -7889,9 +7948,47 @@ var CronParser = class {
|
|
|
7889
7948
|
}
|
|
7890
7949
|
}
|
|
7891
7950
|
/**
|
|
7892
|
-
*
|
|
7951
|
+
* Determines if a task is due for execution at the specified time.
|
|
7952
|
+
*
|
|
7953
|
+
* Uses minute-precision caching to optimize repeated evaluations within
|
|
7954
|
+
* the same scheduling window. Implements LRU eviction to maintain memory efficiency.
|
|
7955
|
+
*
|
|
7956
|
+
* @param expression - Cron expression to evaluate.
|
|
7957
|
+
* @param timezone - Execution timezone.
|
|
7958
|
+
* @param currentDate - Reference time for the check.
|
|
7959
|
+
* @returns True if the expression matches the reference time.
|
|
7893
7960
|
*/
|
|
7894
7961
|
static async isDue(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
|
|
7962
|
+
const minuteKey = `${expression}:${timezone}:${Math.floor(currentDate.getTime() / 6e4)}`;
|
|
7963
|
+
const now = Date.now();
|
|
7964
|
+
const cached = this.cache.get(minuteKey);
|
|
7965
|
+
if (cached) {
|
|
7966
|
+
if (now - cached.timestamp < this.CACHE_TTL) {
|
|
7967
|
+
this.cache.delete(minuteKey);
|
|
7968
|
+
this.cache.set(minuteKey, cached);
|
|
7969
|
+
return cached.result;
|
|
7970
|
+
}
|
|
7971
|
+
this.cache.delete(minuteKey);
|
|
7972
|
+
}
|
|
7973
|
+
const result = await this.computeIsDue(expression, timezone, currentDate);
|
|
7974
|
+
this.cache.set(minuteKey, {
|
|
7975
|
+
result,
|
|
7976
|
+
timestamp: now
|
|
7977
|
+
});
|
|
7978
|
+
this.cleanupCache();
|
|
7979
|
+
return result;
|
|
7980
|
+
}
|
|
7981
|
+
/**
|
|
7982
|
+
* Internal logic for tiered cron evaluation.
|
|
7983
|
+
*
|
|
7984
|
+
* @param expression - Cron expression.
|
|
7985
|
+
* @param timezone - Target timezone.
|
|
7986
|
+
* @param currentDate - Current time.
|
|
7987
|
+
* @returns Boolean indicating if the task is due.
|
|
7988
|
+
*
|
|
7989
|
+
* @internal
|
|
7990
|
+
*/
|
|
7991
|
+
static async computeIsDue(expression, timezone, currentDate) {
|
|
7895
7992
|
try {
|
|
7896
7993
|
return SimpleCronParser.isDue(expression, timezone, currentDate);
|
|
7897
7994
|
} catch (_e) {
|
|
@@ -7909,6 +8006,33 @@ var CronParser = class {
|
|
|
7909
8006
|
return false;
|
|
7910
8007
|
}
|
|
7911
8008
|
}
|
|
8009
|
+
/**
|
|
8010
|
+
* Evicts the oldest entry from the cache when capacity is reached.
|
|
8011
|
+
*
|
|
8012
|
+
* @internal
|
|
8013
|
+
*/
|
|
8014
|
+
static cleanupCache() {
|
|
8015
|
+
if (this.cache.size <= this.MAX_CACHE_SIZE) {
|
|
8016
|
+
return;
|
|
8017
|
+
}
|
|
8018
|
+
const iterator = this.cache.keys();
|
|
8019
|
+
const oldestKey = iterator.next().value;
|
|
8020
|
+
if (oldestKey) {
|
|
8021
|
+
this.cache.delete(oldestKey);
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
/**
|
|
8025
|
+
* Purges all entries from the internal cache.
|
|
8026
|
+
* Useful for testing or when global timezone settings change.
|
|
8027
|
+
*/
|
|
8028
|
+
static clearCache() {
|
|
8029
|
+
this.cache.clear();
|
|
8030
|
+
}
|
|
8031
|
+
/**
|
|
8032
|
+
* Compares two dates with minute precision.
|
|
8033
|
+
*
|
|
8034
|
+
* @internal
|
|
8035
|
+
*/
|
|
7912
8036
|
static minuteMatches(date1, date2) {
|
|
7913
8037
|
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() && date1.getHours() === date2.getHours() && date1.getMinutes() === date2.getMinutes();
|
|
7914
8038
|
}
|
|
@@ -7916,22 +8040,55 @@ var CronParser = class {
|
|
|
7916
8040
|
|
|
7917
8041
|
// src/locks/CacheLockStore.ts
|
|
7918
8042
|
var CacheLockStore = class {
|
|
8043
|
+
/**
|
|
8044
|
+
* Initializes the store with a cache manager.
|
|
8045
|
+
*
|
|
8046
|
+
* @param cache - The Stasis cache instance.
|
|
8047
|
+
* @param prefix - Key prefix to avoid collisions in the shared namespace.
|
|
8048
|
+
*/
|
|
7919
8049
|
constructor(cache, prefix = "scheduler:lock:") {
|
|
7920
8050
|
this.cache = cache;
|
|
7921
8051
|
this.prefix = prefix;
|
|
7922
8052
|
}
|
|
8053
|
+
/**
|
|
8054
|
+
* Computes the fully qualified cache key.
|
|
8055
|
+
*
|
|
8056
|
+
* @internal
|
|
8057
|
+
*/
|
|
7923
8058
|
getKey(key) {
|
|
7924
8059
|
return this.prefix + key;
|
|
7925
8060
|
}
|
|
8061
|
+
/**
|
|
8062
|
+
* Atomic 'add' operation ensures only one node succeeds.
|
|
8063
|
+
*
|
|
8064
|
+
* @param key - Lock key.
|
|
8065
|
+
* @param ttlSeconds - Expiration.
|
|
8066
|
+
*/
|
|
7926
8067
|
async acquire(key, ttlSeconds) {
|
|
7927
8068
|
return this.cache.add(this.getKey(key), "LOCKED", ttlSeconds);
|
|
7928
8069
|
}
|
|
8070
|
+
/**
|
|
8071
|
+
* Removes the lock key from cache.
|
|
8072
|
+
*
|
|
8073
|
+
* @param key - Lock key.
|
|
8074
|
+
*/
|
|
7929
8075
|
async release(key) {
|
|
7930
8076
|
await this.cache.forget(this.getKey(key));
|
|
7931
8077
|
}
|
|
8078
|
+
/**
|
|
8079
|
+
* Overwrites the lock key, effectively resetting the TTL.
|
|
8080
|
+
*
|
|
8081
|
+
* @param key - Lock key.
|
|
8082
|
+
* @param ttlSeconds - New expiration.
|
|
8083
|
+
*/
|
|
7932
8084
|
async forceAcquire(key, ttlSeconds) {
|
|
7933
8085
|
await this.cache.put(this.getKey(key), "LOCKED", ttlSeconds);
|
|
7934
8086
|
}
|
|
8087
|
+
/**
|
|
8088
|
+
* Validates if the lock key is present in the cache.
|
|
8089
|
+
*
|
|
8090
|
+
* @param key - Lock key.
|
|
8091
|
+
*/
|
|
7935
8092
|
async exists(key) {
|
|
7936
8093
|
return this.cache.has(this.getKey(key));
|
|
7937
8094
|
}
|
|
@@ -7939,7 +8096,14 @@ var CacheLockStore = class {
|
|
|
7939
8096
|
|
|
7940
8097
|
// src/locks/MemoryLockStore.ts
|
|
7941
8098
|
var MemoryLockStore = class {
|
|
8099
|
+
/** Map of lock keys to their expiration timestamps (ms). */
|
|
7942
8100
|
locks = /* @__PURE__ */ new Map();
|
|
8101
|
+
/**
|
|
8102
|
+
* Acquires a local lock if the key is not already active.
|
|
8103
|
+
*
|
|
8104
|
+
* @param key - Lock identifier.
|
|
8105
|
+
* @param ttlSeconds - Expiration duration.
|
|
8106
|
+
*/
|
|
7943
8107
|
async acquire(key, ttlSeconds) {
|
|
7944
8108
|
const NOW = Date.now();
|
|
7945
8109
|
const expiresAt = this.locks.get(key);
|
|
@@ -7949,12 +8113,30 @@ var MemoryLockStore = class {
|
|
|
7949
8113
|
this.locks.set(key, NOW + ttlSeconds * 1e3);
|
|
7950
8114
|
return true;
|
|
7951
8115
|
}
|
|
8116
|
+
/**
|
|
8117
|
+
* Deletes the lock from local memory.
|
|
8118
|
+
*
|
|
8119
|
+
* @param key - Lock identifier.
|
|
8120
|
+
*/
|
|
7952
8121
|
async release(key) {
|
|
7953
8122
|
this.locks.delete(key);
|
|
7954
8123
|
}
|
|
8124
|
+
/**
|
|
8125
|
+
* Sets or overwrites a local lock.
|
|
8126
|
+
*
|
|
8127
|
+
* @param key - Lock identifier.
|
|
8128
|
+
* @param ttlSeconds - Expiration.
|
|
8129
|
+
*/
|
|
7955
8130
|
async forceAcquire(key, ttlSeconds) {
|
|
7956
8131
|
this.locks.set(key, Date.now() + ttlSeconds * 1e3);
|
|
7957
8132
|
}
|
|
8133
|
+
/**
|
|
8134
|
+
* Checks if a local lock is present and hasn't expired.
|
|
8135
|
+
*
|
|
8136
|
+
* Automatically purges expired locks upon checking.
|
|
8137
|
+
*
|
|
8138
|
+
* @param key - Lock identifier.
|
|
8139
|
+
*/
|
|
7958
8140
|
async exists(key) {
|
|
7959
8141
|
const expiresAt = this.locks.get(key);
|
|
7960
8142
|
if (!expiresAt) {
|
|
@@ -7971,6 +8153,13 @@ var MemoryLockStore = class {
|
|
|
7971
8153
|
// src/locks/LockManager.ts
|
|
7972
8154
|
var LockManager = class {
|
|
7973
8155
|
store;
|
|
8156
|
+
/**
|
|
8157
|
+
* Initializes the manager with a specific storage driver.
|
|
8158
|
+
*
|
|
8159
|
+
* @param driver - Strategy identifier or a custom `LockStore` implementation.
|
|
8160
|
+
* @param context - Dependencies required by certain drivers (e.g., CacheManager).
|
|
8161
|
+
* @throws {Error} If 'cache' driver is selected but no `CacheManager` is provided.
|
|
8162
|
+
*/
|
|
7974
8163
|
constructor(driver, context) {
|
|
7975
8164
|
if (typeof driver === "object") {
|
|
7976
8165
|
this.store = driver;
|
|
@@ -7985,15 +8174,43 @@ var LockManager = class {
|
|
|
7985
8174
|
this.store = new MemoryLockStore();
|
|
7986
8175
|
}
|
|
7987
8176
|
}
|
|
8177
|
+
/**
|
|
8178
|
+
* Attempts to acquire a lock. Fails if the key is already locked.
|
|
8179
|
+
*
|
|
8180
|
+
* @param key - Unique identifier for the lock.
|
|
8181
|
+
* @param ttlSeconds - Time-to-live in seconds before the lock expires.
|
|
8182
|
+
* @returns True if the lock was successfully acquired.
|
|
8183
|
+
*/
|
|
7988
8184
|
async acquire(key, ttlSeconds) {
|
|
7989
8185
|
return this.store.acquire(key, ttlSeconds);
|
|
7990
8186
|
}
|
|
8187
|
+
/**
|
|
8188
|
+
* Explicitly releases a held lock.
|
|
8189
|
+
*
|
|
8190
|
+
* @param key - The lock identifier to remove.
|
|
8191
|
+
* @returns Resolves when the lock is deleted.
|
|
8192
|
+
*/
|
|
7991
8193
|
async release(key) {
|
|
7992
8194
|
return this.store.release(key);
|
|
7993
8195
|
}
|
|
8196
|
+
/**
|
|
8197
|
+
* Forcibly acquires or overwrites an existing lock.
|
|
8198
|
+
*
|
|
8199
|
+
* Used for execution locks where the latest attempt should take precedence
|
|
8200
|
+
* if the previous one is deemed expired.
|
|
8201
|
+
*
|
|
8202
|
+
* @param key - Lock identifier.
|
|
8203
|
+
* @param ttlSeconds - Expiration duration.
|
|
8204
|
+
*/
|
|
7994
8205
|
async forceAcquire(key, ttlSeconds) {
|
|
7995
8206
|
return this.store.forceAcquire(key, ttlSeconds);
|
|
7996
8207
|
}
|
|
8208
|
+
/**
|
|
8209
|
+
* Checks if a specific lock currently exists and is not expired.
|
|
8210
|
+
*
|
|
8211
|
+
* @param key - Lock identifier.
|
|
8212
|
+
* @returns True if the key is locked and active.
|
|
8213
|
+
*/
|
|
7997
8214
|
async exists(key) {
|
|
7998
8215
|
return this.store.exists(key);
|
|
7999
8216
|
}
|
|
@@ -8020,19 +8237,55 @@ async function runProcess(command) {
|
|
|
8020
8237
|
};
|
|
8021
8238
|
}
|
|
8022
8239
|
var Process = class {
|
|
8240
|
+
/**
|
|
8241
|
+
* Static alias for `runProcess`.
|
|
8242
|
+
*
|
|
8243
|
+
* @param command - Command to execute.
|
|
8244
|
+
* @returns Process outcome.
|
|
8245
|
+
*/
|
|
8023
8246
|
static async run(command) {
|
|
8024
8247
|
return runProcess(command);
|
|
8025
8248
|
}
|
|
8026
8249
|
};
|
|
8027
8250
|
|
|
8251
|
+
// src/utils/validation.ts
|
|
8252
|
+
function parseTime(time) {
|
|
8253
|
+
const timePattern = /^([0-2]\d):([0-5]\d)$/;
|
|
8254
|
+
const match = time.match(timePattern);
|
|
8255
|
+
if (!match) {
|
|
8256
|
+
throw new Error(`Invalid time format: "${time}". Expected HH:mm (24-hour format, 00:00-23:59)`);
|
|
8257
|
+
}
|
|
8258
|
+
const hour = Number.parseInt(match[1], 10);
|
|
8259
|
+
const minute = Number.parseInt(match[2], 10);
|
|
8260
|
+
if (hour > 23) {
|
|
8261
|
+
throw new Error(`Invalid time format: "${time}". Expected HH:mm (24-hour format, 00:00-23:59)`);
|
|
8262
|
+
}
|
|
8263
|
+
return { hour, minute };
|
|
8264
|
+
}
|
|
8265
|
+
function validateMinute(minute) {
|
|
8266
|
+
if (!Number.isInteger(minute) || minute < 0 || minute > 59) {
|
|
8267
|
+
throw new Error(`Invalid minute: ${minute}. Expected integer 0-59`);
|
|
8268
|
+
}
|
|
8269
|
+
}
|
|
8270
|
+
function validateDayOfWeek(dayOfWeek) {
|
|
8271
|
+
if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
|
|
8272
|
+
throw new Error(`Invalid day of week: ${dayOfWeek}. Expected 0-6 (Sunday=0, Saturday=6)`);
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
function validateDayOfMonth(dayOfMonth) {
|
|
8276
|
+
if (!Number.isInteger(dayOfMonth) || dayOfMonth < 1 || dayOfMonth > 31) {
|
|
8277
|
+
throw new Error(`Invalid day of month: ${dayOfMonth}. Expected 1-31`);
|
|
8278
|
+
}
|
|
8279
|
+
}
|
|
8280
|
+
|
|
8028
8281
|
// src/TaskSchedule.ts
|
|
8029
8282
|
var TaskSchedule = class {
|
|
8030
8283
|
task;
|
|
8031
8284
|
/**
|
|
8032
|
-
*
|
|
8285
|
+
* Initializes a new schedule instance with default settings.
|
|
8033
8286
|
*
|
|
8034
|
-
* @param name -
|
|
8035
|
-
* @param callback -
|
|
8287
|
+
* @param name - Unique task name used for identification and locking.
|
|
8288
|
+
* @param callback - Logic to execute on schedule.
|
|
8036
8289
|
*/
|
|
8037
8290
|
constructor(name, callback) {
|
|
8038
8291
|
this.task = {
|
|
@@ -8046,168 +8299,217 @@ var TaskSchedule = class {
|
|
|
8046
8299
|
// 5 minutes default
|
|
8047
8300
|
background: false,
|
|
8048
8301
|
// Wait for task to finish by default
|
|
8302
|
+
preventOverlapping: false,
|
|
8303
|
+
overlappingExpiresAt: 3600,
|
|
8304
|
+
// 1 hour default
|
|
8049
8305
|
onSuccessCallbacks: [],
|
|
8050
8306
|
onFailureCallbacks: []
|
|
8051
8307
|
};
|
|
8052
8308
|
}
|
|
8053
8309
|
// --- Frequency Methods ---
|
|
8054
8310
|
/**
|
|
8055
|
-
*
|
|
8311
|
+
* Configures a raw 5-part cron expression.
|
|
8056
8312
|
*
|
|
8057
|
-
* @param expression -
|
|
8058
|
-
* @returns The TaskSchedule instance.
|
|
8313
|
+
* @param expression - Cron string (e.g., "0 0 * * *").
|
|
8314
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8315
|
+
* @throws {Error} If expression format is invalid or contains forbidden characters.
|
|
8316
|
+
*
|
|
8317
|
+
* @example
|
|
8318
|
+
* ```typescript
|
|
8319
|
+
* schedule.cron('0 9-17 * * 1-5'); // 9-5 on weekdays
|
|
8320
|
+
* ```
|
|
8059
8321
|
*/
|
|
8060
8322
|
cron(expression) {
|
|
8323
|
+
const parts = expression.trim().split(/\s+/);
|
|
8324
|
+
if (parts.length !== 5) {
|
|
8325
|
+
throw new Error(
|
|
8326
|
+
`Invalid cron expression: "${expression}". Expected 5 parts (minute hour day month weekday), got ${parts.length}.`
|
|
8327
|
+
);
|
|
8328
|
+
}
|
|
8329
|
+
const pattern = /^[0-9,\-/*?L#A-Za-z]+$/;
|
|
8330
|
+
for (let i = 0; i < 5; i++) {
|
|
8331
|
+
if (!pattern.test(parts[i])) {
|
|
8332
|
+
throw new Error(
|
|
8333
|
+
`Invalid cron expression: "${expression}". Part ${i + 1} ("${parts[i]}") contains invalid characters.`
|
|
8334
|
+
);
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8061
8337
|
this.task.expression = expression;
|
|
8062
8338
|
return this;
|
|
8063
8339
|
}
|
|
8064
8340
|
/**
|
|
8065
|
-
*
|
|
8341
|
+
* Schedules execution every minute.
|
|
8066
8342
|
*
|
|
8067
|
-
* @returns The TaskSchedule instance.
|
|
8343
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8068
8344
|
*/
|
|
8069
8345
|
everyMinute() {
|
|
8070
8346
|
return this.cron("* * * * *");
|
|
8071
8347
|
}
|
|
8072
8348
|
/**
|
|
8073
|
-
*
|
|
8349
|
+
* Schedules execution every five minutes.
|
|
8074
8350
|
*
|
|
8075
|
-
* @returns The TaskSchedule instance.
|
|
8351
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8076
8352
|
*/
|
|
8077
8353
|
everyFiveMinutes() {
|
|
8078
8354
|
return this.cron("*/5 * * * *");
|
|
8079
8355
|
}
|
|
8080
8356
|
/**
|
|
8081
|
-
*
|
|
8357
|
+
* Schedules execution every ten minutes.
|
|
8082
8358
|
*
|
|
8083
|
-
* @returns The TaskSchedule instance.
|
|
8359
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8084
8360
|
*/
|
|
8085
8361
|
everyTenMinutes() {
|
|
8086
8362
|
return this.cron("*/10 * * * *");
|
|
8087
8363
|
}
|
|
8088
8364
|
/**
|
|
8089
|
-
*
|
|
8365
|
+
* Schedules execution every fifteen minutes.
|
|
8090
8366
|
*
|
|
8091
|
-
* @returns The TaskSchedule instance.
|
|
8367
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8092
8368
|
*/
|
|
8093
8369
|
everyFifteenMinutes() {
|
|
8094
8370
|
return this.cron("*/15 * * * *");
|
|
8095
8371
|
}
|
|
8096
8372
|
/**
|
|
8097
|
-
*
|
|
8373
|
+
* Schedules execution every thirty minutes.
|
|
8098
8374
|
*
|
|
8099
|
-
* @returns The TaskSchedule instance.
|
|
8375
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8100
8376
|
*/
|
|
8101
8377
|
everyThirtyMinutes() {
|
|
8102
8378
|
return this.cron("0,30 * * * *");
|
|
8103
8379
|
}
|
|
8104
8380
|
/**
|
|
8105
|
-
*
|
|
8381
|
+
* Schedules execution hourly at the top of the hour.
|
|
8106
8382
|
*
|
|
8107
|
-
* @returns The TaskSchedule instance.
|
|
8383
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8108
8384
|
*/
|
|
8109
8385
|
hourly() {
|
|
8110
8386
|
return this.cron("0 * * * *");
|
|
8111
8387
|
}
|
|
8112
8388
|
/**
|
|
8113
|
-
*
|
|
8389
|
+
* Schedules execution hourly at a specific minute.
|
|
8114
8390
|
*
|
|
8115
|
-
* @param minute -
|
|
8116
|
-
* @returns The TaskSchedule instance.
|
|
8391
|
+
* @param minute - Target minute (0-59).
|
|
8392
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8393
|
+
* @throws {Error} If minute is outside 0-59 range.
|
|
8117
8394
|
*/
|
|
8118
8395
|
hourlyAt(minute) {
|
|
8396
|
+
validateMinute(minute);
|
|
8119
8397
|
return this.cron(`${minute} * * * *`);
|
|
8120
8398
|
}
|
|
8121
8399
|
/**
|
|
8122
|
-
*
|
|
8400
|
+
* Schedules execution daily at midnight.
|
|
8123
8401
|
*
|
|
8124
|
-
* @returns The TaskSchedule instance.
|
|
8402
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8125
8403
|
*/
|
|
8126
8404
|
daily() {
|
|
8127
8405
|
return this.cron("0 0 * * *");
|
|
8128
8406
|
}
|
|
8129
8407
|
/**
|
|
8130
|
-
*
|
|
8408
|
+
* Schedules execution daily at a specific time.
|
|
8409
|
+
*
|
|
8410
|
+
* @param time - 24-hour time string in "HH:mm" format.
|
|
8411
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8412
|
+
* @throws {Error} If time format is invalid or values are out of range.
|
|
8131
8413
|
*
|
|
8132
|
-
* @
|
|
8133
|
-
*
|
|
8414
|
+
* @example
|
|
8415
|
+
* ```typescript
|
|
8416
|
+
* schedule.dailyAt('14:30');
|
|
8417
|
+
* ```
|
|
8134
8418
|
*/
|
|
8135
8419
|
dailyAt(time) {
|
|
8136
|
-
const
|
|
8137
|
-
return this.cron(`${
|
|
8420
|
+
const { hour, minute } = parseTime(time);
|
|
8421
|
+
return this.cron(`${minute} ${hour} * * *`);
|
|
8138
8422
|
}
|
|
8139
8423
|
/**
|
|
8140
|
-
*
|
|
8424
|
+
* Schedules execution weekly on Sunday at midnight.
|
|
8141
8425
|
*
|
|
8142
|
-
* @returns The TaskSchedule instance.
|
|
8426
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8143
8427
|
*/
|
|
8144
8428
|
weekly() {
|
|
8145
8429
|
return this.cron("0 0 * * 0");
|
|
8146
8430
|
}
|
|
8147
8431
|
/**
|
|
8148
|
-
*
|
|
8432
|
+
* Schedules execution weekly on a specific day and time.
|
|
8433
|
+
*
|
|
8434
|
+
* @param day - Day index (0-6, where 0 is Sunday).
|
|
8435
|
+
* @param time - Optional 24-hour time string "HH:mm" (default "00:00").
|
|
8436
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8437
|
+
* @throws {Error} If day index or time format is invalid.
|
|
8149
8438
|
*
|
|
8150
|
-
* @
|
|
8151
|
-
*
|
|
8152
|
-
*
|
|
8439
|
+
* @example
|
|
8440
|
+
* ```typescript
|
|
8441
|
+
* schedule.weeklyOn(1, '09:00'); // Mondays at 9 AM
|
|
8442
|
+
* ```
|
|
8153
8443
|
*/
|
|
8154
8444
|
weeklyOn(day, time = "00:00") {
|
|
8155
|
-
|
|
8156
|
-
|
|
8445
|
+
validateDayOfWeek(day);
|
|
8446
|
+
const { hour, minute } = parseTime(time);
|
|
8447
|
+
return this.cron(`${minute} ${hour} * * ${day}`);
|
|
8157
8448
|
}
|
|
8158
8449
|
/**
|
|
8159
|
-
*
|
|
8450
|
+
* Schedules execution monthly on the first day at midnight.
|
|
8160
8451
|
*
|
|
8161
|
-
* @returns The TaskSchedule instance.
|
|
8452
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8162
8453
|
*/
|
|
8163
8454
|
monthly() {
|
|
8164
8455
|
return this.cron("0 0 1 * *");
|
|
8165
8456
|
}
|
|
8166
8457
|
/**
|
|
8167
|
-
*
|
|
8458
|
+
* Schedules execution monthly on a specific day and time.
|
|
8168
8459
|
*
|
|
8169
|
-
* @param day - Day of month (1-31)
|
|
8170
|
-
* @param time -
|
|
8171
|
-
* @returns The TaskSchedule instance.
|
|
8460
|
+
* @param day - Day of month (1-31).
|
|
8461
|
+
* @param time - Optional 24-hour time string "HH:mm" (default "00:00").
|
|
8462
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8463
|
+
* @throws {Error} If day or time format is invalid.
|
|
8172
8464
|
*/
|
|
8173
8465
|
monthlyOn(day, time = "00:00") {
|
|
8174
|
-
|
|
8175
|
-
|
|
8466
|
+
validateDayOfMonth(day);
|
|
8467
|
+
const { hour, minute } = parseTime(time);
|
|
8468
|
+
return this.cron(`${minute} ${hour} ${day} * *`);
|
|
8176
8469
|
}
|
|
8177
8470
|
// --- Constraints ---
|
|
8178
8471
|
/**
|
|
8179
|
-
*
|
|
8472
|
+
* Specifies the timezone for evaluating schedule frequency.
|
|
8180
8473
|
*
|
|
8181
|
-
* @param timezone -
|
|
8182
|
-
* @returns The TaskSchedule instance.
|
|
8474
|
+
* @param timezone - IANA timezone identifier (e.g., "America/New_York").
|
|
8475
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8476
|
+
* @throws {Error} If the timezone identifier is not recognized by the system.
|
|
8183
8477
|
*/
|
|
8184
8478
|
timezone(timezone) {
|
|
8479
|
+
try {
|
|
8480
|
+
(/* @__PURE__ */ new Date()).toLocaleString("en-US", { timeZone: timezone });
|
|
8481
|
+
} catch {
|
|
8482
|
+
throw new Error(
|
|
8483
|
+
`Invalid timezone: "${timezone}". See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for valid values.`
|
|
8484
|
+
);
|
|
8485
|
+
}
|
|
8185
8486
|
this.task.timezone = timezone;
|
|
8186
8487
|
return this;
|
|
8187
8488
|
}
|
|
8188
8489
|
/**
|
|
8189
|
-
*
|
|
8190
|
-
*
|
|
8490
|
+
* Modifies the execution time of the existing frequency.
|
|
8491
|
+
* Typically used after frequency methods like daily() or weekly().
|
|
8191
8492
|
*
|
|
8192
|
-
* @param time -
|
|
8193
|
-
* @returns The TaskSchedule instance.
|
|
8493
|
+
* @param time - 24-hour time string in "HH:mm" format.
|
|
8494
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8495
|
+
* @throws {Error} If time format is invalid.
|
|
8194
8496
|
*/
|
|
8195
8497
|
at(time) {
|
|
8196
|
-
const
|
|
8498
|
+
const { hour, minute } = parseTime(time);
|
|
8197
8499
|
const parts = this.task.expression.split(" ");
|
|
8198
8500
|
if (parts.length >= 5) {
|
|
8199
|
-
parts[0] = String(
|
|
8200
|
-
parts[1] = String(
|
|
8501
|
+
parts[0] = String(minute);
|
|
8502
|
+
parts[1] = String(hour);
|
|
8201
8503
|
this.task.expression = parts.join(" ");
|
|
8202
8504
|
}
|
|
8203
8505
|
return this;
|
|
8204
8506
|
}
|
|
8205
8507
|
/**
|
|
8206
|
-
*
|
|
8207
|
-
*
|
|
8508
|
+
* Enables distributed locking to ensure only one instance runs globally.
|
|
8509
|
+
* Prevents duplicate execution when multiple servers are polling the same schedule.
|
|
8208
8510
|
*
|
|
8209
|
-
* @param lockTtlSeconds -
|
|
8210
|
-
* @returns The TaskSchedule instance.
|
|
8511
|
+
* @param lockTtlSeconds - Duration in seconds to hold the window lock (default 300).
|
|
8512
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8211
8513
|
*/
|
|
8212
8514
|
onOneServer(lockTtlSeconds = 300) {
|
|
8213
8515
|
this.task.shouldRunOnOneServer = true;
|
|
@@ -8215,41 +8517,85 @@ var TaskSchedule = class {
|
|
|
8215
8517
|
return this;
|
|
8216
8518
|
}
|
|
8217
8519
|
/**
|
|
8218
|
-
*
|
|
8219
|
-
* Prevents overlapping executions of the same task.
|
|
8520
|
+
* Prevents a new task instance from starting if the previous one is still running.
|
|
8220
8521
|
*
|
|
8221
|
-
*
|
|
8222
|
-
*
|
|
8522
|
+
* Unlike `onOneServer()` which uses a time-window lock, this uses an execution lock
|
|
8523
|
+
* to handle long-running tasks that might span multiple scheduling intervals.
|
|
8524
|
+
*
|
|
8525
|
+
* @param expiresAt - Max duration in seconds to keep the execution lock (default 3600).
|
|
8526
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8527
|
+
*
|
|
8528
|
+
* @example
|
|
8529
|
+
* ```typescript
|
|
8530
|
+
* // Prevents overlapping if the heavy operation takes > 1 minute
|
|
8531
|
+
* scheduler.task('data-sync', heavySync)
|
|
8532
|
+
* .everyMinute()
|
|
8533
|
+
* .withoutOverlapping();
|
|
8534
|
+
* ```
|
|
8223
8535
|
*/
|
|
8224
|
-
withoutOverlapping(expiresAt =
|
|
8225
|
-
|
|
8536
|
+
withoutOverlapping(expiresAt = 3600) {
|
|
8537
|
+
this.task.preventOverlapping = true;
|
|
8538
|
+
this.task.overlappingExpiresAt = expiresAt;
|
|
8539
|
+
return this;
|
|
8226
8540
|
}
|
|
8227
8541
|
/**
|
|
8228
|
-
*
|
|
8229
|
-
*
|
|
8230
|
-
* but affects how we handle error catching and lock release.
|
|
8542
|
+
* Executes the task in background mode.
|
|
8543
|
+
* The scheduler will not wait for this task to finish before proceeding to the next.
|
|
8231
8544
|
*
|
|
8232
|
-
* @returns The TaskSchedule instance.
|
|
8545
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8233
8546
|
*/
|
|
8234
8547
|
runInBackground() {
|
|
8235
8548
|
this.task.background = true;
|
|
8236
8549
|
return this;
|
|
8237
8550
|
}
|
|
8238
8551
|
/**
|
|
8239
|
-
*
|
|
8552
|
+
* Restricts task execution to nodes with a specific role.
|
|
8240
8553
|
*
|
|
8241
|
-
* @param role -
|
|
8242
|
-
* @returns The TaskSchedule instance.
|
|
8554
|
+
* @param role - Target role identifier (e.g., "worker").
|
|
8555
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8243
8556
|
*/
|
|
8244
8557
|
onNode(role) {
|
|
8245
8558
|
this.task.nodeRole = role;
|
|
8246
8559
|
return this;
|
|
8247
8560
|
}
|
|
8248
8561
|
/**
|
|
8249
|
-
*
|
|
8562
|
+
* Defines a maximum execution time for the task.
|
|
8563
|
+
*
|
|
8564
|
+
* @param ms - Timeout duration in milliseconds.
|
|
8565
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8566
|
+
* @throws {Error} If timeout is not a positive number.
|
|
8567
|
+
*/
|
|
8568
|
+
timeout(ms) {
|
|
8569
|
+
if (ms <= 0) {
|
|
8570
|
+
throw new Error("Timeout must be a positive number");
|
|
8571
|
+
}
|
|
8572
|
+
this.task.timeout = ms;
|
|
8573
|
+
return this;
|
|
8574
|
+
}
|
|
8575
|
+
/**
|
|
8576
|
+
* Configures automatic retry behavior for failed executions.
|
|
8577
|
+
*
|
|
8578
|
+
* @param attempts - Max number of retries (default 3).
|
|
8579
|
+
* @param delayMs - Wait time between retries in milliseconds (default 1000).
|
|
8580
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8581
|
+
* @throws {Error} If attempts or delay are negative.
|
|
8582
|
+
*/
|
|
8583
|
+
retry(attempts = 3, delayMs = 1e3) {
|
|
8584
|
+
if (attempts < 0) {
|
|
8585
|
+
throw new Error("Retry attempts must be non-negative");
|
|
8586
|
+
}
|
|
8587
|
+
if (delayMs < 0) {
|
|
8588
|
+
throw new Error("Retry delay must be non-negative");
|
|
8589
|
+
}
|
|
8590
|
+
this.task.retries = attempts;
|
|
8591
|
+
this.task.retryDelay = delayMs;
|
|
8592
|
+
return this;
|
|
8593
|
+
}
|
|
8594
|
+
/**
|
|
8595
|
+
* Sets the shell command for execution-based tasks.
|
|
8250
8596
|
*
|
|
8251
8597
|
* @param command - The command string.
|
|
8252
|
-
* @returns The TaskSchedule instance.
|
|
8598
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8253
8599
|
* @internal
|
|
8254
8600
|
*/
|
|
8255
8601
|
setCommand(command) {
|
|
@@ -8258,39 +8604,39 @@ var TaskSchedule = class {
|
|
|
8258
8604
|
}
|
|
8259
8605
|
// --- Hooks ---
|
|
8260
8606
|
/**
|
|
8261
|
-
*
|
|
8607
|
+
* Registers a callback to execute upon successful task completion.
|
|
8262
8608
|
*
|
|
8263
|
-
* @param callback -
|
|
8264
|
-
* @returns The TaskSchedule instance.
|
|
8609
|
+
* @param callback - Function to run on success.
|
|
8610
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8265
8611
|
*/
|
|
8266
8612
|
onSuccess(callback) {
|
|
8267
8613
|
this.task.onSuccessCallbacks.push(callback);
|
|
8268
8614
|
return this;
|
|
8269
8615
|
}
|
|
8270
8616
|
/**
|
|
8271
|
-
*
|
|
8617
|
+
* Registers a callback to execute when the task fails.
|
|
8272
8618
|
*
|
|
8273
|
-
* @param callback -
|
|
8274
|
-
* @returns The TaskSchedule instance.
|
|
8619
|
+
* @param callback - Function to run on failure.
|
|
8620
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8275
8621
|
*/
|
|
8276
8622
|
onFailure(callback) {
|
|
8277
8623
|
this.task.onFailureCallbacks.push(callback);
|
|
8278
8624
|
return this;
|
|
8279
8625
|
}
|
|
8280
8626
|
/**
|
|
8281
|
-
*
|
|
8627
|
+
* Attaches a human-readable description to the task.
|
|
8282
8628
|
*
|
|
8283
|
-
* @param _text -
|
|
8284
|
-
* @returns The TaskSchedule instance.
|
|
8629
|
+
* @param _text - Description text.
|
|
8630
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8285
8631
|
*/
|
|
8286
8632
|
description(_text) {
|
|
8287
8633
|
return this;
|
|
8288
8634
|
}
|
|
8289
8635
|
// --- Accessor ---
|
|
8290
8636
|
/**
|
|
8291
|
-
*
|
|
8637
|
+
* Returns the final task configuration.
|
|
8292
8638
|
*
|
|
8293
|
-
* @returns The ScheduledTask object.
|
|
8639
|
+
* @returns The constructed ScheduledTask object.
|
|
8294
8640
|
*/
|
|
8295
8641
|
getTask() {
|
|
8296
8642
|
return this.task;
|
|
@@ -8299,6 +8645,14 @@ var TaskSchedule = class {
|
|
|
8299
8645
|
|
|
8300
8646
|
// src/SchedulerManager.ts
|
|
8301
8647
|
var SchedulerManager = class {
|
|
8648
|
+
/**
|
|
8649
|
+
* Initializes the scheduler engine.
|
|
8650
|
+
*
|
|
8651
|
+
* @param lockManager - Backend for distributed locking.
|
|
8652
|
+
* @param logger - Optional logger for operational visibility.
|
|
8653
|
+
* @param hooks - Optional manager for lifecycle event hooks.
|
|
8654
|
+
* @param currentNodeRole - Role identifier for the local node (used for filtering).
|
|
8655
|
+
*/
|
|
8302
8656
|
constructor(lockManager, logger, hooks, currentNodeRole) {
|
|
8303
8657
|
this.lockManager = lockManager;
|
|
8304
8658
|
this.logger = logger;
|
|
@@ -8307,11 +8661,18 @@ var SchedulerManager = class {
|
|
|
8307
8661
|
}
|
|
8308
8662
|
tasks = [];
|
|
8309
8663
|
/**
|
|
8310
|
-
*
|
|
8664
|
+
* Registers a new callback-based scheduled task.
|
|
8311
8665
|
*
|
|
8312
|
-
* @param name - Unique name for
|
|
8313
|
-
* @param callback -
|
|
8314
|
-
* @returns
|
|
8666
|
+
* @param name - Unique task name used for identification and locking.
|
|
8667
|
+
* @param callback - Asynchronous function containing the task logic.
|
|
8668
|
+
* @returns A fluent TaskSchedule instance for further configuration.
|
|
8669
|
+
*
|
|
8670
|
+
* @example
|
|
8671
|
+
* ```typescript
|
|
8672
|
+
* scheduler.task('process-queues', async () => {
|
|
8673
|
+
* await queue.process();
|
|
8674
|
+
* }).everyMinute();
|
|
8675
|
+
* ```
|
|
8315
8676
|
*/
|
|
8316
8677
|
task(name, callback) {
|
|
8317
8678
|
const task = new TaskSchedule(name, callback);
|
|
@@ -8319,11 +8680,21 @@ var SchedulerManager = class {
|
|
|
8319
8680
|
return task;
|
|
8320
8681
|
}
|
|
8321
8682
|
/**
|
|
8322
|
-
*
|
|
8683
|
+
* Registers a shell command as a scheduled task.
|
|
8684
|
+
*
|
|
8685
|
+
* Executes the command via `sh -c` on matching nodes.
|
|
8686
|
+
*
|
|
8687
|
+
* @param name - Unique identifier for the command task.
|
|
8688
|
+
* @param command - Raw shell command string.
|
|
8689
|
+
* @returns A fluent TaskSchedule instance.
|
|
8690
|
+
* @throws {Error} If the shell command returns a non-zero exit code during execution.
|
|
8323
8691
|
*
|
|
8324
|
-
* @
|
|
8325
|
-
*
|
|
8326
|
-
*
|
|
8692
|
+
* @example
|
|
8693
|
+
* ```typescript
|
|
8694
|
+
* scheduler.exec('log-rotate', 'logrotate /etc/logrotate.conf')
|
|
8695
|
+
* .daily()
|
|
8696
|
+
* .onNode('worker');
|
|
8697
|
+
* ```
|
|
8327
8698
|
*/
|
|
8328
8699
|
exec(name, command) {
|
|
8329
8700
|
const task = new TaskSchedule(name, async () => {
|
|
@@ -8337,27 +8708,29 @@ var SchedulerManager = class {
|
|
|
8337
8708
|
return task;
|
|
8338
8709
|
}
|
|
8339
8710
|
/**
|
|
8340
|
-
*
|
|
8711
|
+
* Injects a pre-configured TaskSchedule instance into the registry.
|
|
8341
8712
|
*
|
|
8342
|
-
* @param schedule -
|
|
8713
|
+
* @param schedule - Configured task schedule to register.
|
|
8343
8714
|
*/
|
|
8344
8715
|
add(schedule) {
|
|
8345
8716
|
this.tasks.push(schedule);
|
|
8346
8717
|
}
|
|
8347
8718
|
/**
|
|
8348
|
-
*
|
|
8719
|
+
* Exports all registered tasks for external inspection or serialization.
|
|
8349
8720
|
*
|
|
8350
|
-
* @returns An array of
|
|
8721
|
+
* @returns An array of raw task configurations.
|
|
8351
8722
|
*/
|
|
8352
8723
|
getTasks() {
|
|
8353
8724
|
return this.tasks.map((t) => t.getTask());
|
|
8354
8725
|
}
|
|
8355
8726
|
/**
|
|
8356
|
-
*
|
|
8357
|
-
* This is typically called every minute by a system cron or worker loop.
|
|
8727
|
+
* Main evaluation loop that triggers tasks due for execution.
|
|
8358
8728
|
*
|
|
8359
|
-
*
|
|
8360
|
-
*
|
|
8729
|
+
* Should be invoked every minute by a system timer (systemd/cron) or daemon.
|
|
8730
|
+
* Performs frequency checks, role filtering, and parallel execution.
|
|
8731
|
+
*
|
|
8732
|
+
* @param date - Reference time for cron evaluation (default: current time).
|
|
8733
|
+
* @returns Resolves when all due tasks have been initiated.
|
|
8361
8734
|
*/
|
|
8362
8735
|
async run(date = /* @__PURE__ */ new Date()) {
|
|
8363
8736
|
await this.hooks?.doAction("scheduler:run:start", { date });
|
|
@@ -8369,6 +8742,14 @@ var SchedulerManager = class {
|
|
|
8369
8742
|
}
|
|
8370
8743
|
}
|
|
8371
8744
|
if (dueTasks.length > 0) {
|
|
8745
|
+
this.logger?.debug(`[Horizon] Found ${dueTasks.length} due task(s) to execute`, {
|
|
8746
|
+
tasks: dueTasks.map((t) => ({
|
|
8747
|
+
name: t.name,
|
|
8748
|
+
expression: t.expression,
|
|
8749
|
+
background: t.background,
|
|
8750
|
+
oneServer: t.shouldRunOnOneServer
|
|
8751
|
+
}))
|
|
8752
|
+
});
|
|
8372
8753
|
}
|
|
8373
8754
|
for (const task of dueTasks) {
|
|
8374
8755
|
this.runTask(task, date).catch((err) => {
|
|
@@ -8378,21 +8759,45 @@ var SchedulerManager = class {
|
|
|
8378
8759
|
await this.hooks?.doAction("scheduler:run:complete", { date, dueCount: dueTasks.length });
|
|
8379
8760
|
}
|
|
8380
8761
|
/**
|
|
8381
|
-
*
|
|
8762
|
+
* Executes an individual task after validating execution constraints.
|
|
8763
|
+
*
|
|
8764
|
+
* Evaluates node roles, overlapping prevention, and distributed time-window locks
|
|
8765
|
+
* before initiating the actual task logic.
|
|
8766
|
+
*
|
|
8767
|
+
* @param task - Target task configuration.
|
|
8768
|
+
* @param date - Reference time used for lock key generation.
|
|
8382
8769
|
*
|
|
8383
|
-
* @param task - The task to execute.
|
|
8384
8770
|
* @internal
|
|
8385
8771
|
*/
|
|
8386
8772
|
async runTask(task, date = /* @__PURE__ */ new Date()) {
|
|
8387
8773
|
if (task.nodeRole && this.currentNodeRole && task.nodeRole !== this.currentNodeRole) {
|
|
8388
8774
|
return;
|
|
8389
8775
|
}
|
|
8390
|
-
|
|
8776
|
+
const runningLockKey = `task:running:${task.name}`;
|
|
8777
|
+
if (task.preventOverlapping) {
|
|
8778
|
+
const isRunning = await this.lockManager.exists(runningLockKey);
|
|
8779
|
+
if (isRunning) {
|
|
8780
|
+
this.logger?.debug(
|
|
8781
|
+
`[Horizon] Task "${task.name}" is still running, skipping this execution`
|
|
8782
|
+
);
|
|
8783
|
+
await this.hooks?.doAction("scheduler:task:skipped", {
|
|
8784
|
+
name: task.name,
|
|
8785
|
+
reason: "overlapping",
|
|
8786
|
+
timestamp: date
|
|
8787
|
+
});
|
|
8788
|
+
return;
|
|
8789
|
+
}
|
|
8790
|
+
await this.lockManager.forceAcquire(runningLockKey, task.overlappingExpiresAt);
|
|
8791
|
+
}
|
|
8792
|
+
let acquiredTimeLock = false;
|
|
8391
8793
|
const timestamp = `${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, "0")}${date.getDate().toString().padStart(2, "0")}${date.getHours().toString().padStart(2, "0")}${date.getMinutes().toString().padStart(2, "0")}`;
|
|
8392
|
-
const
|
|
8794
|
+
const timeLockKey = `task:${task.name}:${timestamp}`;
|
|
8393
8795
|
if (task.shouldRunOnOneServer) {
|
|
8394
|
-
|
|
8395
|
-
if (!
|
|
8796
|
+
acquiredTimeLock = await this.lockManager.acquire(timeLockKey, task.lockTtl);
|
|
8797
|
+
if (!acquiredTimeLock) {
|
|
8798
|
+
if (task.preventOverlapping) {
|
|
8799
|
+
await this.lockManager.release(runningLockKey);
|
|
8800
|
+
}
|
|
8396
8801
|
return;
|
|
8397
8802
|
}
|
|
8398
8803
|
}
|
|
@@ -8400,48 +8805,99 @@ var SchedulerManager = class {
|
|
|
8400
8805
|
if (task.background) {
|
|
8401
8806
|
this.executeTask(task).catch((err) => {
|
|
8402
8807
|
this.logger?.error(`Background task ${task.name} failed`, err);
|
|
8808
|
+
}).finally(async () => {
|
|
8809
|
+
if (task.preventOverlapping) {
|
|
8810
|
+
await this.lockManager.release(runningLockKey);
|
|
8811
|
+
}
|
|
8403
8812
|
});
|
|
8404
8813
|
} else {
|
|
8405
8814
|
await this.executeTask(task);
|
|
8815
|
+
if (task.preventOverlapping) {
|
|
8816
|
+
await this.lockManager.release(runningLockKey);
|
|
8817
|
+
}
|
|
8406
8818
|
}
|
|
8407
8819
|
} catch (err) {
|
|
8408
|
-
if (
|
|
8409
|
-
await this.lockManager.release(
|
|
8820
|
+
if (task.preventOverlapping) {
|
|
8821
|
+
await this.lockManager.release(runningLockKey);
|
|
8822
|
+
}
|
|
8823
|
+
if (acquiredTimeLock) {
|
|
8824
|
+
await this.lockManager.release(timeLockKey);
|
|
8410
8825
|
}
|
|
8411
8826
|
throw err;
|
|
8412
8827
|
}
|
|
8413
8828
|
}
|
|
8414
8829
|
/**
|
|
8415
|
-
*
|
|
8830
|
+
* Internal wrapper for executing task logic with reliability controls.
|
|
8831
|
+
*
|
|
8832
|
+
* Handles timeouts, retries, and emits lifecycle hooks for monitoring.
|
|
8416
8833
|
*
|
|
8417
|
-
* @param task - The task to execute.
|
|
8834
|
+
* @param task - The scheduled task to execute.
|
|
8835
|
+
* @returns Resolves when the task (and its retries) completes or fails permanently.
|
|
8836
|
+
*
|
|
8837
|
+
* @internal
|
|
8418
8838
|
*/
|
|
8419
8839
|
async executeTask(task) {
|
|
8420
8840
|
const startTime = Date.now();
|
|
8421
8841
|
await this.hooks?.doAction("scheduler:task:start", { name: task.name, startTime });
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8842
|
+
const timeout = task.timeout || 36e5;
|
|
8843
|
+
const maxRetries = task.retries ?? 0;
|
|
8844
|
+
const retryDelay = task.retryDelay ?? 1e3;
|
|
8845
|
+
let lastError;
|
|
8846
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
8847
|
+
if (attempt > 0) {
|
|
8848
|
+
this.logger?.info(
|
|
8849
|
+
`[Horizon] Retrying task "${task.name}" (attempt ${attempt}/${maxRetries})...`
|
|
8850
|
+
);
|
|
8851
|
+
await this.hooks?.doAction("scheduler:task:retry", {
|
|
8852
|
+
name: task.name,
|
|
8853
|
+
attempt,
|
|
8854
|
+
maxRetries,
|
|
8855
|
+
error: lastError,
|
|
8856
|
+
delay: retryDelay
|
|
8857
|
+
});
|
|
8858
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
8431
8859
|
}
|
|
8432
|
-
|
|
8433
|
-
const
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
error: err,
|
|
8438
|
-
duration
|
|
8860
|
+
let timeoutId;
|
|
8861
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
8862
|
+
timeoutId = setTimeout(() => {
|
|
8863
|
+
reject(new Error(`Task "${task.name}" timed out after ${timeout}ms`));
|
|
8864
|
+
}, timeout);
|
|
8439
8865
|
});
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8866
|
+
try {
|
|
8867
|
+
await Promise.race([task.callback(), timeoutPromise]);
|
|
8868
|
+
const duration2 = Date.now() - startTime;
|
|
8869
|
+
await this.hooks?.doAction("scheduler:task:success", {
|
|
8870
|
+
name: task.name,
|
|
8871
|
+
duration: duration2,
|
|
8872
|
+
attempts: attempt + 1
|
|
8873
|
+
});
|
|
8874
|
+
for (const cb of task.onSuccessCallbacks) {
|
|
8875
|
+
try {
|
|
8876
|
+
await cb({ name: task.name });
|
|
8877
|
+
} catch {
|
|
8878
|
+
}
|
|
8444
8879
|
}
|
|
8880
|
+
return;
|
|
8881
|
+
} catch (err) {
|
|
8882
|
+
lastError = err;
|
|
8883
|
+
this.logger?.error(`Task ${task.name} failed (attempt ${attempt + 1})`, err);
|
|
8884
|
+
} finally {
|
|
8885
|
+
if (timeoutId) {
|
|
8886
|
+
clearTimeout(timeoutId);
|
|
8887
|
+
}
|
|
8888
|
+
}
|
|
8889
|
+
}
|
|
8890
|
+
const duration = Date.now() - startTime;
|
|
8891
|
+
await this.hooks?.doAction("scheduler:task:failure", {
|
|
8892
|
+
name: task.name,
|
|
8893
|
+
error: lastError,
|
|
8894
|
+
duration,
|
|
8895
|
+
attempts: maxRetries + 1
|
|
8896
|
+
});
|
|
8897
|
+
for (const cb of task.onFailureCallbacks) {
|
|
8898
|
+
try {
|
|
8899
|
+
await cb(lastError);
|
|
8900
|
+
} catch {
|
|
8445
8901
|
}
|
|
8446
8902
|
}
|
|
8447
8903
|
}
|
|
@@ -8450,10 +8906,17 @@ var SchedulerManager = class {
|
|
|
8450
8906
|
// src/OrbitHorizon.ts
|
|
8451
8907
|
var OrbitHorizon = class {
|
|
8452
8908
|
/**
|
|
8453
|
-
*
|
|
8454
|
-
*
|
|
8909
|
+
* Integrates the Horizon scheduler into the PlanetCore lifecycle.
|
|
8910
|
+
*
|
|
8911
|
+
* Orchestrates the initialization sequence:
|
|
8912
|
+
* 1. Resolves lock backend (memory or cache-based).
|
|
8913
|
+
* 2. Instantiates SchedulerManager with global dependencies (logger, hooks).
|
|
8914
|
+
* 3. Registers the scheduler in the IoC container for CLI and global access.
|
|
8915
|
+
* 4. Injects the scheduler into the request context via middleware.
|
|
8916
|
+
*
|
|
8917
|
+
* @param core - PlanetCore instance providing configuration and service container.
|
|
8455
8918
|
*
|
|
8456
|
-
* @
|
|
8919
|
+
* @throws {Error} If the cache driver is explicitly requested but `CacheManager` is unavailable.
|
|
8457
8920
|
*/
|
|
8458
8921
|
install(core) {
|
|
8459
8922
|
const config = core.config.get("scheduler", {});
|