@gravito/horizon 3.0.0 → 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 +634 -243
- package/dist/index.d.cts +661 -137
- package/dist/index.d.ts +661 -137
- package/dist/index.js +637 -246
- package/package.json +7 -5
package/dist/index.cjs
CHANGED
|
@@ -7822,128 +7822,118 @@ __export(index_exports, {
|
|
|
7822
7822
|
module.exports = __toCommonJS(index_exports);
|
|
7823
7823
|
|
|
7824
7824
|
// src/SimpleCronParser.ts
|
|
7825
|
-
var SimpleCronParser = {
|
|
7825
|
+
var SimpleCronParser = class {
|
|
7826
7826
|
/**
|
|
7827
|
-
*
|
|
7828
|
-
* Only supports standard 5-field cron expressions.
|
|
7827
|
+
* Evaluates if a cron expression matches the specified date and timezone.
|
|
7829
7828
|
*
|
|
7830
|
-
*
|
|
7831
|
-
*
|
|
7832
|
-
*
|
|
7833
|
-
*
|
|
7834
|
-
*
|
|
7835
|
-
*
|
|
7836
|
-
*
|
|
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
|
+
* ```
|
|
7837
7839
|
*/
|
|
7838
|
-
isDue(expression, timezone, date) {
|
|
7839
|
-
const
|
|
7840
|
-
if (
|
|
7841
|
-
throw new Error(
|
|
7842
|
-
}
|
|
7843
|
-
let targetDate = date;
|
|
7844
|
-
if (timezone && timezone !== "UTC") {
|
|
7845
|
-
try {
|
|
7846
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
7847
|
-
timeZone: timezone,
|
|
7848
|
-
year: "numeric",
|
|
7849
|
-
month: "numeric",
|
|
7850
|
-
day: "numeric",
|
|
7851
|
-
hour: "numeric",
|
|
7852
|
-
minute: "numeric",
|
|
7853
|
-
second: "numeric",
|
|
7854
|
-
hour12: false
|
|
7855
|
-
}).formatToParts(date);
|
|
7856
|
-
const partMap = {};
|
|
7857
|
-
parts.forEach((p) => {
|
|
7858
|
-
partMap[p.type] = p.value;
|
|
7859
|
-
});
|
|
7860
|
-
targetDate = new Date(
|
|
7861
|
-
parseInt(partMap.year ?? "0", 10),
|
|
7862
|
-
parseInt(partMap.month ?? "1", 10) - 1,
|
|
7863
|
-
parseInt(partMap.day ?? "1", 10),
|
|
7864
|
-
parseInt(partMap.hour ?? "0", 10) === 24 ? 0 : parseInt(partMap.hour ?? "0", 10),
|
|
7865
|
-
parseInt(partMap.minute ?? "0", 10),
|
|
7866
|
-
0
|
|
7867
|
-
);
|
|
7868
|
-
} catch (_e) {
|
|
7869
|
-
throw new Error(`Invalid timezone: ${timezone}`);
|
|
7870
|
-
}
|
|
7871
|
-
} else if (timezone === "UTC") {
|
|
7872
|
-
targetDate = new Date(
|
|
7873
|
-
date.getUTCFullYear(),
|
|
7874
|
-
date.getUTCMonth(),
|
|
7875
|
-
date.getUTCDate(),
|
|
7876
|
-
date.getUTCHours(),
|
|
7877
|
-
date.getUTCMinutes(),
|
|
7878
|
-
0
|
|
7879
|
-
);
|
|
7880
|
-
}
|
|
7881
|
-
const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
|
|
7882
|
-
if (minute === void 0 || hour === void 0 || dayOfMonth === void 0 || month === void 0 || dayOfWeek === void 0) {
|
|
7883
|
-
throw new Error("Invalid cron expression");
|
|
7840
|
+
static isDue(expression, timezone = "UTC", date = /* @__PURE__ */ new Date()) {
|
|
7841
|
+
const parts = expression.trim().split(/\s+/);
|
|
7842
|
+
if (parts.length !== 5) {
|
|
7843
|
+
throw new Error(`Invalid cron expression: ${expression}`);
|
|
7884
7844
|
}
|
|
7885
|
-
const
|
|
7886
|
-
const
|
|
7887
|
-
const
|
|
7888
|
-
const
|
|
7889
|
-
const
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
};
|
|
7893
|
-
function matchField(pattern, value, min, max) {
|
|
7894
|
-
if (pattern === "*") {
|
|
7895
|
-
return true;
|
|
7845
|
+
const targetDate = this.getDateInTimezone(date, timezone);
|
|
7846
|
+
const minutes = targetDate.getMinutes();
|
|
7847
|
+
const hours = targetDate.getHours();
|
|
7848
|
+
const dayOfMonth = targetDate.getDate();
|
|
7849
|
+
const month = targetDate.getMonth() + 1;
|
|
7850
|
+
const dayOfWeek = targetDate.getDay();
|
|
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);
|
|
7896
7852
|
}
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
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
|
+
*/
|
|
7865
|
+
static match(pattern, value, _min, _max, isDayOfWeek = false) {
|
|
7866
|
+
if (pattern === "*") {
|
|
7867
|
+
return true;
|
|
7901
7868
|
}
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
return (value - min) % step === 0;
|
|
7869
|
+
if (pattern.includes(",")) {
|
|
7870
|
+
return pattern.split(",").some((p) => this.match(p, value, _min, _max, isDayOfWeek));
|
|
7905
7871
|
}
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
const end = parseInt(endStr, 10);
|
|
7913
|
-
if (value >= start && value <= end) {
|
|
7914
|
-
return (value - start) % step === 0;
|
|
7872
|
+
const stepMatch = pattern.match(/^(\*|\d+(-\d+)?)\/(\d+)$/);
|
|
7873
|
+
if (stepMatch) {
|
|
7874
|
+
const range = stepMatch[1];
|
|
7875
|
+
const step = parseInt(stepMatch[3], 10);
|
|
7876
|
+
if (range === "*") {
|
|
7877
|
+
return value % step === 0;
|
|
7915
7878
|
}
|
|
7916
|
-
|
|
7879
|
+
const [rMin, rMax] = range.split("-").map((n) => parseInt(n, 10));
|
|
7880
|
+
return value >= rMin && value <= rMax && (value - rMin) % step === 0;
|
|
7917
7881
|
}
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
return parts.some((part) => matchField(part, value, min, max));
|
|
7922
|
-
}
|
|
7923
|
-
if (pattern.includes("-")) {
|
|
7924
|
-
const [startStr, endStr] = pattern.split("-");
|
|
7925
|
-
if (startStr === void 0 || endStr === void 0) {
|
|
7926
|
-
return false;
|
|
7882
|
+
if (pattern.includes("-")) {
|
|
7883
|
+
const [rMin, rMax] = pattern.split("-").map((n) => parseInt(n, 10));
|
|
7884
|
+
return value >= rMin && value <= rMax;
|
|
7927
7885
|
}
|
|
7928
|
-
const
|
|
7929
|
-
|
|
7930
|
-
|
|
7886
|
+
const patternVal = parseInt(pattern, 10);
|
|
7887
|
+
if (isDayOfWeek && patternVal === 7 && value === 0) {
|
|
7888
|
+
return true;
|
|
7889
|
+
}
|
|
7890
|
+
return patternVal === value;
|
|
7931
7891
|
}
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
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
|
+
*/
|
|
7902
|
+
static getDateInTimezone(date, timezone) {
|
|
7903
|
+
try {
|
|
7904
|
+
const tzDate = new Date(date.toLocaleString("en-US", { timeZone: timezone }));
|
|
7905
|
+
if (Number.isNaN(tzDate.getTime())) {
|
|
7906
|
+
throw new Error();
|
|
7907
|
+
}
|
|
7908
|
+
return tzDate;
|
|
7909
|
+
} catch {
|
|
7910
|
+
throw new Error(`Invalid timezone: ${timezone}`);
|
|
7911
|
+
}
|
|
7935
7912
|
}
|
|
7936
|
-
|
|
7937
|
-
}
|
|
7913
|
+
};
|
|
7938
7914
|
|
|
7939
7915
|
// src/CronParser.ts
|
|
7940
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;
|
|
7941
7922
|
/**
|
|
7942
|
-
*
|
|
7923
|
+
* Calculates the next occurrence of a cron expression.
|
|
7943
7924
|
*
|
|
7944
|
-
*
|
|
7945
|
-
*
|
|
7946
|
-
* @param
|
|
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
|
+
* ```
|
|
7947
7937
|
*/
|
|
7948
7938
|
static async nextDate(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
|
|
7949
7939
|
try {
|
|
@@ -7958,13 +7948,47 @@ var CronParser = class {
|
|
|
7958
7948
|
}
|
|
7959
7949
|
}
|
|
7960
7950
|
/**
|
|
7961
|
-
*
|
|
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.
|
|
7962
7955
|
*
|
|
7963
|
-
* @param expression - Cron expression
|
|
7964
|
-
* @param timezone -
|
|
7965
|
-
* @param currentDate - Reference
|
|
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.
|
|
7966
7960
|
*/
|
|
7967
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) {
|
|
7968
7992
|
try {
|
|
7969
7993
|
return SimpleCronParser.isDue(expression, timezone, currentDate);
|
|
7970
7994
|
} catch (_e) {
|
|
@@ -7982,6 +8006,33 @@ var CronParser = class {
|
|
|
7982
8006
|
return false;
|
|
7983
8007
|
}
|
|
7984
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
|
+
*/
|
|
7985
8036
|
static minuteMatches(date1, date2) {
|
|
7986
8037
|
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() && date1.getHours() === date2.getHours() && date1.getMinutes() === date2.getMinutes();
|
|
7987
8038
|
}
|
|
@@ -7989,22 +8040,55 @@ var CronParser = class {
|
|
|
7989
8040
|
|
|
7990
8041
|
// src/locks/CacheLockStore.ts
|
|
7991
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
|
+
*/
|
|
7992
8049
|
constructor(cache, prefix = "scheduler:lock:") {
|
|
7993
8050
|
this.cache = cache;
|
|
7994
8051
|
this.prefix = prefix;
|
|
7995
8052
|
}
|
|
8053
|
+
/**
|
|
8054
|
+
* Computes the fully qualified cache key.
|
|
8055
|
+
*
|
|
8056
|
+
* @internal
|
|
8057
|
+
*/
|
|
7996
8058
|
getKey(key) {
|
|
7997
8059
|
return this.prefix + key;
|
|
7998
8060
|
}
|
|
8061
|
+
/**
|
|
8062
|
+
* Atomic 'add' operation ensures only one node succeeds.
|
|
8063
|
+
*
|
|
8064
|
+
* @param key - Lock key.
|
|
8065
|
+
* @param ttlSeconds - Expiration.
|
|
8066
|
+
*/
|
|
7999
8067
|
async acquire(key, ttlSeconds) {
|
|
8000
8068
|
return this.cache.add(this.getKey(key), "LOCKED", ttlSeconds);
|
|
8001
8069
|
}
|
|
8070
|
+
/**
|
|
8071
|
+
* Removes the lock key from cache.
|
|
8072
|
+
*
|
|
8073
|
+
* @param key - Lock key.
|
|
8074
|
+
*/
|
|
8002
8075
|
async release(key) {
|
|
8003
8076
|
await this.cache.forget(this.getKey(key));
|
|
8004
8077
|
}
|
|
8078
|
+
/**
|
|
8079
|
+
* Overwrites the lock key, effectively resetting the TTL.
|
|
8080
|
+
*
|
|
8081
|
+
* @param key - Lock key.
|
|
8082
|
+
* @param ttlSeconds - New expiration.
|
|
8083
|
+
*/
|
|
8005
8084
|
async forceAcquire(key, ttlSeconds) {
|
|
8006
8085
|
await this.cache.put(this.getKey(key), "LOCKED", ttlSeconds);
|
|
8007
8086
|
}
|
|
8087
|
+
/**
|
|
8088
|
+
* Validates if the lock key is present in the cache.
|
|
8089
|
+
*
|
|
8090
|
+
* @param key - Lock key.
|
|
8091
|
+
*/
|
|
8008
8092
|
async exists(key) {
|
|
8009
8093
|
return this.cache.has(this.getKey(key));
|
|
8010
8094
|
}
|
|
@@ -8012,7 +8096,14 @@ var CacheLockStore = class {
|
|
|
8012
8096
|
|
|
8013
8097
|
// src/locks/MemoryLockStore.ts
|
|
8014
8098
|
var MemoryLockStore = class {
|
|
8099
|
+
/** Map of lock keys to their expiration timestamps (ms). */
|
|
8015
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
|
+
*/
|
|
8016
8107
|
async acquire(key, ttlSeconds) {
|
|
8017
8108
|
const NOW = Date.now();
|
|
8018
8109
|
const expiresAt = this.locks.get(key);
|
|
@@ -8022,12 +8113,30 @@ var MemoryLockStore = class {
|
|
|
8022
8113
|
this.locks.set(key, NOW + ttlSeconds * 1e3);
|
|
8023
8114
|
return true;
|
|
8024
8115
|
}
|
|
8116
|
+
/**
|
|
8117
|
+
* Deletes the lock from local memory.
|
|
8118
|
+
*
|
|
8119
|
+
* @param key - Lock identifier.
|
|
8120
|
+
*/
|
|
8025
8121
|
async release(key) {
|
|
8026
8122
|
this.locks.delete(key);
|
|
8027
8123
|
}
|
|
8124
|
+
/**
|
|
8125
|
+
* Sets or overwrites a local lock.
|
|
8126
|
+
*
|
|
8127
|
+
* @param key - Lock identifier.
|
|
8128
|
+
* @param ttlSeconds - Expiration.
|
|
8129
|
+
*/
|
|
8028
8130
|
async forceAcquire(key, ttlSeconds) {
|
|
8029
8131
|
this.locks.set(key, Date.now() + ttlSeconds * 1e3);
|
|
8030
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
|
+
*/
|
|
8031
8140
|
async exists(key) {
|
|
8032
8141
|
const expiresAt = this.locks.get(key);
|
|
8033
8142
|
if (!expiresAt) {
|
|
@@ -8044,6 +8153,13 @@ var MemoryLockStore = class {
|
|
|
8044
8153
|
// src/locks/LockManager.ts
|
|
8045
8154
|
var LockManager = class {
|
|
8046
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
|
+
*/
|
|
8047
8163
|
constructor(driver, context) {
|
|
8048
8164
|
if (typeof driver === "object") {
|
|
8049
8165
|
this.store = driver;
|
|
@@ -8058,15 +8174,43 @@ var LockManager = class {
|
|
|
8058
8174
|
this.store = new MemoryLockStore();
|
|
8059
8175
|
}
|
|
8060
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
|
+
*/
|
|
8061
8184
|
async acquire(key, ttlSeconds) {
|
|
8062
8185
|
return this.store.acquire(key, ttlSeconds);
|
|
8063
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
|
+
*/
|
|
8064
8193
|
async release(key) {
|
|
8065
8194
|
return this.store.release(key);
|
|
8066
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
|
+
*/
|
|
8067
8205
|
async forceAcquire(key, ttlSeconds) {
|
|
8068
8206
|
return this.store.forceAcquire(key, ttlSeconds);
|
|
8069
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
|
+
*/
|
|
8070
8214
|
async exists(key) {
|
|
8071
8215
|
return this.store.exists(key);
|
|
8072
8216
|
}
|
|
@@ -8093,19 +8237,55 @@ async function runProcess(command) {
|
|
|
8093
8237
|
};
|
|
8094
8238
|
}
|
|
8095
8239
|
var Process = class {
|
|
8240
|
+
/**
|
|
8241
|
+
* Static alias for `runProcess`.
|
|
8242
|
+
*
|
|
8243
|
+
* @param command - Command to execute.
|
|
8244
|
+
* @returns Process outcome.
|
|
8245
|
+
*/
|
|
8096
8246
|
static async run(command) {
|
|
8097
8247
|
return runProcess(command);
|
|
8098
8248
|
}
|
|
8099
8249
|
};
|
|
8100
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
|
+
|
|
8101
8281
|
// src/TaskSchedule.ts
|
|
8102
8282
|
var TaskSchedule = class {
|
|
8103
8283
|
task;
|
|
8104
8284
|
/**
|
|
8105
|
-
*
|
|
8285
|
+
* Initializes a new schedule instance with default settings.
|
|
8106
8286
|
*
|
|
8107
|
-
* @param name -
|
|
8108
|
-
* @param callback -
|
|
8287
|
+
* @param name - Unique task name used for identification and locking.
|
|
8288
|
+
* @param callback - Logic to execute on schedule.
|
|
8109
8289
|
*/
|
|
8110
8290
|
constructor(name, callback) {
|
|
8111
8291
|
this.task = {
|
|
@@ -8119,168 +8299,217 @@ var TaskSchedule = class {
|
|
|
8119
8299
|
// 5 minutes default
|
|
8120
8300
|
background: false,
|
|
8121
8301
|
// Wait for task to finish by default
|
|
8302
|
+
preventOverlapping: false,
|
|
8303
|
+
overlappingExpiresAt: 3600,
|
|
8304
|
+
// 1 hour default
|
|
8122
8305
|
onSuccessCallbacks: [],
|
|
8123
8306
|
onFailureCallbacks: []
|
|
8124
8307
|
};
|
|
8125
8308
|
}
|
|
8126
8309
|
// --- Frequency Methods ---
|
|
8127
8310
|
/**
|
|
8128
|
-
*
|
|
8311
|
+
* Configures a raw 5-part cron expression.
|
|
8312
|
+
*
|
|
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.
|
|
8129
8316
|
*
|
|
8130
|
-
* @
|
|
8131
|
-
*
|
|
8317
|
+
* @example
|
|
8318
|
+
* ```typescript
|
|
8319
|
+
* schedule.cron('0 9-17 * * 1-5'); // 9-5 on weekdays
|
|
8320
|
+
* ```
|
|
8132
8321
|
*/
|
|
8133
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
|
+
}
|
|
8134
8337
|
this.task.expression = expression;
|
|
8135
8338
|
return this;
|
|
8136
8339
|
}
|
|
8137
8340
|
/**
|
|
8138
|
-
*
|
|
8341
|
+
* Schedules execution every minute.
|
|
8139
8342
|
*
|
|
8140
|
-
* @returns The TaskSchedule instance.
|
|
8343
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8141
8344
|
*/
|
|
8142
8345
|
everyMinute() {
|
|
8143
8346
|
return this.cron("* * * * *");
|
|
8144
8347
|
}
|
|
8145
8348
|
/**
|
|
8146
|
-
*
|
|
8349
|
+
* Schedules execution every five minutes.
|
|
8147
8350
|
*
|
|
8148
|
-
* @returns The TaskSchedule instance.
|
|
8351
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8149
8352
|
*/
|
|
8150
8353
|
everyFiveMinutes() {
|
|
8151
8354
|
return this.cron("*/5 * * * *");
|
|
8152
8355
|
}
|
|
8153
8356
|
/**
|
|
8154
|
-
*
|
|
8357
|
+
* Schedules execution every ten minutes.
|
|
8155
8358
|
*
|
|
8156
|
-
* @returns The TaskSchedule instance.
|
|
8359
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8157
8360
|
*/
|
|
8158
8361
|
everyTenMinutes() {
|
|
8159
8362
|
return this.cron("*/10 * * * *");
|
|
8160
8363
|
}
|
|
8161
8364
|
/**
|
|
8162
|
-
*
|
|
8365
|
+
* Schedules execution every fifteen minutes.
|
|
8163
8366
|
*
|
|
8164
|
-
* @returns The TaskSchedule instance.
|
|
8367
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8165
8368
|
*/
|
|
8166
8369
|
everyFifteenMinutes() {
|
|
8167
8370
|
return this.cron("*/15 * * * *");
|
|
8168
8371
|
}
|
|
8169
8372
|
/**
|
|
8170
|
-
*
|
|
8373
|
+
* Schedules execution every thirty minutes.
|
|
8171
8374
|
*
|
|
8172
|
-
* @returns The TaskSchedule instance.
|
|
8375
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8173
8376
|
*/
|
|
8174
8377
|
everyThirtyMinutes() {
|
|
8175
8378
|
return this.cron("0,30 * * * *");
|
|
8176
8379
|
}
|
|
8177
8380
|
/**
|
|
8178
|
-
*
|
|
8381
|
+
* Schedules execution hourly at the top of the hour.
|
|
8179
8382
|
*
|
|
8180
|
-
* @returns The TaskSchedule instance.
|
|
8383
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8181
8384
|
*/
|
|
8182
8385
|
hourly() {
|
|
8183
8386
|
return this.cron("0 * * * *");
|
|
8184
8387
|
}
|
|
8185
8388
|
/**
|
|
8186
|
-
*
|
|
8389
|
+
* Schedules execution hourly at a specific minute.
|
|
8187
8390
|
*
|
|
8188
|
-
* @param minute -
|
|
8189
|
-
* @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.
|
|
8190
8394
|
*/
|
|
8191
8395
|
hourlyAt(minute) {
|
|
8396
|
+
validateMinute(minute);
|
|
8192
8397
|
return this.cron(`${minute} * * * *`);
|
|
8193
8398
|
}
|
|
8194
8399
|
/**
|
|
8195
|
-
*
|
|
8400
|
+
* Schedules execution daily at midnight.
|
|
8196
8401
|
*
|
|
8197
|
-
* @returns The TaskSchedule instance.
|
|
8402
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8198
8403
|
*/
|
|
8199
8404
|
daily() {
|
|
8200
8405
|
return this.cron("0 0 * * *");
|
|
8201
8406
|
}
|
|
8202
8407
|
/**
|
|
8203
|
-
*
|
|
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.
|
|
8204
8413
|
*
|
|
8205
|
-
* @
|
|
8206
|
-
*
|
|
8414
|
+
* @example
|
|
8415
|
+
* ```typescript
|
|
8416
|
+
* schedule.dailyAt('14:30');
|
|
8417
|
+
* ```
|
|
8207
8418
|
*/
|
|
8208
8419
|
dailyAt(time) {
|
|
8209
|
-
const
|
|
8210
|
-
return this.cron(`${
|
|
8420
|
+
const { hour, minute } = parseTime(time);
|
|
8421
|
+
return this.cron(`${minute} ${hour} * * *`);
|
|
8211
8422
|
}
|
|
8212
8423
|
/**
|
|
8213
|
-
*
|
|
8424
|
+
* Schedules execution weekly on Sunday at midnight.
|
|
8214
8425
|
*
|
|
8215
|
-
* @returns The TaskSchedule instance.
|
|
8426
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8216
8427
|
*/
|
|
8217
8428
|
weekly() {
|
|
8218
8429
|
return this.cron("0 0 * * 0");
|
|
8219
8430
|
}
|
|
8220
8431
|
/**
|
|
8221
|
-
*
|
|
8432
|
+
* Schedules execution weekly on a specific day and time.
|
|
8222
8433
|
*
|
|
8223
|
-
* @param day - Day
|
|
8224
|
-
* @param time -
|
|
8225
|
-
* @returns The TaskSchedule instance.
|
|
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.
|
|
8438
|
+
*
|
|
8439
|
+
* @example
|
|
8440
|
+
* ```typescript
|
|
8441
|
+
* schedule.weeklyOn(1, '09:00'); // Mondays at 9 AM
|
|
8442
|
+
* ```
|
|
8226
8443
|
*/
|
|
8227
8444
|
weeklyOn(day, time = "00:00") {
|
|
8228
|
-
|
|
8229
|
-
|
|
8445
|
+
validateDayOfWeek(day);
|
|
8446
|
+
const { hour, minute } = parseTime(time);
|
|
8447
|
+
return this.cron(`${minute} ${hour} * * ${day}`);
|
|
8230
8448
|
}
|
|
8231
8449
|
/**
|
|
8232
|
-
*
|
|
8450
|
+
* Schedules execution monthly on the first day at midnight.
|
|
8233
8451
|
*
|
|
8234
|
-
* @returns The TaskSchedule instance.
|
|
8452
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8235
8453
|
*/
|
|
8236
8454
|
monthly() {
|
|
8237
8455
|
return this.cron("0 0 1 * *");
|
|
8238
8456
|
}
|
|
8239
8457
|
/**
|
|
8240
|
-
*
|
|
8458
|
+
* Schedules execution monthly on a specific day and time.
|
|
8241
8459
|
*
|
|
8242
|
-
* @param day - Day of month (1-31)
|
|
8243
|
-
* @param time -
|
|
8244
|
-
* @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.
|
|
8245
8464
|
*/
|
|
8246
8465
|
monthlyOn(day, time = "00:00") {
|
|
8247
|
-
|
|
8248
|
-
|
|
8466
|
+
validateDayOfMonth(day);
|
|
8467
|
+
const { hour, minute } = parseTime(time);
|
|
8468
|
+
return this.cron(`${minute} ${hour} ${day} * *`);
|
|
8249
8469
|
}
|
|
8250
8470
|
// --- Constraints ---
|
|
8251
8471
|
/**
|
|
8252
|
-
*
|
|
8472
|
+
* Specifies the timezone for evaluating schedule frequency.
|
|
8253
8473
|
*
|
|
8254
|
-
* @param timezone -
|
|
8255
|
-
* @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.
|
|
8256
8477
|
*/
|
|
8257
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
|
+
}
|
|
8258
8486
|
this.task.timezone = timezone;
|
|
8259
8487
|
return this;
|
|
8260
8488
|
}
|
|
8261
8489
|
/**
|
|
8262
|
-
*
|
|
8263
|
-
*
|
|
8490
|
+
* Modifies the execution time of the existing frequency.
|
|
8491
|
+
* Typically used after frequency methods like daily() or weekly().
|
|
8264
8492
|
*
|
|
8265
|
-
* @param time -
|
|
8266
|
-
* @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.
|
|
8267
8496
|
*/
|
|
8268
8497
|
at(time) {
|
|
8269
|
-
const
|
|
8498
|
+
const { hour, minute } = parseTime(time);
|
|
8270
8499
|
const parts = this.task.expression.split(" ");
|
|
8271
8500
|
if (parts.length >= 5) {
|
|
8272
|
-
parts[0] = String(
|
|
8273
|
-
parts[1] = String(
|
|
8501
|
+
parts[0] = String(minute);
|
|
8502
|
+
parts[1] = String(hour);
|
|
8274
8503
|
this.task.expression = parts.join(" ");
|
|
8275
8504
|
}
|
|
8276
8505
|
return this;
|
|
8277
8506
|
}
|
|
8278
8507
|
/**
|
|
8279
|
-
*
|
|
8280
|
-
*
|
|
8508
|
+
* Enables distributed locking to ensure only one instance runs globally.
|
|
8509
|
+
* Prevents duplicate execution when multiple servers are polling the same schedule.
|
|
8281
8510
|
*
|
|
8282
|
-
* @param lockTtlSeconds -
|
|
8283
|
-
* @returns The TaskSchedule instance.
|
|
8511
|
+
* @param lockTtlSeconds - Duration in seconds to hold the window lock (default 300).
|
|
8512
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8284
8513
|
*/
|
|
8285
8514
|
onOneServer(lockTtlSeconds = 300) {
|
|
8286
8515
|
this.task.shouldRunOnOneServer = true;
|
|
@@ -8288,41 +8517,85 @@ var TaskSchedule = class {
|
|
|
8288
8517
|
return this;
|
|
8289
8518
|
}
|
|
8290
8519
|
/**
|
|
8291
|
-
*
|
|
8292
|
-
*
|
|
8520
|
+
* Prevents a new task instance from starting if the previous one is still running.
|
|
8521
|
+
*
|
|
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.
|
|
8293
8527
|
*
|
|
8294
|
-
* @
|
|
8295
|
-
*
|
|
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
|
+
* ```
|
|
8296
8535
|
*/
|
|
8297
|
-
withoutOverlapping(expiresAt =
|
|
8298
|
-
|
|
8536
|
+
withoutOverlapping(expiresAt = 3600) {
|
|
8537
|
+
this.task.preventOverlapping = true;
|
|
8538
|
+
this.task.overlappingExpiresAt = expiresAt;
|
|
8539
|
+
return this;
|
|
8299
8540
|
}
|
|
8300
8541
|
/**
|
|
8301
|
-
*
|
|
8302
|
-
*
|
|
8303
|
-
* 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.
|
|
8304
8544
|
*
|
|
8305
|
-
* @returns The TaskSchedule instance.
|
|
8545
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8306
8546
|
*/
|
|
8307
8547
|
runInBackground() {
|
|
8308
8548
|
this.task.background = true;
|
|
8309
8549
|
return this;
|
|
8310
8550
|
}
|
|
8311
8551
|
/**
|
|
8312
|
-
*
|
|
8552
|
+
* Restricts task execution to nodes with a specific role.
|
|
8313
8553
|
*
|
|
8314
|
-
* @param role -
|
|
8315
|
-
* @returns The TaskSchedule instance.
|
|
8554
|
+
* @param role - Target role identifier (e.g., "worker").
|
|
8555
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8316
8556
|
*/
|
|
8317
8557
|
onNode(role) {
|
|
8318
8558
|
this.task.nodeRole = role;
|
|
8319
8559
|
return this;
|
|
8320
8560
|
}
|
|
8321
8561
|
/**
|
|
8322
|
-
*
|
|
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.
|
|
8323
8596
|
*
|
|
8324
8597
|
* @param command - The command string.
|
|
8325
|
-
* @returns The TaskSchedule instance.
|
|
8598
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8326
8599
|
* @internal
|
|
8327
8600
|
*/
|
|
8328
8601
|
setCommand(command) {
|
|
@@ -8331,39 +8604,39 @@ var TaskSchedule = class {
|
|
|
8331
8604
|
}
|
|
8332
8605
|
// --- Hooks ---
|
|
8333
8606
|
/**
|
|
8334
|
-
*
|
|
8607
|
+
* Registers a callback to execute upon successful task completion.
|
|
8335
8608
|
*
|
|
8336
|
-
* @param callback -
|
|
8337
|
-
* @returns The TaskSchedule instance.
|
|
8609
|
+
* @param callback - Function to run on success.
|
|
8610
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8338
8611
|
*/
|
|
8339
8612
|
onSuccess(callback) {
|
|
8340
8613
|
this.task.onSuccessCallbacks.push(callback);
|
|
8341
8614
|
return this;
|
|
8342
8615
|
}
|
|
8343
8616
|
/**
|
|
8344
|
-
*
|
|
8617
|
+
* Registers a callback to execute when the task fails.
|
|
8345
8618
|
*
|
|
8346
|
-
* @param callback -
|
|
8347
|
-
* @returns The TaskSchedule instance.
|
|
8619
|
+
* @param callback - Function to run on failure.
|
|
8620
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8348
8621
|
*/
|
|
8349
8622
|
onFailure(callback) {
|
|
8350
8623
|
this.task.onFailureCallbacks.push(callback);
|
|
8351
8624
|
return this;
|
|
8352
8625
|
}
|
|
8353
8626
|
/**
|
|
8354
|
-
*
|
|
8627
|
+
* Attaches a human-readable description to the task.
|
|
8355
8628
|
*
|
|
8356
|
-
* @param _text -
|
|
8357
|
-
* @returns The TaskSchedule instance.
|
|
8629
|
+
* @param _text - Description text.
|
|
8630
|
+
* @returns The TaskSchedule instance for chaining.
|
|
8358
8631
|
*/
|
|
8359
8632
|
description(_text) {
|
|
8360
8633
|
return this;
|
|
8361
8634
|
}
|
|
8362
8635
|
// --- Accessor ---
|
|
8363
8636
|
/**
|
|
8364
|
-
*
|
|
8637
|
+
* Returns the final task configuration.
|
|
8365
8638
|
*
|
|
8366
|
-
* @returns The ScheduledTask object.
|
|
8639
|
+
* @returns The constructed ScheduledTask object.
|
|
8367
8640
|
*/
|
|
8368
8641
|
getTask() {
|
|
8369
8642
|
return this.task;
|
|
@@ -8372,6 +8645,14 @@ var TaskSchedule = class {
|
|
|
8372
8645
|
|
|
8373
8646
|
// src/SchedulerManager.ts
|
|
8374
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
|
+
*/
|
|
8375
8656
|
constructor(lockManager, logger, hooks, currentNodeRole) {
|
|
8376
8657
|
this.lockManager = lockManager;
|
|
8377
8658
|
this.logger = logger;
|
|
@@ -8380,11 +8661,18 @@ var SchedulerManager = class {
|
|
|
8380
8661
|
}
|
|
8381
8662
|
tasks = [];
|
|
8382
8663
|
/**
|
|
8383
|
-
*
|
|
8664
|
+
* Registers a new callback-based scheduled task.
|
|
8665
|
+
*
|
|
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.
|
|
8384
8669
|
*
|
|
8385
|
-
* @
|
|
8386
|
-
*
|
|
8387
|
-
*
|
|
8670
|
+
* @example
|
|
8671
|
+
* ```typescript
|
|
8672
|
+
* scheduler.task('process-queues', async () => {
|
|
8673
|
+
* await queue.process();
|
|
8674
|
+
* }).everyMinute();
|
|
8675
|
+
* ```
|
|
8388
8676
|
*/
|
|
8389
8677
|
task(name, callback) {
|
|
8390
8678
|
const task = new TaskSchedule(name, callback);
|
|
@@ -8392,11 +8680,21 @@ var SchedulerManager = class {
|
|
|
8392
8680
|
return task;
|
|
8393
8681
|
}
|
|
8394
8682
|
/**
|
|
8395
|
-
*
|
|
8683
|
+
* Registers a shell command as a scheduled task.
|
|
8396
8684
|
*
|
|
8397
|
-
*
|
|
8398
|
-
*
|
|
8399
|
-
* @
|
|
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.
|
|
8691
|
+
*
|
|
8692
|
+
* @example
|
|
8693
|
+
* ```typescript
|
|
8694
|
+
* scheduler.exec('log-rotate', 'logrotate /etc/logrotate.conf')
|
|
8695
|
+
* .daily()
|
|
8696
|
+
* .onNode('worker');
|
|
8697
|
+
* ```
|
|
8400
8698
|
*/
|
|
8401
8699
|
exec(name, command) {
|
|
8402
8700
|
const task = new TaskSchedule(name, async () => {
|
|
@@ -8410,27 +8708,29 @@ var SchedulerManager = class {
|
|
|
8410
8708
|
return task;
|
|
8411
8709
|
}
|
|
8412
8710
|
/**
|
|
8413
|
-
*
|
|
8711
|
+
* Injects a pre-configured TaskSchedule instance into the registry.
|
|
8414
8712
|
*
|
|
8415
|
-
* @param schedule -
|
|
8713
|
+
* @param schedule - Configured task schedule to register.
|
|
8416
8714
|
*/
|
|
8417
8715
|
add(schedule) {
|
|
8418
8716
|
this.tasks.push(schedule);
|
|
8419
8717
|
}
|
|
8420
8718
|
/**
|
|
8421
|
-
*
|
|
8719
|
+
* Exports all registered tasks for external inspection or serialization.
|
|
8422
8720
|
*
|
|
8423
|
-
* @returns An array of
|
|
8721
|
+
* @returns An array of raw task configurations.
|
|
8424
8722
|
*/
|
|
8425
8723
|
getTasks() {
|
|
8426
8724
|
return this.tasks.map((t) => t.getTask());
|
|
8427
8725
|
}
|
|
8428
8726
|
/**
|
|
8429
|
-
*
|
|
8430
|
-
*
|
|
8727
|
+
* Main evaluation loop that triggers tasks due for execution.
|
|
8728
|
+
*
|
|
8729
|
+
* Should be invoked every minute by a system timer (systemd/cron) or daemon.
|
|
8730
|
+
* Performs frequency checks, role filtering, and parallel execution.
|
|
8431
8731
|
*
|
|
8432
|
-
* @param date -
|
|
8433
|
-
* @returns
|
|
8732
|
+
* @param date - Reference time for cron evaluation (default: current time).
|
|
8733
|
+
* @returns Resolves when all due tasks have been initiated.
|
|
8434
8734
|
*/
|
|
8435
8735
|
async run(date = /* @__PURE__ */ new Date()) {
|
|
8436
8736
|
await this.hooks?.doAction("scheduler:run:start", { date });
|
|
@@ -8442,6 +8742,14 @@ var SchedulerManager = class {
|
|
|
8442
8742
|
}
|
|
8443
8743
|
}
|
|
8444
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
|
+
});
|
|
8445
8753
|
}
|
|
8446
8754
|
for (const task of dueTasks) {
|
|
8447
8755
|
this.runTask(task, date).catch((err) => {
|
|
@@ -8451,21 +8759,45 @@ var SchedulerManager = class {
|
|
|
8451
8759
|
await this.hooks?.doAction("scheduler:run:complete", { date, dueCount: dueTasks.length });
|
|
8452
8760
|
}
|
|
8453
8761
|
/**
|
|
8454
|
-
*
|
|
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.
|
|
8455
8769
|
*
|
|
8456
|
-
* @param task - The task to execute.
|
|
8457
8770
|
* @internal
|
|
8458
8771
|
*/
|
|
8459
8772
|
async runTask(task, date = /* @__PURE__ */ new Date()) {
|
|
8460
8773
|
if (task.nodeRole && this.currentNodeRole && task.nodeRole !== this.currentNodeRole) {
|
|
8461
8774
|
return;
|
|
8462
8775
|
}
|
|
8463
|
-
|
|
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;
|
|
8464
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")}`;
|
|
8465
|
-
const
|
|
8794
|
+
const timeLockKey = `task:${task.name}:${timestamp}`;
|
|
8466
8795
|
if (task.shouldRunOnOneServer) {
|
|
8467
|
-
|
|
8468
|
-
if (!
|
|
8796
|
+
acquiredTimeLock = await this.lockManager.acquire(timeLockKey, task.lockTtl);
|
|
8797
|
+
if (!acquiredTimeLock) {
|
|
8798
|
+
if (task.preventOverlapping) {
|
|
8799
|
+
await this.lockManager.release(runningLockKey);
|
|
8800
|
+
}
|
|
8469
8801
|
return;
|
|
8470
8802
|
}
|
|
8471
8803
|
}
|
|
@@ -8473,59 +8805,118 @@ var SchedulerManager = class {
|
|
|
8473
8805
|
if (task.background) {
|
|
8474
8806
|
this.executeTask(task).catch((err) => {
|
|
8475
8807
|
this.logger?.error(`Background task ${task.name} failed`, err);
|
|
8808
|
+
}).finally(async () => {
|
|
8809
|
+
if (task.preventOverlapping) {
|
|
8810
|
+
await this.lockManager.release(runningLockKey);
|
|
8811
|
+
}
|
|
8476
8812
|
});
|
|
8477
8813
|
} else {
|
|
8478
8814
|
await this.executeTask(task);
|
|
8815
|
+
if (task.preventOverlapping) {
|
|
8816
|
+
await this.lockManager.release(runningLockKey);
|
|
8817
|
+
}
|
|
8479
8818
|
}
|
|
8480
8819
|
} catch (err) {
|
|
8481
|
-
if (
|
|
8482
|
-
await this.lockManager.release(
|
|
8820
|
+
if (task.preventOverlapping) {
|
|
8821
|
+
await this.lockManager.release(runningLockKey);
|
|
8822
|
+
}
|
|
8823
|
+
if (acquiredTimeLock) {
|
|
8824
|
+
await this.lockManager.release(timeLockKey);
|
|
8483
8825
|
}
|
|
8484
8826
|
throw err;
|
|
8485
8827
|
}
|
|
8486
8828
|
}
|
|
8487
8829
|
/**
|
|
8488
|
-
*
|
|
8830
|
+
* Internal wrapper for executing task logic with reliability controls.
|
|
8489
8831
|
*
|
|
8490
|
-
*
|
|
8832
|
+
* Handles timeouts, retries, and emits lifecycle hooks for monitoring.
|
|
8833
|
+
*
|
|
8834
|
+
* @param task - The scheduled task to execute.
|
|
8835
|
+
* @returns Resolves when the task (and its retries) completes or fails permanently.
|
|
8836
|
+
*
|
|
8837
|
+
* @internal
|
|
8491
8838
|
*/
|
|
8492
8839
|
async executeTask(task) {
|
|
8493
8840
|
const startTime = Date.now();
|
|
8494
8841
|
await this.hooks?.doAction("scheduler:task:start", { name: task.name, startTime });
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
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));
|
|
8504
8859
|
}
|
|
8505
|
-
|
|
8506
|
-
const
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
error: err,
|
|
8511
|
-
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);
|
|
8512
8865
|
});
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
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
|
+
}
|
|
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);
|
|
8517
8887
|
}
|
|
8518
8888
|
}
|
|
8519
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 {
|
|
8901
|
+
}
|
|
8902
|
+
}
|
|
8520
8903
|
}
|
|
8521
8904
|
};
|
|
8522
8905
|
|
|
8523
8906
|
// src/OrbitHorizon.ts
|
|
8524
8907
|
var OrbitHorizon = class {
|
|
8525
8908
|
/**
|
|
8526
|
-
*
|
|
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.
|
|
8527
8918
|
*
|
|
8528
|
-
* @
|
|
8919
|
+
* @throws {Error} If the cache driver is explicitly requested but `CacheManager` is unavailable.
|
|
8529
8920
|
*/
|
|
8530
8921
|
install(core) {
|
|
8531
8922
|
const config = core.config.get("scheduler", {});
|
|
@@ -8534,7 +8925,7 @@ var OrbitHorizon = class {
|
|
|
8534
8925
|
const nodeRole = config.nodeRole;
|
|
8535
8926
|
let lockManager;
|
|
8536
8927
|
if (lockDriver === "cache") {
|
|
8537
|
-
const cacheManager = core.
|
|
8928
|
+
const cacheManager = core.container.make("cache");
|
|
8538
8929
|
if (!cacheManager) {
|
|
8539
8930
|
core.logger.warn(
|
|
8540
8931
|
"[OrbitHorizon] Cache driver requested but cache service not found (ensure orbit-cache is loaded first). Falling back to Memory lock."
|
|
@@ -8547,7 +8938,7 @@ var OrbitHorizon = class {
|
|
|
8547
8938
|
lockManager = new LockManager("memory");
|
|
8548
8939
|
}
|
|
8549
8940
|
const scheduler = new SchedulerManager(lockManager, core.logger, core.hooks, nodeRole);
|
|
8550
|
-
core.
|
|
8941
|
+
core.container.instance(exposeAs, scheduler);
|
|
8551
8942
|
core.adapter.use("*", async (c, next) => {
|
|
8552
8943
|
c.set("scheduler", scheduler);
|
|
8553
8944
|
return await next();
|