@gravito/horizon 3.0.1 → 3.2.1

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/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
- * Check if a cron expression is due at the given date/time
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 === "*") return true;
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 === "*") return value % step === 0;
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) return true;
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())) throw new Error();
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
- * Get the next execution date based on a cron expression.
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
- * Check if the cron expression is due to run at the current time (minute precision).
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
- * Create a new TaskSchedule instance.
8285
+ * Initializes a new schedule instance with default settings.
8033
8286
  *
8034
- * @param name - The unique name of the task.
8035
- * @param callback - The function to execute.
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
- * Set a custom cron expression.
8311
+ * Configures a raw 5-part cron expression.
8056
8312
  *
8057
- * @param expression - Standard cron expression (e.g., "* * * * *")
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
- * Run the task every minute.
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
- * Run the task every 5 minutes.
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
- * Run the task every 10 minutes.
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
- * Run the task every 15 minutes.
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
- * Run the task every 30 minutes.
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
- * Run the task hourly (at minute 0).
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
- * Run the task hourly at a specific minute.
8389
+ * Schedules execution hourly at a specific minute.
8114
8390
  *
8115
- * @param minute - Minute (0-59)
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
- * Run the task daily at midnight (00:00).
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
- * Run the task daily at a specific time.
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
- * @param time - Time in "HH:mm" format (24h)
8133
- * @returns The TaskSchedule instance.
8414
+ * @example
8415
+ * ```typescript
8416
+ * schedule.dailyAt('14:30');
8417
+ * ```
8134
8418
  */
8135
8419
  dailyAt(time) {
8136
- const [hour, minute] = time.split(":");
8137
- return this.cron(`${Number(minute)} ${Number(hour)} * * *`);
8420
+ const { hour, minute } = parseTime(time);
8421
+ return this.cron(`${minute} ${hour} * * *`);
8138
8422
  }
