@gravito/horizon 3.2.1 → 3.2.2

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 CHANGED
@@ -4,15 +4,32 @@ Enterprise-grade distributed task scheduler for the Gravito framework.
4
4
 
5
5
  `@gravito/horizon` provides a robust, fluent, and highly configurable system for managing scheduled tasks (Cron jobs) in a distributed environment. It supports multiple locking mechanisms to prevent duplicate execution, node role filtering, retries, and comprehensive monitoring hooks.
6
6
 
7
- ## Features
8
-
9
- - **Fluent API**: Human-readable syntax for defining task schedules (e.g., `.daily().at('14:00')`).
10
- - **Distributed Locking**: Prevents duplicate task execution across multiple servers (supports Memory, Cache, and Redis).
11
- - **Node Role Awareness**: Restrict tasks to specific nodes (e.g., only run on `worker` nodes) or broadcast maintenance tasks to all matching nodes.
12
- - **Reliability Features**: Built-in support for task timeouts and automatic retries with configurable delays.
13
- - **Shell Command Support**: Schedule raw shell commands alongside TypeScript callbacks.
14
- - **Lazy Cron Parsing**: Lightweight `SimpleCronParser` for standard expressions, with `cron-parser` only loaded when complex logic is required.
15
- - **Comprehensive Hooks**: Lifecycle events for monitoring task success, failure, retries, and scheduler activity.
7
+ ## Features
8
+
9
+ - 🪐 **Galaxy-Ready Distributed Scheduler**: Native integration with PlanetCore for universal task management across all Satellites.
10
+ - 🕒 **Fluent API**: Human-readable syntax for defining task schedules (e.g., `.daily().at('14:00')`).
11
+ - 🔐 **Distributed Locking**: Prevents duplicate task execution across multiple servers (supports Redis/Plasma and SQL).
12
+ - 🧬 **Node-Role Awareness**: Intelligently restrict tasks to specific nodes (e.g., only run on `worker` nodes) or broadcast maintenance tasks.
13
+ - 🛡️ **Reliability Features**: Built-in support for task timeouts and automatic retries with configurable exponential backoff.
14
+ - 🐚 **Shell Command Support**: Schedule raw shell commands alongside TypeScript callbacks (powered by `@gravito/nova`).
15
+ - 📊 **Monitoring Hooks**: Comprehensive lifecycle events for tracking task success, failure, and execution duration.
16
+
17
+ ## 🌌 Role in Galaxy Architecture
18
+
19
+ In the **Gravito Galaxy Architecture**, Horizon acts as the **Clockwork of Jobs (Temporal Coordinator)**.
20
+
21
+ - **System Heartbeat**: Triggers recurring maintenance, cleanup, and synchronization tasks that keep the Galaxy running smoothly over time.
22
+ - **Node Coordination**: Ensures that even if you have 100 instances of a Satellite, a "Daily Report" task only runs exactly once per day across the entire cluster.
23
+ - **Background Orchestration**: Works with `@gravito/stream` to initiate long-running background processes at specific intervals.
24
+
25
+ ```mermaid
26
+ graph TD
27
+ H[Horizon: Scheduler] -->|Time Trigger| L{Distributed Lock}
28
+ L -- "Win Lock" --> T[Task: Daily Cleanup]
29
+ L -- "Lose Lock" --> S[Skip Execution]
30
+ T -->|Call| Sat[Satellite: Admin]
31
+ T -->|Metrics| Mon[Monitor Orbit]
32
+ ```
16
33
 
17
34
  ## Installation
18
35
 
