@gravito/horizon 2.0.0 → 3.0.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
@@ -7822,128 +7822,59 @@ __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
- * Check if a cron expression matches the given date.
7828
- * Only supports standard 5-field cron expressions.
7829
- *
7830
- * Fields: minute hour day-of-month month day-of-week
7831
- * Values:
7832
- * - * (any)
7833
- * - numbers (0-59, 1-31, 0-11, 0-7)
7834
- * - ranges (1-5)
7835
- * - lists (1,2,3)
7836
- * - steps (*\/5, 1-10/2)
7827
+ * Check if a cron expression is due at the given date/time
7837
7828
  */
7838
- isDue(expression, timezone, date) {
7839
- const fields = expression.trim().split(/\s+/);
7840
- if (fields.length !== 5) {
7841
- throw new Error("SimpleCronParser only supports 5-field cron expressions");
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");
7884
- }
7885
- const matchMinute = matchField(minute, targetDate.getMinutes(), 0, 59);
7886
- const matchHour = matchField(hour, targetDate.getHours(), 0, 23);
7887
- const matchDayOfMonth = matchField(dayOfMonth, targetDate.getDate(), 1, 31);
7888
- const matchMonth = matchField(month, targetDate.getMonth() + 1, 1, 12);
7889
- const matchDayOfWeek = matchField(dayOfWeek, targetDate.getDay(), 0, 7);
7890
- return matchMinute && matchHour && matchDayOfMonth && matchMonth && matchDayOfWeek;
7891
- }
7892
- };
7893
- function matchField(pattern, value, min, max) {
7894
- if (pattern === "*") {
7895
- return true;
7896
- }
7897
- if (pattern.includes("/")) {
7898
- const [range, stepStr] = pattern.split("/");
7899
- if (range === void 0 || stepStr === void 0) {
7900
- return false;
7901
- }
7902
- const step = parseInt(stepStr, 10);
7903
- if (range === "*") {
7904
- return (value - min) % step === 0;
7905
- }
7906
- if (range.includes("-")) {
7907
- const [startStr, endStr] = range.split("-");
7908
- if (startStr === void 0 || endStr === void 0) {
7909
- return false;
7910
- }
7911
- const start = parseInt(startStr, 10);
7912
- const end = parseInt(endStr, 10);
7913
- if (value >= start && value <= end) {
7914
- return (value - start) % step === 0;
7915
- }
7916
- return false;
7829
+ static isDue(expression, timezone = "UTC", date = /* @__PURE__ */ new Date()) {
7830
+ const parts = expression.trim().split(/\s+/);
7831
+ if (parts.length !== 5) {
7832
+ throw new Error(`Invalid cron expression: ${expression}`);
7917
7833
  }
7834
+ const targetDate = this.getDateInTimezone(date, timezone);
7835
+ const minutes = targetDate.getMinutes();
7836
+ const hours = targetDate.getHours();
7837
+ const dayOfMonth = targetDate.getDate();
7838
+ const month = targetDate.getMonth() + 1;
7839
+ const dayOfWeek = targetDate.getDay();
7840
+ 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);
7918
7841
  }
7919
- if (pattern.includes(",")) {
7920
- const parts = pattern.split(",");
7921
- return parts.some((part) => matchField(part, value, min, max));
7842
+ static match(pattern, value, _min, _max, isDayOfWeek = false) {
7843
+ if (pattern === "*") return true;
7844
+ if (pattern.includes(",")) {
7845
+ return pattern.split(",").some((p) => this.match(p, value, _min, _max, isDayOfWeek));
7846
+ }
7847
+ const stepMatch = pattern.match(/^(\*|\d+(-\d+)?)\/(\d+)$/);
7848
+ if (stepMatch) {
7849
+ const range = stepMatch[1];
7850
+ const step = parseInt(stepMatch[3], 10);
7851
+ if (range === "*") return value % step === 0;
7852
+ const [rMin, rMax] = range.split("-").map((n) => parseInt(n, 10));
7853
+ return value >= rMin && value <= rMax && (value - rMin) % step === 0;
7854
+ }
7855
+ if (pattern.includes("-")) {
7856
+ const [rMin, rMax] = pattern.split("-").map((n) => parseInt(n, 10));
7857
+ return value >= rMin && value <= rMax;
7858
+ }
7859
+ const patternVal = parseInt(pattern, 10);
7860
+ if (isDayOfWeek && patternVal === 7 && value === 0) return true;
7861
+ return patternVal === value;
7922
7862
  }
7923
- if (pattern.includes("-")) {
7924
- const [startStr, endStr] = pattern.split("-");
7925
- if (startStr === void 0 || endStr === void 0) {
7926
- return false;
7863
+ static getDateInTimezone(date, timezone) {
7864
+ try {
7865
+ const tzDate = new Date(date.toLocaleString("en-US", { timeZone: timezone }));
7866
+ if (isNaN(tzDate.getTime())) throw new Error();
7867
+ return tzDate;
7868
+ } catch {
7869
+ throw new Error(`Invalid timezone: ${timezone}`);
7927
7870
  }
7928
- const start = parseInt(startStr, 10);
7929
- const end = parseInt(endStr, 10);
7930
- return value >= start && value <= end;
7931
7871
  }
7932
- const expected = parseInt(pattern, 10);
7933
- if (max === 7 && expected === 7 && value === 0) {
7934
- return true;
7935
- }
7936
- return value === expected;
7937
- }
7872
+ };
7938
7873
 
7939
7874
  // src/CronParser.ts
7940
7875
  var CronParser = class {
7941
7876
  /**
7942
7877
  * Get the next execution date based on a cron expression.
7943
- *
7944
- * @param expression - Cron expression
7945
- * @param timezone - Timezone identifier
7946
- * @param currentDate - Reference date
7947
7878
  */
7948
7879
  static async nextDate(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
7949
7880
  try {
@@ -7959,10 +7890,6 @@ var CronParser = class {
7959
7890
  }
7960
7891
  /**
7961
7892
  * Check if the cron expression is due to run at the current time (minute precision).
7962
- *
7963
- * @param expression - Cron expression
7964
- * @param timezone - Timezone identifier
7965
- * @param currentDate - Reference date
7966
7893
  */
7967
7894
  static async isDue(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
7968
7895
  try {
@@ -8524,6 +8451,7 @@ var SchedulerManager = class {
8524
8451
  var OrbitHorizon = class {
8525
8452
  /**
8526
8453
  * Install the Horizon Orbit into PlanetCore.
8454
+ * Registers the SchedulerManager and configures the distributed lock driver.
8527
8455
  *
8528
8456
  * @param core - The PlanetCore instance.
8529
8457
  */
@@ -8534,7 +8462,7 @@ var OrbitHorizon = class {
8534
8462
  const nodeRole = config.nodeRole;
8535
8463
  let lockManager;
8536
8464
  if (lockDriver === "cache") {
8537
- const cacheManager = core.services.get("cache");
8465
+ const cacheManager = core.container.make("cache");
8538
8466
  if (!cacheManager) {
8539
8467
  core.logger.warn(
8540
8468
  "[OrbitHorizon] Cache driver requested but cache service not found (ensure orbit-cache is loaded first). Falling back to Memory lock."
@@ -8547,11 +8475,10 @@ var OrbitHorizon = class {
8547
8475
  lockManager = new LockManager("memory");
8548
8476
  }
8549
8477
  const scheduler = new SchedulerManager(lockManager, core.logger, core.hooks, nodeRole);
8550
- core.services.set(exposeAs, scheduler);
8478
+ core.container.instance(exposeAs, scheduler);
8551
8479
  core.adapter.use("*", async (c, next) => {
8552
8480
  c.set("scheduler", scheduler);
8553
- await next();
8554
- return void 0;
8481
+ return await next();
8555
8482
  });
8556
8483
  core.logger.info(`[OrbitHorizon] Initialized (Driver: ${lockDriver})`);
8557
8484
  }
package/dist/index.d.cts CHANGED
@@ -1,26 +1,30 @@
1
1
  import { CacheManager } from '@gravito/stasis';
2
- import { GravitoOrbit, PlanetCore, ActionCallback, Logger, HookManager } from '@gravito/core';
2
+ import { ActionCallback, Logger, HookManager, GravitoOrbit, PlanetCore } from '@gravito/core';
3
3
 
4
+ /**
5
+ * Advanced cron expression parser with fallback support.
6
+ */
4
7
  declare class CronParser {
5
8
  /**
6
9
  * Get the next execution date based on a cron expression.
7
- *
8
- * @param expression - Cron expression
9
- * @param timezone - Timezone identifier
10
- * @param currentDate - Reference date
11
10
  */
12
11
  static nextDate(expression: string, timezone?: string, currentDate?: Date): Promise<Date>;
13
12
  /**
14
13
  * Check if the cron expression is due to run at the current time (minute precision).
15
- *
16
- * @param expression - Cron expression
17
- * @param timezone - Timezone identifier
18
- * @param currentDate - Reference date
19
14
  */
20
15
  static isDue(expression: string, timezone?: string, currentDate?: Date): Promise<boolean>;
21
16
  private static minuteMatches;
22
17
  }
23
18
 
19
+ /**
20
+ * Interface for distributed lock storage backends.
21
+ *
22
+ * Provides methods for acquiring, releasing, and managing
23
+ * distributed locks to prevent concurrent task execution.
24
+ *
25
+ * @public
26
+ * @since 3.0.0
27
+ */
24
28
  interface LockStore {
25
29
  /**
26
30
  * Attempt to acquire a lock
@@ -47,6 +51,21 @@ interface LockStore {
47
51
  exists(key: string): Promise<boolean>;
48
52
  }
49
53
 
54
+ /**
55
+ * Cache-based lock store using Gravito Stasis.
56
+ *
57
+ * Stores locks in the cache system for distributed locking
58
+ * across multiple application instances.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const store = new CacheLockStore(cacheManager, 'locks:')
63
+ * const acquired = await store.acquire('task-123', 300)
64
+ * ```
65
+ *
66
+ * @since 3.0.0
67
+ * @public
68
+ */
50
69
  declare class CacheLockStore implements LockStore {
51
70
  private cache;
52
71
  private prefix;
@@ -58,6 +77,34 @@ declare class CacheLockStore implements LockStore {
58
77
  exists(key: string): Promise<boolean>;
59
78
  }
60
79
 
80
+ /**
81
+ * Manager for distributed locks to prevent concurrent task execution.
82
+ *
83
+ * Supports multiple storage backends (memory, cache, custom) and provides
84
+ * a unified interface for acquiring and releasing locks.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * // Using memory store
89
+ * const locks = new LockManager('memory')
90
+ *
91
+ * // Using cache store
92
+ * const locks = new LockManager('cache', { cache: cacheManager })
93
+ *
94
+ * // Acquire a lock
95
+ * const acquired = await locks.acquire('task:send-emails', 300)
96
+ * if (acquired) {
97
+ * try {
98
+ * // Execute task
99
+ * } finally {
100
+ * await locks.release('task:send-emails')
101
+ * }
102
+ * }
103
+ * ```
104
+ *
105
+ * @since 3.0.0
106
+ * @public
107
+ */
61
108
  declare class LockManager {
62
109
  private store;
63
110
  constructor(driver: 'memory' | 'cache' | LockStore, context?: {
@@ -69,6 +116,25 @@ declare class LockManager {
69
116
  exists(key: string): Promise<boolean>;
70
117
  }
71
118
 
119
+ /**
120
+ * In-memory lock store for single-instance deployments.
121
+ *
122
+ * Stores locks in memory with automatic expiration.
123
+ * Not suitable for multi-instance deployments.
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const store = new MemoryLockStore()
128
+ * const acquired = await store.acquire('task-123', 300)
129
+ * if (acquired) {
130
+ * // Execute task
131
+ * await store.release('task-123')
132
+ * }
133
+ * ```
134
+ *
135
+ * @since 3.0.0
136
+ * @public
137
+ */
72
138
  declare class MemoryLockStore implements LockStore {
73
139
  private locks;
74
140
  acquire(key: string, ttlSeconds: number): Promise<boolean>;
@@ -77,47 +143,49 @@ declare class MemoryLockStore implements LockStore {
77
143
  exists(key: string): Promise<boolean>;
78
144
  }
79
145
 
80
- declare class OrbitHorizon implements GravitoOrbit {
81
- /**
82
- * Install the Horizon Orbit into PlanetCore.
83
- *
84
- * @param core - The PlanetCore instance.
85
- */
86
- install(core: PlanetCore): void;
87
- }
88
-
89
- interface ProcessResult {
90
- exitCode: number;
91
- stdout: string;
92
- stderr: string;
93
- success: boolean;
94
- }
95
- declare function runProcess(command: string): Promise<ProcessResult>;
96
- declare class Process {
97
- static run(command: string): Promise<ProcessResult>;
98
- }
99
-
146
+ /**
147
+ * Represents a configuration for a scheduled task (Cron job).
148
+ *
149
+ * @public
150
+ * @since 3.0.0
151
+ */
100
152
  interface ScheduledTask {
153
+ /** Unique name for the task */
101
154
  name: string;
155
+ /** Standard cron expression mapping the frequency */
102
156
  expression: string;
157
+ /** Timezone for evaluating the cron expression (default: UTC) */
103
158
  timezone: string;
159
+ /** The function or task to execute */
104
160
  callback: () => void | Promise<void>;
161
+ /** If true, uses a distributed lock to ensure only one server runs the task */
105
162
  shouldRunOnOneServer: boolean;
163
+ /** Time-to-live for the distributed lock in seconds (default: 300) */
106
164
  lockTtl: number;
165
+ /** If true, the task runs in the background and doesn't block the scheduler loop */
107
166
  background: boolean;
167
+ /** Optional node role required to run this task (e.g., 'worker') */
108
168
  nodeRole?: string;
169
+ /** Command string if the task was registered via a shell command */
109
170
  command?: string;
171
+ /** Callbacks executed after successful task completion */
110
172
  onSuccessCallbacks: ActionCallback[];
173
+ /** Callbacks executed after task failure */
111
174
  onFailureCallbacks: ActionCallback[];
112
175
  }
113
176
  /**
114
177
  * Fluent API for defining task schedules.
115
178
  *
116
179
  * @example
180
+ * ```typescript
117
181
  * scheduler.task('backup')
118
182
  * .daily()
119
183
  * .at('02:00')
120
- * .onOneServer()
184
+ * .onOneServer();
185
+ * ```
186
+ *
187
+ * @public
188
+ * @since 3.0.0
121
189
  */
122
190
  declare class TaskSchedule {
123
191
  private task;
@@ -363,4 +431,85 @@ declare class SchedulerManager {
363
431
  private executeTask;
364
432
  }
365
433
 
434
+ /**
435
+ * OrbitHorizon is the official Task Scheduler for Gravito.
436
+ * It provides a fluent API for defining Cron jobs that can run on a schedule,
437
+ * with support for distributed locking (run on one server) and node roles.
438
+ *
439
+ * @example
440
+ * ```typescript
441
+ * const horizon = new OrbitHorizon();
442
+ * core.addOrbit(horizon);
443
+ *
444
+ * // Defining a task
445
+ * const scheduler = core.container.make('scheduler');
446
+ * scheduler.call('cleanup', async () => { ... }).daily().at('03:00').onOneServer();
447
+ * ```
448
+ * @public
449
+ */
450
+ declare class OrbitHorizon implements GravitoOrbit {
451
+ /**
452
+ * Install the Horizon Orbit into PlanetCore.
453
+ * Registers the SchedulerManager and configures the distributed lock driver.
454
+ *
455
+ * @param core - The PlanetCore instance.
456
+ */
457
+ install(core: PlanetCore): void;
458
+ }
459
+ declare module '@gravito/core' {
460
+ interface GravitoVariables {
461
+ /** Scheduler manager for task scheduling */
462
+ scheduler?: SchedulerManager;
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Result of a process execution.
468
+ *
469
+ * @public
470
+ * @since 3.0.0
471
+ */
472
+ interface ProcessResult {
473
+ /** Exit code of the process (0 indicates success). */
474
+ exitCode: number;
475
+ /** Standard output from the process. */
476
+ stdout: string;
477
+ /** Standard error output from the process. */
478
+ stderr: string;
479
+ /** Whether the process completed successfully (exitCode === 0). */
480
+ success: boolean;
481
+ }
482
+ /**
483
+ * Execute a shell command and capture its output.
484
+ *
485
+ * @param command - The command string to execute.
486
+ * @returns A promise that resolves to the process result.
487
+ *
488
+ * @public
489
+ * @since 3.0.0
490
+ */
491
+ declare function runProcess(command: string): Promise<ProcessResult>;
492
+ /**
493
+ * Process execution utility for running shell commands.
494
+ *
495
+ * Provides a simple interface for executing shell commands
496
+ * and capturing their output.
497
+ *
498
+ * @example
499
+ * ```typescript
500
+ * const result = await Process.run('ls -la')
501
+ * if (result.success) {
502
+ * console.log(result.stdout)
503
+ * } else {
504
+ * console.error(result.stderr)
505
+ * }
506
+ * ```
507
+ *
508
+ * @since 3.0.0
509
+ * @public
510
+ */
511
+ declare class Process {
512
+ static run(command: string): Promise<ProcessResult>;
513
+ }
514
+
366
515
  export { CacheLockStore, CronParser, LockManager, type LockStore, MemoryLockStore, OrbitHorizon, Process, type ProcessResult, type ScheduledTask, SchedulerManager, TaskSchedule, runProcess };
package/dist/index.d.ts CHANGED
@@ -1,26 +1,30 @@
1
1
  import { CacheManager } from '@gravito/stasis';
2
- import { GravitoOrbit, PlanetCore, ActionCallback, Logger, HookManager } from '@gravito/core';
2
+ import { ActionCallback, Logger, HookManager, GravitoOrbit, PlanetCore } from '@gravito/core';
3
3
 
4
+ /**
5
+ * Advanced cron expression parser with fallback support.
6
+ */
4
7
  declare class CronParser {
5
8
  /**
6
9
  * Get the next execution date based on a cron expression.
7
- *
8
- * @param expression - Cron expression
9
- * @param timezone - Timezone identifier
10
- * @param currentDate - Reference date
11
10
  */
12
11
  static nextDate(expression: string, timezone?: string, currentDate?: Date): Promise<Date>;
13
12
  /**
14
13
  * Check if the cron expression is due to run at the current time (minute precision).
15
- *
16
- * @param expression - Cron expression
17
- * @param timezone - Timezone identifier
18
- * @param currentDate - Reference date
19
14
  */
20
15
  static isDue(expression: string, timezone?: string, currentDate?: Date): Promise<boolean>;
21
16
  private static minuteMatches;
22
17
  }
23
18
 
19
+ /**
20
+ * Interface for distributed lock storage backends.
21
+ *
22
+ * Provides methods for acquiring, releasing, and managing
23
+ * distributed locks to prevent concurrent task execution.
24
+ *
25
+ * @public
26
+ * @since 3.0.0
27
+ */
24
28
  interface LockStore {
25
29
  /**
26
30
  * Attempt to acquire a lock
@@ -47,6 +51,21 @@ interface LockStore {
47
51
  exists(key: string): Promise<boolean>;
48
52
  }
49
53
 
54
+ /**
55
+ * Cache-based lock store using Gravito Stasis.
56
+ *
57
+ * Stores locks in the cache system for distributed locking
58
+ * across multiple application instances.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const store = new CacheLockStore(cacheManager, 'locks:')
63
+ * const acquired = await store.acquire('task-123', 300)
64
+ * ```
65
+ *
66
+ * @since 3.0.0
67
+ * @public
68
+ */
50
69
  declare class CacheLockStore implements LockStore {
51
70
  private cache;
52
71
  private prefix;
@@ -58,6 +77,34 @@ declare class CacheLockStore implements LockStore {
58
77
  exists(key: string): Promise<boolean>;
59
78
  }
60
79
 
80
+ /**
81
+ * Manager for distributed locks to prevent concurrent task execution.
82
+ *
83
+ * Supports multiple storage backends (memory, cache, custom) and provides
84
+ * a unified interface for acquiring and releasing locks.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * // Using memory store
89
+ * const locks = new LockManager('memory')
90
+ *
91
+ * // Using cache store
92
+ * const locks = new LockManager('cache', { cache: cacheManager })
93
+ *
94
+ * // Acquire a lock
95
+ * const acquired = await locks.acquire('task:send-emails', 300)
96
+ * if (acquired) {
97
+ * try {
98
+ * // Execute task
99
+ * } finally {
100
+ * await locks.release('task:send-emails')
101
+ * }
102
+ * }
103
+ * ```
104
+ *
105
+ * @since 3.0.0
106
+ * @public
107
+ */
61
108
  declare class LockManager {
62
109
  private store;
63
110
  constructor(driver: 'memory' | 'cache' | LockStore, context?: {
@@ -69,6 +116,25 @@ declare class LockManager {
69
116
  exists(key: string): Promise<boolean>;
70
117
  }
71
118
 
119
+ /**
120
+ * In-memory lock store for single-instance deployments.
121
+ *
122
+ * Stores locks in memory with automatic expiration.
123
+ * Not suitable for multi-instance deployments.
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const store = new MemoryLockStore()
128
+ * const acquired = await store.acquire('task-123', 300)
129
+ * if (acquired) {
130
+ * // Execute task
131
+ * await store.release('task-123')
132
+ * }
133
+ * ```
134
+ *
135
+ * @since 3.0.0
136
+ * @public
137
+ */
72
138
  declare class MemoryLockStore implements LockStore {
73
139
  private locks;
74
140
  acquire(key: string, ttlSeconds: number): Promise<boolean>;
@@ -77,47 +143,49 @@ declare class MemoryLockStore implements LockStore {
77
143
  exists(key: string): Promise<boolean>;
78
144
  }
79
145
 
80
- declare class OrbitHorizon implements GravitoOrbit {
81
- /**
82
- * Install the Horizon Orbit into PlanetCore.
83
- *
84
- * @param core - The PlanetCore instance.
85
- */
86
- install(core: PlanetCore): void;
87
- }
88
-
89
- interface ProcessResult {
90
- exitCode: number;
91
- stdout: string;
92
- stderr: string;
93
- success: boolean;
94
- }
95
- declare function runProcess(command: string): Promise<ProcessResult>;
96
- declare class Process {
97
- static run(command: string): Promise<ProcessResult>;
98
- }
99
-
146
+ /**
147
+ * Represents a configuration for a scheduled task (Cron job).
148
+ *
149
+ * @public
150
+ * @since 3.0.0
151
+ */
100
152
  interface ScheduledTask {
153
+ /** Unique name for the task */
101
154
  name: string;
155
+ /** Standard cron expression mapping the frequency */
102
156
  expression: string;
157
+ /** Timezone for evaluating the cron expression (default: UTC) */
103
158
  timezone: string;
159
+ /** The function or task to execute */
104
160
  callback: () => void | Promise<void>;
161
+ /** If true, uses a distributed lock to ensure only one server runs the task */
105
162
  shouldRunOnOneServer: boolean;
163
+ /** Time-to-live for the distributed lock in seconds (default: 300) */
106
164
  lockTtl: number;
165
+ /** If true, the task runs in the background and doesn't block the scheduler loop */
107
166
  background: boolean;
167
+ /** Optional node role required to run this task (e.g., 'worker') */
108
168
  nodeRole?: string;
169
+ /** Command string if the task was registered via a shell command */
109
170
  command?: string;
171
+ /** Callbacks executed after successful task completion */
110
172
  onSuccessCallbacks: ActionCallback[];
173
+ /** Callbacks executed after task failure */
111
174
  onFailureCallbacks: ActionCallback[];
112
175
  }
113
176
  /**
114
177
  * Fluent API for defining task schedules.
115
178
  *
116
179
  * @example
180
+ * ```typescript
117
181
  * scheduler.task('backup')
118
182
  * .daily()
119
183
  * .at('02:00')
120
- * .onOneServer()
184
+ * .onOneServer();
185
+ * ```
186
+ *
187
+ * @public
188
+ * @since 3.0.0
121
189
  */
122
190
  declare class TaskSchedule {
123
191
  private task;
@@ -363,4 +431,85 @@ declare class SchedulerManager {
363
431
  private executeTask;
364
432
  }
365
433
 
434
+ /**
435
+ * OrbitHorizon is the official Task Scheduler for Gravito.
436
+ * It provides a fluent API for defining Cron jobs that can run on a schedule,
437
+ * with support for distributed locking (run on one server) and node roles.
438
+ *
439
+ * @example
440
+ * ```typescript
441
+ * const horizon = new OrbitHorizon();
442
+ * core.addOrbit(horizon);
443
+ *
444
+ * // Defining a task
445
+ * const scheduler = core.container.make('scheduler');
446
+ * scheduler.call('cleanup', async () => { ... }).daily().at('03:00').onOneServer();
447
+ * ```
448
+ * @public
449
+ */
450
+ declare class OrbitHorizon implements GravitoOrbit {
451
+ /**
452
+ * Install the Horizon Orbit into PlanetCore.
453
+ * Registers the SchedulerManager and configures the distributed lock driver.
454
+ *
455
+ * @param core - The PlanetCore instance.
456
+ */
457
+ install(core: PlanetCore): void;
458
+ }
459
+ declare module '@gravito/core' {
460
+ interface GravitoVariables {
461
+ /** Scheduler manager for task scheduling */
462
+ scheduler?: SchedulerManager;
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Result of a process execution.
468
+ *
469
+ * @public
470
+ * @since 3.0.0
471
+ */
472
+ interface ProcessResult {
473
+ /** Exit code of the process (0 indicates success). */
474
+ exitCode: number;
475
+ /** Standard output from the process. */
476
+ stdout: string;
477
+ /** Standard error output from the process. */
478
+ stderr: string;
479
+ /** Whether the process completed successfully (exitCode === 0). */
480
+ success: boolean;
481
+ }
482
+ /**
483
+ * Execute a shell command and capture its output.
484
+ *
485
+ * @param command - The command string to execute.
486
+ * @returns A promise that resolves to the process result.
487
+ *
488
+ * @public
489
+ * @since 3.0.0
490
+ */
491
+ declare function runProcess(command: string): Promise<ProcessResult>;
492
+ /**
493
+ * Process execution utility for running shell commands.
494
+ *
495
+ * Provides a simple interface for executing shell commands
496
+ * and capturing their output.
497
+ *
498
+ * @example
499
+ * ```typescript
500
+ * const result = await Process.run('ls -la')
501
+ * if (result.success) {
502
+ * console.log(result.stdout)
503
+ * } else {
504
+ * console.error(result.stderr)
505
+ * }
506
+ * ```
507
+ *
508
+ * @since 3.0.0
509
+ * @public
510
+ */
511
+ declare class Process {
512
+ static run(command: string): Promise<ProcessResult>;
513
+ }
514
+
366
515
  export { CacheLockStore, CronParser, LockManager, type LockStore, MemoryLockStore, OrbitHorizon, Process, type ProcessResult, type ScheduledTask, SchedulerManager, TaskSchedule, runProcess };
package/dist/index.js CHANGED
@@ -1,128 +1,59 @@
1
1
  import "./chunk-MCKGQKYU.js";
2
2
 
3
3
  // src/SimpleCronParser.ts
4
- var SimpleCronParser = {
5
- /**
6
- * Check if a cron expression matches the given date.
7
- * Only supports standard 5-field cron expressions.
8
- *
9
- * Fields: minute hour day-of-month month day-of-week
10
- * Values:
11
- * - * (any)
12
- * - numbers (0-59, 1-31, 0-11, 0-7)
13
- * - ranges (1-5)
14
- * - lists (1,2,3)
15
- * - steps (*\/5, 1-10/2)
16
- */
17
- isDue(expression, timezone, date) {
18
- const fields = expression.trim().split(/\s+/);
19
- if (fields.length !== 5) {
20
- throw new Error("SimpleCronParser only supports 5-field cron expressions");
4
+ var SimpleCronParser = class {
5
+ /**
6
+ * Check if a cron expression is due at the given date/time
7
+ */
8
+ static isDue(expression, timezone = "UTC", date = /* @__PURE__ */ new Date()) {
9
+ const parts = expression.trim().split(/\s+/);
10
+ if (parts.length !== 5) {
11
+ throw new Error(`Invalid cron expression: ${expression}`);
21
12
  }
22
- let targetDate = date;
23
- if (timezone && timezone !== "UTC") {
24
- try {
25
- const parts = new Intl.DateTimeFormat("en-US", {
26
- timeZone: timezone,
27
- year: "numeric",
28
- month: "numeric",
29
- day: "numeric",
30
- hour: "numeric",
31
- minute: "numeric",
32
- second: "numeric",
33
- hour12: false
34
- }).formatToParts(date);
35
- const partMap = {};
36
- parts.forEach((p) => {
37
- partMap[p.type] = p.value;
38
- });
39
- targetDate = new Date(
40
- parseInt(partMap.year ?? "0", 10),
41
- parseInt(partMap.month ?? "1", 10) - 1,
42
- parseInt(partMap.day ?? "1", 10),
43
- parseInt(partMap.hour ?? "0", 10) === 24 ? 0 : parseInt(partMap.hour ?? "0", 10),
44
- parseInt(partMap.minute ?? "0", 10),
45
- 0
46
- );
47
- } catch (_e) {
48
- throw new Error(`Invalid timezone: ${timezone}`);
49
- }
50
- } else if (timezone === "UTC") {
51
- targetDate = new Date(
52
- date.getUTCFullYear(),
53
- date.getUTCMonth(),
54
- date.getUTCDate(),
55
- date.getUTCHours(),
56
- date.getUTCMinutes(),
57
- 0
58
- );
13
+ const targetDate = this.getDateInTimezone(date, timezone);
14
+ const minutes = targetDate.getMinutes();
15
+ const hours = targetDate.getHours();
16
+ const dayOfMonth = targetDate.getDate();
17
+ const month = targetDate.getMonth() + 1;
18
+ const dayOfWeek = targetDate.getDay();
19
+ 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);
20
+ }
21
+ static match(pattern, value, _min, _max, isDayOfWeek = false) {
22
+ if (pattern === "*") return true;
23
+ if (pattern.includes(",")) {
24
+ return pattern.split(",").some((p) => this.match(p, value, _min, _max, isDayOfWeek));
59
25
  }
60
- const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
61
- if (minute === void 0 || hour === void 0 || dayOfMonth === void 0 || month === void 0 || dayOfWeek === void 0) {
62
- throw new Error("Invalid cron expression");
26
+ const stepMatch = pattern.match(/^(\*|\d+(-\d+)?)\/(\d+)$/);
27
+ if (stepMatch) {
28
+ const range = stepMatch[1];
29
+ const step = parseInt(stepMatch[3], 10);
30
+ if (range === "*") return value % step === 0;
31
+ const [rMin, rMax] = range.split("-").map((n) => parseInt(n, 10));
32
+ return value >= rMin && value <= rMax && (value - rMin) % step === 0;
63
33
  }
64
- const matchMinute = matchField(minute, targetDate.getMinutes(), 0, 59);
65
- const matchHour = matchField(hour, targetDate.getHours(), 0, 23);
66
- const matchDayOfMonth = matchField(dayOfMonth, targetDate.getDate(), 1, 31);
67
- const matchMonth = matchField(month, targetDate.getMonth() + 1, 1, 12);
68
- const matchDayOfWeek = matchField(dayOfWeek, targetDate.getDay(), 0, 7);
69
- return matchMinute && matchHour && matchDayOfMonth && matchMonth && matchDayOfWeek;
70
- }
71
- };
72
- function matchField(pattern, value, min, max) {
73
- if (pattern === "*") {
74
- return true;
75
- }
76
- if (pattern.includes("/")) {
77
- const [range, stepStr] = pattern.split("/");
78
- if (range === void 0 || stepStr === void 0) {
79
- return false;
80
- }
81
- const step = parseInt(stepStr, 10);
82
- if (range === "*") {
83
- return (value - min) % step === 0;
84
- }
85
- if (range.includes("-")) {
86
- const [startStr, endStr] = range.split("-");
87
- if (startStr === void 0 || endStr === void 0) {
88
- return false;
89
- }
90
- const start = parseInt(startStr, 10);
91
- const end = parseInt(endStr, 10);
92
- if (value >= start && value <= end) {
93
- return (value - start) % step === 0;
94
- }
95
- return false;
34
+ if (pattern.includes("-")) {
35
+ const [rMin, rMax] = pattern.split("-").map((n) => parseInt(n, 10));
36
+ return value >= rMin && value <= rMax;
96
37
  }
38
+ const patternVal = parseInt(pattern, 10);
39
+ if (isDayOfWeek && patternVal === 7 && value === 0) return true;
40
+ return patternVal === value;
97
41
  }
98
- if (pattern.includes(",")) {
99
- const parts = pattern.split(",");
100
- return parts.some((part) => matchField(part, value, min, max));
101
- }
102
- if (pattern.includes("-")) {
103
- const [startStr, endStr] = pattern.split("-");
104
- if (startStr === void 0 || endStr === void 0) {
105
- return false;
42
+ static getDateInTimezone(date, timezone) {
43
+ try {
44
+ const tzDate = new Date(date.toLocaleString("en-US", { timeZone: timezone }));
45
+ if (isNaN(tzDate.getTime())) throw new Error();
46
+ return tzDate;
47
+ } catch {
48
+ throw new Error(`Invalid timezone: ${timezone}`);
106
49
  }
107
- const start = parseInt(startStr, 10);
108
- const end = parseInt(endStr, 10);
109
- return value >= start && value <= end;
110
50
  }
111
- const expected = parseInt(pattern, 10);
112
- if (max === 7 && expected === 7 && value === 0) {
113
- return true;
114
- }
115
- return value === expected;
116
- }
51
+ };
117
52
 
118
53
  // src/CronParser.ts
119
54
  var CronParser = class {
120
55
  /**
121
56
  * Get the next execution date based on a cron expression.
122
- *
123
- * @param expression - Cron expression
124
- * @param timezone - Timezone identifier
125
- * @param currentDate - Reference date
126
57
  */
127
58
  static async nextDate(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
128
59
  try {
@@ -138,10 +69,6 @@ var CronParser = class {
138
69
  }
139
70
  /**
140
71
  * Check if the cron expression is due to run at the current time (minute precision).
141
- *
142
- * @param expression - Cron expression
143
- * @param timezone - Timezone identifier
144
- * @param currentDate - Reference date
145
72
  */
146
73
  static async isDue(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
147
74
  try {
@@ -703,6 +630,7 @@ var SchedulerManager = class {
703
630
  var OrbitHorizon = class {
704
631
  /**
705
632
  * Install the Horizon Orbit into PlanetCore.
633
+ * Registers the SchedulerManager and configures the distributed lock driver.
706
634
  *
707
635
  * @param core - The PlanetCore instance.
708
636
  */
@@ -713,7 +641,7 @@ var OrbitHorizon = class {
713
641
  const nodeRole = config.nodeRole;
714
642
  let lockManager;
715
643
  if (lockDriver === "cache") {
716
- const cacheManager = core.services.get("cache");
644
+ const cacheManager = core.container.make("cache");
717
645
  if (!cacheManager) {
718
646
  core.logger.warn(
719
647
  "[OrbitHorizon] Cache driver requested but cache service not found (ensure orbit-cache is loaded first). Falling back to Memory lock."
@@ -726,11 +654,10 @@ var OrbitHorizon = class {
726
654
  lockManager = new LockManager("memory");
727
655
  }
728
656
  const scheduler = new SchedulerManager(lockManager, core.logger, core.hooks, nodeRole);
729
- core.services.set(exposeAs, scheduler);
657
+ core.container.instance(exposeAs, scheduler);
730
658
  core.adapter.use("*", async (c, next) => {
731
659
  c.set("scheduler", scheduler);
732
- await next();
733
- return void 0;
660
+ return await next();
734
661
  });
735
662
  core.logger.info(`[OrbitHorizon] Initialized (Driver: ${lockDriver})`);
736
663
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravito/horizon",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Distributed task scheduler for Gravito framework",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -23,8 +23,8 @@
23
23
  "test": "bun test",
24
24
  "lint": "biome lint ./src",
25
25
  "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
26
- "test:coverage": "bun test --coverage --coverage-threshold=80",
27
- "test:ci": "bun test --coverage --coverage-threshold=80"
26
+ "test:coverage": "bun test --coverage --coverage-threshold=60",
27
+ "test:ci": "bun test --coverage --coverage-threshold=60"
28
28
  },
29
29
  "keywords": [
30
30
  "gravito",