8139
8423
  /**
8140
- * Run the task weekly on Sunday at midnight.
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
- * Run the task weekly on a specific day and time.
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
- * @param day - Day of week (0-7, 0 or 7 is Sunday)
8151
- * @param time - Time in "HH:mm" format (default "00:00")
8152
- * @returns The TaskSchedule instance.
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
- const [hour, minute] = time.split(":");
8156
- return this.cron(`${Number(minute)} ${Number(hour)} * * ${day}`);
8445
+ validateDayOfWeek(day);
8446
+ const { hour, minute } = parseTime(time);
8447
+ return this.cron(`${minute} ${hour} * * ${day}`);
8157
8448
  }
8158
8449
  /**
8159
- * Run the task monthly on the 1st day at midnight.
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
- * Run the task monthly on a specific day and time.
8458
+ * Schedules execution monthly on a specific day and time.
8168
8459
  *
8169
- * @param day - Day of month (1-31)
8170
- * @param time - Time in "HH:mm" format (default "00:00")
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
- const [hour, minute] = time.split(":");
8175
- return this.cron(`${Number(minute)} ${Number(hour)} ${day} * *`);
8466
+ validateDayOfMonth(day);
8467
+ const { hour, minute } = parseTime(time);
8468
+ return this.cron(`${minute} ${hour} ${day} * *`);
8176
8469
  }
8177
8470
  // --- Constraints ---
8178
8471
  /**
8179
- * Set the timezone for the task execution.
8472
+ * Specifies the timezone for evaluating schedule frequency.
8180
8473
  *
8181
- * @param timezone - Timezone identifier (e.g., "Asia/Taipei", "UTC")
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
- * Set the time of execution for the current frequency.
8190
- * Useful when chaining with daily(), weekly(), etc.
8490
+ * Modifies the execution time of the existing frequency.
8491
+ * Typically used after frequency methods like daily() or weekly().
8191
8492
  *
8192
- * @param time - Time in "HH:mm" format
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 [hour, minute] = time.split(":");
8498
+ const { hour, minute } = parseTime(time);
8197
8499
  const parts = this.task.expression.split(" ");
8198
8500
  if (parts.length >= 5) {
8199
- parts[0] = String(Number(minute));
8200
- parts[1] = String(Number(hour));
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
- * Ensure task runs on only one server at a time (Distributed Locking).
8207
- * Requires a configured LockStore (Cache or Redis).
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 - Time in seconds to hold the lock (default 300)
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
- * Alias for onOneServer.
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
- * @param expiresAt - Lock TTL in seconds
8222
- * @returns The TaskSchedule instance.
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 = 300) {
8225
- return this.onOneServer(expiresAt);
8536
+ withoutOverlapping(expiresAt = 3600) {
8537
+ this.task.preventOverlapping = true;
8538
+ this.task.overlappingExpiresAt = expiresAt;
8539
+ return this;
8226
8540
  }
8227
8541
  /**
8228
- * Run task in background (do not wait for completion in the loop).
8229
- * Note: In Node.js non-blocking environment this is largely semantic,
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
- * Restrict task execution to a specific node role.
8552
+ * Restricts task execution to nodes with a specific role.
8240
8553
  *
8241
- * @param role - The required node role (e.g., 'api', 'worker')
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
- * Set the command string for exec tasks.
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
- * Register a callback to run on task success.
8607
+ * Registers a callback to execute upon successful task completion.
8262
8608
  *
8263
- * @param callback - The callback function.
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
- * Register a callback to run on task failure.
8617
+ * Registers a callback to execute when the task fails.
8272
8618
  *
8273
- * @param callback - The callback function.
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
- * Set a description for the task (useful for listing).
8627
+ * Attaches a human-readable description to the task.
8282
8628
  *
8283
- * @param _text - The description 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
- * Get the underlying task configuration.
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
- * Define a new scheduled task.
8664
+ * Registers a new callback-based scheduled task.
8311
8665
  *
8312
- * @param name - Unique name for the task
8313
- * @param callback - Function to execute
8314
- * @returns The newly created TaskSchedule.
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
- * Define a new scheduled command execution task.
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
- * @param name - Unique name for the task
8325
- * @param command - Shell command to execute
8326
- * @returns The newly created TaskSchedule.
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
- * Add a pre-configured task schedule object.
8711
+ * Injects a pre-configured TaskSchedule instance into the registry.
8341
8712
  *
8342
- * @param schedule - The task schedule to add.
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
- * Get all registered task definitions.
8719
+ * Exports all registered tasks for external inspection or serialization.
8349
8720
  *
8350
- * @returns An array of scheduled tasks.
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
- * Trigger the scheduler to check and run due tasks.
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
- * @param date - The current reference date (default: now)
8360
- * @returns A promise that resolves when the scheduler run is complete.
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
- * Execute a specific task with locking logic.
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
- let acquiredLock = false;
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 lockKey = `task:${task.name}:${timestamp}`;
8794
+ const timeLockKey = `task:${task.name}:${timestamp}`;
8393
8795
  if (task.shouldRunOnOneServer) {
8394
- acquiredLock = await this.lockManager.acquire(lockKey, task.lockTtl);
8395
- if (!acquiredLock) {
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 (acquiredLock) {
8409
- await this.lockManager.release(lockKey);
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
- * Execute the task callback and handle hooks.
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
- try {
8423
- await task.callback();
8424
- const duration = Date.now() - startTime;
8425
- await this.hooks?.doAction("scheduler:task:success", { name: task.name, duration });
8426
- for (const cb of task.onSuccessCallbacks) {
8427
- try {
8428
- await cb({ name: task.name });
8429
- } catch {
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
- } catch (err) {
8433
- const duration = Date.now() - startTime;
8434
- this.logger?.error(`Task ${task.name} failed`, err);
8435
- await this.hooks?.doAction("scheduler:task:failure", {
8436
- name: task.name,
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
- for (const cb of task.onFailureCallbacks) {
8441
- try {
8442
- await cb(err);
8443
- } catch {
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
- * Install the Horizon Orbit into PlanetCore.
8454
- * Registers the SchedulerManager and configures the distributed lock driver.
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
- * @param core - The PlanetCore instance.
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", {});