@@ -65,13 +82,22 @@ scheduler.task('daily-cleanup', async () => {
65
82
  .dailyAt('02:00')
66
83
  .onOneServer() // Distributed lock
67
84
 
68
- // Shell command execution
85
+ // Shell command execution (type-safe via @gravito/nova)
69
86
  scheduler.exec('sync-storage', 'aws s3 sync ./local s3://bucket')
70
87
  .everyFiveMinutes()
71
88
  .onNode('worker')
72
89
  .retry(3, 5000) // Retry 3 times with 5s delay
90
+ .timeout(300000) // 5 minute timeout
73
91
  ```
74
92
 
93
+ ## 📚 Documentation
94
+
95
+ Detailed guides and references for the Galaxy Architecture:
96
+
97
+ - [🏗️ **Architecture Overview**](./README.md) — Distributed task scheduler core.
98
+ - [🕒 **Distributed Scheduling**](./doc/DISTRIBUTED_SCHEDULING.md) — **NEW**: Multi-server coordination and locking.
99
+ - [🐚 **Shell Execution**](#shell-execution-with-nova) — Type-safe shell commands via Nova.
100
+
75
101
  ## Scheduling API
76
102
 
77
103
  ### Frequency Methods
@@ -135,6 +161,39 @@ Poll every minute in a long-running process (ideal for Docker):
135
161
  bun run gravito schedule:work
136
162
  ```
137
163
 
164
+ ## Shell Execution with Nova
165
+
166
+ Horizon uses [@gravito/nova](../nova) for shell command execution, which provides:
167
+
168
+ - **Type-Safe Execution**: Template literal-based API prevents shell injection
169
+ - **Automatic Escaping**: All command arguments are automatically escaped
170
+ - **Consistent API**: Same Shell API used across Gravito framework
171
+ - **Error Handling**: Comprehensive error capture with stdout/stderr
172
+
173
+ Example with advanced shell operations:
174
+
175
+ ```typescript
176
+ import { Shell } from '@gravito/nova'
177
+
178
+ scheduler.task('backup-database', async () => {
179
+ // Use nova Shell API for custom commands
180
+ const result = await Shell.run`mysqldump -u ${dbUser} -p${dbPassword} ${dbName}`
181
+ .nothrow()
182
+ .run()
183
+
184
+ if (result.success) {
185
+ // Upload backup to S3
186
+ await Shell.run`aws s3 cp - s3://backups/${Date.now()}.sql`
187
+ .nothrow()
188
+ .run()
189
+ }
190
+ })
191
+ .dailyAt('03:00')
192
+ .onOneServer()
193
+ ```
194
+
195
+ ---
196
+
138
197
  ## Monitoring Hooks
139
198
 
140
199
  Subscribe to hooks via `core.hooks` to build monitoring dashboards or alerts:
package/dist/index.cjs CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -8217,24 +8216,19 @@ var LockManager = class {
8217
8216
  };
8218
8217
 
8219
8218
  // src/process/Process.ts
8220
- var import_core = require("@gravito/core");
8219
+ var import_nova = require("@gravito/nova");
8221
8220
  async function runProcess(command) {
8222
- const runtime = (0, import_core.getRuntimeAdapter)();
8223
- const proc = runtime.spawn(["sh", "-c", command], {
8224
- stdout: "pipe",
8225
- stderr: "pipe"
8226
- });
8227
- const [stdout, stderr] = await Promise.all([
8228
- new Response(proc.stdout ?? null).text(),
8229
- new Response(proc.stderr ?? null).text()
8230
- ]);
8231
- const exitCode = await proc.exited;
8232
- return {
8233
- exitCode,
8234
- stdout,
8235
- stderr,
8236
- success: exitCode === 0
8237
- };
8221
+ try {
8222
+ const result = await import_nova.Shell.run`bash -c ${command}`.nothrow().run();
8223
+ return result;
8224
+ } catch (error) {
8225
+ return {
8226
+ exitCode: 1,
8227
+ stdout: "",
8228
+ stderr: error instanceof Error ? error.message : String(error),
8229
+ success: false
8230
+ };
8231
+ }
8238
8232
  }
8239
8233
  var Process = class {
8240
8234
  /**
@@ -8805,14 +8799,15 @@ var SchedulerManager = class {
8805
8799
  if (task.background) {
8806
8800
  this.executeTask(task).catch((err) => {
8807
8801
  this.logger?.error(`Background task ${task.name} failed`, err);
8808
- }).finally(async () => {
8809
- if (task.preventOverlapping) {
8802
+ return { success: false, timedOut: false };
8803
+ }).then(async (result) => {
8804
+ if (task.preventOverlapping && !result.timedOut) {
8810
8805
  await this.lockManager.release(runningLockKey);
8811
8806
  }
8812
8807
  });
8813
8808
  } else {
8814
- await this.executeTask(task);
8815
- if (task.preventOverlapping) {
8809
+ const result = await this.executeTask(task);
8810
+ if (task.preventOverlapping && !result.timedOut) {
8816
8811
  await this.lockManager.release(runningLockKey);
8817
8812
  }
8818
8813
  }
@@ -8877,7 +8872,7 @@ var SchedulerManager = class {
8877
8872
  } catch {
8878
8873
  }
8879
8874
  }
8880
- return;
8875
+ return { success: true, timedOut: false };
8881
8876
  } catch (err) {
8882
8877
  lastError = err;
8883
8878
  this.logger?.error(`Task ${task.name} failed (attempt ${attempt + 1})`, err);
@@ -8900,6 +8895,10 @@ var SchedulerManager = class {
8900
8895
  } catch {
8901
8896
  }
8902
8897
  }
8898
+ return {
8899
+ success: false,
8900
+ timedOut: lastError?.message.includes("timed out after") ?? false
8901
+ };
8903
8902
  }
8904
8903
  };
8905
8904
 
package/dist/index.d.cts CHANGED
@@ -674,7 +674,7 @@ declare class SchedulerManager {
674
674
  * @param hooks - Optional manager for lifecycle event hooks.
675
675
  * @param currentNodeRole - Role identifier for the local node (used for filtering).
676
676
  */
677
- constructor(lockManager: LockManager, logger?: Logger | undefined, hooks?: HookManager | undefined, currentNodeRole?: string | undefined);
677
+ constructor(lockManager: LockManager, logger?: Logger, hooks?: HookManager, currentNodeRole?: string);
678
678
  /**
679
679
  * Registers a new callback-based scheduled task.
680
680
  *
@@ -844,9 +844,9 @@ interface ProcessResult {
844
844
  /**
845
845
  * Spawns a shell command and asynchronously captures its full output.
846
846
  *
847
- * Leverages the Gravito runtime adapter to ensure compatibility across different
848
- * JavaScript runtimes (Bun, Node.js). Executes commands within a shell (`sh -c`)
849
- * to support pipes, redirects, and environment variables.
847
+ * Leverages the Nova Shell orchestration engine to ensure type-safe,
848
+ * shell-injection-resistant command execution. Supports pipes, redirects,
849
+ * and environment variables.
850
850
  *
851
851
  * @param command - Raw shell command string to execute.
852
852
  * @returns Resolves to a detailed `ProcessResult` object.
package/dist/index.d.ts CHANGED
@@ -674,7 +674,7 @@ declare class SchedulerManager {
674
674
  * @param hooks - Optional manager for lifecycle event hooks.
675
675
  * @param currentNodeRole - Role identifier for the local node (used for filtering).
676
676
  */
677
- constructor(lockManager: LockManager, logger?: Logger | undefined, hooks?: HookManager | undefined, currentNodeRole?: string | undefined);
677
+ constructor(lockManager: LockManager, logger?: Logger, hooks?: HookManager, currentNodeRole?: string);
678
678
  /**
679
679
  * Registers a new callback-based scheduled task.
680
680
  *
@@ -844,9 +844,9 @@ interface ProcessResult {
844
844
  /**
845
845
  * Spawns a shell command and asynchronously captures its full output.
846
846
  *
847
- * Leverages the Gravito runtime adapter to ensure compatibility across different
848
- * JavaScript runtimes (Bun, Node.js). Executes commands within a shell (`sh -c`)
849
- * to support pipes, redirects, and environment variables.
847
+ * Leverages the Nova Shell orchestration engine to ensure type-safe,
848
+ * shell-injection-resistant command execution. Supports pipes, redirects,
849
+ * and environment variables.
850
850
  *
851
851
  * @param command - Raw shell command string to execute.
852
852
  * @returns Resolves to a detailed `ProcessResult` object.
package/dist/index.js CHANGED
@@ -396,24 +396,19 @@ var LockManager = class {
396
396
  };
397
397
 
398
398
  // src/process/Process.ts
399
- import { getRuntimeAdapter } from "@gravito/core";
399
+ import { Shell } from "@gravito/nova";
400
400
  async function runProcess(command) {
401
- const runtime = getRuntimeAdapter();
402
- const proc = runtime.spawn(["sh", "-c", command], {
403
- stdout: "pipe",
404
- stderr: "pipe"
405
- });
406
- const [stdout, stderr] = await Promise.all([
407
- new Response(proc.stdout ?? null).text(),
408
- new Response(proc.stderr ?? null).text()
409
- ]);
410
- const exitCode = await proc.exited;
411
- return {
412
- exitCode,
413
- stdout,
414
- stderr,
415
- success: exitCode === 0
416
- };
401
+ try {
402
+ const result = await Shell.run`bash -c ${command}`.nothrow().run();
403
+ return result;
404
+ } catch (error) {
405
+ return {
406
+ exitCode: 1,
407
+ stdout: "",
408
+ stderr: error instanceof Error ? error.message : String(error),
409
+ success: false
410
+ };
411
+ }
417
412
  }
418
413
  var Process = class {
419
414
  /**
@@ -984,14 +979,15 @@ var SchedulerManager = class {
984
979
  if (task.background) {
985
980
  this.executeTask(task).catch((err) => {
986
981
  this.logger?.error(`Background task ${task.name} failed`, err);
987
- }).finally(async () => {
988
- if (task.preventOverlapping) {
982
+ return { success: false, timedOut: false };
983
+ }).then(async (result) => {
984
+ if (task.preventOverlapping && !result.timedOut) {
989
985
  await this.lockManager.release(runningLockKey);
990
986
  }
991
987
  });
992
988
  } else {
993
- await this.executeTask(task);
994
- if (task.preventOverlapping) {
989
+ const result = await this.executeTask(task);
990
+ if (task.preventOverlapping && !result.timedOut) {
995
991
  await this.lockManager.release(runningLockKey);
996
992
  }
997
993
  }
@@ -1056,7 +1052,7 @@ var SchedulerManager = class {
1056
1052
  } catch {
1057
1053
  }
1058
1054
  }
1059
- return;
1055
+ return { success: true, timedOut: false };
1060
1056
  } catch (err) {
1061
1057
  lastError = err;
1062
1058
  this.logger?.error(`Task ${task.name} failed (attempt ${attempt + 1})`, err);
@@ -1079,6 +1075,10 @@ var SchedulerManager = class {
1079
1075
  } catch {
1080
1076
  }
1081
1077
  }
1078
+ return {
1079
+ success: false,
1080
+ timedOut: lastError?.message.includes("timed out after") ?? false
1081
+ };
1082
1082
  }
1083
1083
  };
1084
1084
 
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@gravito/horizon",
3
- "version": "3.2.1",
3
+ "sideEffects": false,
4
+ "version": "3.2.2",
4
5
  "description": "Distributed task scheduler for Gravito framework",
5
6
  "main": "./dist/index.cjs",
6
7
  "module": "./dist/index.js",
@@ -20,6 +21,7 @@
20
21
  ],
21
22
  "scripts": {
22
23
  "build": "bun run build.ts",
24
+ "build:dts": "bun run build.ts --dts-only",
23
25
  "test": "bun test --timeout=10000",
24
26
  "lint": "biome lint ./src",
25
27
  "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
@@ -37,18 +39,20 @@
37
39
  ],
38
40
  "author": "Carl Lee <carllee0520@gmail.com>",
39
41
  "license": "MIT",
40
- "dependencies": {},
42
+ "dependencies": {
43
+ "@gravito/nova": "^1.0.2"
44
+ },
41
45
  "optionalDependencies": {
42
46
  "cron-parser": "^4.9.0"
43
47
  },
44
48
  "peerDependencies": {
45
- "@gravito/core": "^1.6.1",
46
- "@gravito/stasis": "^3.1.1"
49
+ "@gravito/core": "^2.0.0",
50
+ "@gravito/stasis": "^3.2.0"
47
51
  },
48
52
  "devDependencies": {
49
- "@gravito/stasis": "^3.1.1",
53
+ "@gravito/stasis": "workspace:*",
50
54
  "bun-types": "latest",
51
- "@gravito/core": "^1.6.1",
55
+ "@gravito/core": "workspace:*",
52
56
  "tsup": "^8.5.1",
53
57
  "typescript": "^5.9.3"
54
58
  },