@gravito/horizon 1.0.0-alpha.6 → 1.0.0-beta.6

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.js ADDED
@@ -0,0 +1,746 @@
1
+ import "./chunk-MCKGQKYU.js";
2
+
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");
21
+ }
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
+ );
59
+ }
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");
63
+ }
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;
96
+ }
97
+ }
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;
106
+ }
107
+ const start = parseInt(startStr, 10);
108
+ const end = parseInt(endStr, 10);
109
+ return value >= start && value <= end;
110
+ }
111
+ const expected = parseInt(pattern, 10);
112
+ if (max === 7 && expected === 7 && value === 0) {
113
+ return true;
114
+ }
115
+ return value === expected;
116
+ }
117
+
118
+ // src/CronParser.ts
119
+ var CronParser = class {
120
+ /**
121
+ * 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
+ */
127
+ static async nextDate(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
128
+ try {
129
+ const parser = await import("./parser-EEVME3L3.js");
130
+ const interval = parser.default.parseExpression(expression, {
131
+ currentDate,
132
+ tz: timezone
133
+ });
134
+ return interval.next().toDate();
135
+ } catch (_err) {
136
+ throw new Error(`Invalid cron expression: ${expression}`);
137
+ }
138
+ }
139
+ /**
140
+ * 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
+ */
146
+ static async isDue(expression, timezone = "UTC", currentDate = /* @__PURE__ */ new Date()) {
147
+ try {
148
+ return SimpleCronParser.isDue(expression, timezone, currentDate);
149
+ } catch (_e) {
150
+ }
151
+ try {
152
+ const previousMinute = new Date(currentDate.getTime() - 6e4);
153
+ const parser = await import("./parser-EEVME3L3.js");
154
+ const interval = parser.default.parseExpression(expression, {
155
+ currentDate: previousMinute,
156
+ tz: timezone
157
+ });
158
+ const nextRun = interval.next().toDate();
159
+ return this.minuteMatches(nextRun, currentDate);
160
+ } catch (_err) {
161
+ return false;
162
+ }
163
+ }
164
+ static minuteMatches(date1, date2) {
165
+ return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() && date1.getHours() === date2.getHours() && date1.getMinutes() === date2.getMinutes();
166
+ }
167
+ };
168
+
169
+ // src/locks/CacheLockStore.ts
170
+ var CacheLockStore = class {
171
+ constructor(cache, prefix = "scheduler:lock:") {
172
+ this.cache = cache;
173
+ this.prefix = prefix;
174
+ }
175
+ getKey(key) {
176
+ return this.prefix + key;
177
+ }
178
+ async acquire(key, ttlSeconds) {
179
+ return this.cache.add(this.getKey(key), "LOCKED", ttlSeconds);
180
+ }
181
+ async release(key) {
182
+ await this.cache.forget(this.getKey(key));
183
+ }
184
+ async forceAcquire(key, ttlSeconds) {
185
+ await this.cache.put(this.getKey(key), "LOCKED", ttlSeconds);
186
+ }
187
+ async exists(key) {
188
+ return this.cache.has(this.getKey(key));
189
+ }
190
+ };
191
+
192
+ // src/locks/MemoryLockStore.ts
193
+ var MemoryLockStore = class {
194
+ locks = /* @__PURE__ */ new Map();
195
+ async acquire(key, ttlSeconds) {
196
+ const NOW = Date.now();
197
+ const expiresAt = this.locks.get(key);
198
+ if (expiresAt && expiresAt > NOW) {
199
+ return false;
200
+ }
201
+ this.locks.set(key, NOW + ttlSeconds * 1e3);
202
+ return true;
203
+ }
204
+ async release(key) {
205
+ this.locks.delete(key);
206
+ }
207
+ async forceAcquire(key, ttlSeconds) {
208
+ this.locks.set(key, Date.now() + ttlSeconds * 1e3);
209
+ }
210
+ async exists(key) {
211
+ const expiresAt = this.locks.get(key);
212
+ if (!expiresAt) {
213
+ return false;
214
+ }
215
+ if (expiresAt <= Date.now()) {
216
+ this.locks.delete(key);
217
+ return false;
218
+ }
219
+ return true;
220
+ }
221
+ };
222
+
223
+ // src/locks/LockManager.ts
224
+ var LockManager = class {
225
+ store;
226
+ constructor(driver, context) {
227
+ if (typeof driver === "object") {
228
+ this.store = driver;
229
+ } else if (driver === "memory") {
230
+ this.store = new MemoryLockStore();
231
+ } else if (driver === "cache") {
232
+ if (!context?.cache) {
233
+ throw new Error("CacheManager is required for cache lock driver");
234
+ }
235
+ this.store = new CacheLockStore(context.cache);
236
+ } else {
237
+ this.store = new MemoryLockStore();
238
+ }
239
+ }
240
+ async acquire(key, ttlSeconds) {
241
+ return this.store.acquire(key, ttlSeconds);
242
+ }
243
+ async release(key) {
244
+ return this.store.release(key);
245
+ }
246
+ async forceAcquire(key, ttlSeconds) {
247
+ return this.store.forceAcquire(key, ttlSeconds);
248
+ }
249
+ async exists(key) {
250
+ return this.store.exists(key);
251
+ }
252
+ };
253
+
254
+ // src/process/Process.ts
255
+ async function runProcess(command) {
256
+ const proc = Bun.spawn(["sh", "-c", command], {
257
+ stdout: "pipe",
258
+ stderr: "pipe"
259
+ });
260
+ const [stdout, stderr] = await Promise.all([
261
+ new Response(proc.stdout).text(),
262
+ new Response(proc.stderr).text()
263
+ ]);
264
+ const exitCode = await proc.exited;
265
+ return {
266
+ exitCode,
267
+ stdout,
268
+ stderr,
269
+ success: exitCode === 0
270
+ };
271
+ }
272
+ var Process = class {
273
+ static async run(command) {
274
+ return runProcess(command);
275
+ }
276
+ };
277
+
278
+ // src/TaskSchedule.ts
279
+ var TaskSchedule = class {
280
+ task;
281
+ /**
282
+ * Create a new TaskSchedule instance.
283
+ *
284
+ * @param name - The unique name of the task.
285
+ * @param callback - The function to execute.
286
+ */
287
+ constructor(name, callback) {
288
+ this.task = {
289
+ name,
290
+ callback,
291
+ expression: "* * * * *",
292
+ // Default every minute
293
+ timezone: "UTC",
294
+ shouldRunOnOneServer: false,
295
+ lockTtl: 300,
296
+ // 5 minutes default
297
+ background: false,
298
+ // Wait for task to finish by default
299
+ onSuccessCallbacks: [],
300
+ onFailureCallbacks: []
301
+ };
302
+ }
303
+ // --- Frequency Methods ---
304
+ /**
305
+ * Set a custom cron expression.
306
+ *
307
+ * @param expression - Standard cron expression (e.g., "* * * * *")
308
+ * @returns The TaskSchedule instance.
309
+ */
310
+ cron(expression) {
311
+ this.task.expression = expression;
312
+ return this;
313
+ }
314
+ /**
315
+ * Run the task every minute.
316
+ *
317
+ * @returns The TaskSchedule instance.
318
+ */
319
+ everyMinute() {
320
+ return this.cron("* * * * *");
321
+ }
322
+ /**
323
+ * Run the task every 5 minutes.
324
+ *
325
+ * @returns The TaskSchedule instance.
326
+ */
327
+ everyFiveMinutes() {
328
+ return this.cron("*/5 * * * *");
329
+ }
330
+ /**
331
+ * Run the task every 10 minutes.
332
+ *
333
+ * @returns The TaskSchedule instance.
334
+ */
335
+ everyTenMinutes() {
336
+ return this.cron("*/10 * * * *");
337
+ }
338
+ /**
339
+ * Run the task every 15 minutes.
340
+ *
341
+ * @returns The TaskSchedule instance.
342
+ */
343
+ everyFifteenMinutes() {
344
+ return this.cron("*/15 * * * *");
345
+ }
346
+ /**
347
+ * Run the task every 30 minutes.
348
+ *
349
+ * @returns The TaskSchedule instance.
350
+ */
351
+ everyThirtyMinutes() {
352
+ return this.cron("0,30 * * * *");
353
+ }
354
+ /**
355
+ * Run the task hourly (at minute 0).
356
+ *
357
+ * @returns The TaskSchedule instance.
358
+ */
359
+ hourly() {
360
+ return this.cron("0 * * * *");
361
+ }
362
+ /**
363
+ * Run the task hourly at a specific minute.
364
+ *
365
+ * @param minute - Minute (0-59)
366
+ * @returns The TaskSchedule instance.
367
+ */
368
+ hourlyAt(minute) {
369
+ return this.cron(`${minute} * * * *`);
370
+ }
371
+ /**
372
+ * Run the task daily at midnight (00:00).
373
+ *
374
+ * @returns The TaskSchedule instance.
375
+ */
376
+ daily() {
377
+ return this.cron("0 0 * * *");
378
+ }
379
+ /**
380
+ * Run the task daily at a specific time.
381
+ *
382
+ * @param time - Time in "HH:mm" format (24h)
383
+ * @returns The TaskSchedule instance.
384
+ */
385
+ dailyAt(time) {
386
+ const [hour, minute] = time.split(":");
387
+ return this.cron(`${Number(minute)} ${Number(hour)} * * *`);
388
+ }
389
+ /**
390
+ * Run the task weekly on Sunday at midnight.
391
+ *
392
+ * @returns The TaskSchedule instance.
393
+ */
394
+ weekly() {
395
+ return this.cron("0 0 * * 0");
396
+ }
397
+ /**
398
+ * Run the task weekly on a specific day and time.
399
+ *
400
+ * @param day - Day of week (0-7, 0 or 7 is Sunday)
401
+ * @param time - Time in "HH:mm" format (default "00:00")
402
+ * @returns The TaskSchedule instance.
403
+ */
404
+ weeklyOn(day, time = "00:00") {
405
+ const [hour, minute] = time.split(":");
406
+ return this.cron(`${Number(minute)} ${Number(hour)} * * ${day}`);
407
+ }
408
+ /**
409
+ * Run the task monthly on the 1st day at midnight.
410
+ *
411
+ * @returns The TaskSchedule instance.
412
+ */
413
+ monthly() {
414
+ return this.cron("0 0 1 * *");
415
+ }
416
+ /**
417
+ * Run the task monthly on a specific day and time.
418
+ *
419
+ * @param day - Day of month (1-31)
420
+ * @param time - Time in "HH:mm" format (default "00:00")
421
+ * @returns The TaskSchedule instance.
422
+ */
423
+ monthlyOn(day, time = "00:00") {
424
+ const [hour, minute] = time.split(":");
425
+ return this.cron(`${Number(minute)} ${Number(hour)} ${day} * *`);
426
+ }
427
+ // --- Constraints ---
428
+ /**
429
+ * Set the timezone for the task execution.
430
+ *
431
+ * @param timezone - Timezone identifier (e.g., "Asia/Taipei", "UTC")
432
+ * @returns The TaskSchedule instance.
433
+ */
434
+ timezone(timezone) {
435
+ this.task.timezone = timezone;
436
+ return this;
437
+ }
438
+ /**
439
+ * Set the time of execution for the current frequency.
440
+ * Useful when chaining with daily(), weekly(), etc.
441
+ *
442
+ * @param time - Time in "HH:mm" format
443
+ * @returns The TaskSchedule instance.
444
+ */
445
+ at(time) {
446
+ const [hour, minute] = time.split(":");
447
+ const parts = this.task.expression.split(" ");
448
+ if (parts.length >= 5) {
449
+ parts[0] = String(Number(minute));
450
+ parts[1] = String(Number(hour));
451
+ this.task.expression = parts.join(" ");
452
+ }
453
+ return this;
454
+ }
455
+ /**
456
+ * Ensure task runs on only one server at a time (Distributed Locking).
457
+ * Requires a configured LockStore (Cache or Redis).
458
+ *
459
+ * @param lockTtlSeconds - Time in seconds to hold the lock (default 300)
460
+ * @returns The TaskSchedule instance.
461
+ */
462
+ onOneServer(lockTtlSeconds = 300) {
463
+ this.task.shouldRunOnOneServer = true;
464
+ this.task.lockTtl = lockTtlSeconds;
465
+ return this;
466
+ }
467
+ /**
468
+ * Alias for onOneServer.
469
+ * Prevents overlapping executions of the same task.
470
+ *
471
+ * @param expiresAt - Lock TTL in seconds
472
+ * @returns The TaskSchedule instance.
473
+ */
474
+ withoutOverlapping(expiresAt = 300) {
475
+ return this.onOneServer(expiresAt);
476
+ }
477
+ /**
478
+ * Run task in background (do not wait for completion in the loop).
479
+ * Note: In Node.js non-blocking environment this is largely semantic,
480
+ * but affects how we handle error catching and lock release.
481
+ *
482
+ * @returns The TaskSchedule instance.
483
+ */
484
+ runInBackground() {
485
+ this.task.background = true;
486
+ return this;
487
+ }
488
+ /**
489
+ * Restrict task execution to a specific node role.
490
+ *
491
+ * @param role - The required node role (e.g., 'api', 'worker')
492
+ * @returns The TaskSchedule instance.
493
+ */
494
+ onNode(role) {
495
+ this.task.nodeRole = role;
496
+ return this;
497
+ }
498
+ /**
499
+ * Set the command string for exec tasks.
500
+ *
501
+ * @param command - The command string.
502
+ * @returns The TaskSchedule instance.
503
+ * @internal
504
+ */
505
+ setCommand(command) {
506
+ this.task.command = command;
507
+ return this;
508
+ }
509
+ // --- Hooks ---
510
+ /**
511
+ * Register a callback to run on task success.
512
+ *
513
+ * @param callback - The callback function.
514
+ * @returns The TaskSchedule instance.
515
+ */
516
+ onSuccess(callback) {
517
+ this.task.onSuccessCallbacks.push(callback);
518
+ return this;
519
+ }
520
+ /**
521
+ * Register a callback to run on task failure.
522
+ *
523
+ * @param callback - The callback function.
524
+ * @returns The TaskSchedule instance.
525
+ */
526
+ onFailure(callback) {
527
+ this.task.onFailureCallbacks.push(callback);
528
+ return this;
529
+ }
530
+ /**
531
+ * Set a description for the task (useful for listing).
532
+ *
533
+ * @param _text - The description text.
534
+ * @returns The TaskSchedule instance.
535
+ */
536
+ description(_text) {
537
+ return this;
538
+ }
539
+ // --- Accessor ---
540
+ /**
541
+ * Get the underlying task configuration.
542
+ *
543
+ * @returns The ScheduledTask object.
544
+ */
545
+ getTask() {
546
+ return this.task;
547
+ }
548
+ };
549
+
550
+ // src/SchedulerManager.ts
551
+ var SchedulerManager = class {
552
+ constructor(lockManager, logger, hooks, currentNodeRole) {
553
+ this.lockManager = lockManager;
554
+ this.logger = logger;
555
+ this.hooks = hooks;
556
+ this.currentNodeRole = currentNodeRole;
557
+ }
558
+ tasks = [];
559
+ /**
560
+ * Define a new scheduled task.
561
+ *
562
+ * @param name - Unique name for the task
563
+ * @param callback - Function to execute
564
+ * @returns The newly created TaskSchedule.
565
+ */
566
+ task(name, callback) {
567
+ const task = new TaskSchedule(name, callback);
568
+ this.tasks.push(task);
569
+ return task;
570
+ }
571
+ /**
572
+ * Define a new scheduled command execution task.
573
+ *
574
+ * @param name - Unique name for the task
575
+ * @param command - Shell command to execute
576
+ * @returns The newly created TaskSchedule.
577
+ */
578
+ exec(name, command) {
579
+ const task = new TaskSchedule(name, async () => {
580
+ const result = await Process.run(command);
581
+ if (!result.success) {
582
+ throw new Error(`Command failed: ${result.stderr || result.stdout}`);
583
+ }
584
+ });
585
+ task.setCommand(command);
586
+ this.tasks.push(task);
587
+ return task;
588
+ }
589
+ /**
590
+ * Add a pre-configured task schedule object.
591
+ *
592
+ * @param schedule - The task schedule to add.
593
+ */
594
+ add(schedule) {
595
+ this.tasks.push(schedule);
596
+ }
597
+ /**
598
+ * Get all registered task definitions.
599
+ *
600
+ * @returns An array of scheduled tasks.
601
+ */
602
+ getTasks() {
603
+ return this.tasks.map((t) => t.getTask());
604
+ }
605
+ /**
606
+ * Trigger the scheduler to check and run due tasks.
607
+ * This is typically called every minute by a system cron or worker loop.
608
+ *
609
+ * @param date - The current reference date (default: now)
610
+ * @returns A promise that resolves when the scheduler run is complete.
611
+ */
612
+ async run(date = /* @__PURE__ */ new Date()) {
613
+ await this.hooks?.doAction("scheduler:run:start", { date });
614
+ const tasks = this.getTasks();
615
+ const dueTasks = [];
616
+ for (const task of tasks) {
617
+ if (await CronParser.isDue(task.expression, task.timezone, date)) {
618
+ dueTasks.push(task);
619
+ }
620
+ }
621
+ if (dueTasks.length > 0) {
622
+ }
623
+ for (const task of dueTasks) {
624
+ this.runTask(task, date).catch((err) => {
625
+ this.logger?.error(`[Scheduler] Unexpected error running task ${task.name}`, err);
626
+ });
627
+ }
628
+ await this.hooks?.doAction("scheduler:run:complete", { date, dueCount: dueTasks.length });
629
+ }
630
+ /**
631
+ * Execute a specific task with locking logic.
632
+ *
633
+ * @param task - The task to execute.
634
+ * @internal
635
+ */
636
+ async runTask(task, date = /* @__PURE__ */ new Date()) {
637
+ if (task.nodeRole && this.currentNodeRole && task.nodeRole !== this.currentNodeRole) {
638
+ return;
639
+ }
640
+ let acquiredLock = false;
641
+ 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")}`;
642
+ const lockKey = `task:${task.name}:${timestamp}`;
643
+ if (task.shouldRunOnOneServer) {
644
+ acquiredLock = await this.lockManager.acquire(lockKey, task.lockTtl);
645
+ if (!acquiredLock) {
646
+ return;
647
+ }
648
+ }
649
+ try {
650
+ if (task.background) {
651
+ this.executeTask(task).catch((err) => {
652
+ this.logger?.error(`Background task ${task.name} failed`, err);
653
+ });
654
+ } else {
655
+ await this.executeTask(task);
656
+ }
657
+ } catch (err) {
658
+ if (acquiredLock) {
659
+ await this.lockManager.release(lockKey);
660
+ }
661
+ throw err;
662
+ }
663
+ }
664
+ /**
665
+ * Execute the task callback and handle hooks.
666
+ *
667
+ * @param task - The task to execute.
668
+ */
669
+ async executeTask(task) {
670
+ const startTime = Date.now();
671
+ await this.hooks?.doAction("scheduler:task:start", { name: task.name, startTime });
672
+ try {
673
+ await task.callback();
674
+ const duration = Date.now() - startTime;
675
+ await this.hooks?.doAction("scheduler:task:success", { name: task.name, duration });
676
+ for (const cb of task.onSuccessCallbacks) {
677
+ try {
678
+ await cb({ name: task.name });
679
+ } catch {
680
+ }
681
+ }
682
+ } catch (err) {
683
+ const duration = Date.now() - startTime;
684
+ this.logger?.error(`Task ${task.name} failed`, err);
685
+ await this.hooks?.doAction("scheduler:task:failure", {
686
+ name: task.name,
687
+ error: err,
688
+ duration
689
+ });
690
+ for (const cb of task.onFailureCallbacks) {
691
+ try {
692
+ await cb(err);
693
+ } catch {
694
+ }
695
+ }
696
+ }
697
+ }
698
+ };
699
+
700
+ // src/OrbitHorizon.ts
701
+ var OrbitHorizon = class {
702
+ /**
703
+ * Install the Horizon Orbit into PlanetCore.
704
+ *
705
+ * @param core - The PlanetCore instance.
706
+ */
707
+ install(core) {
708
+ const config = core.config.get("scheduler", {});
709
+ const lockDriver = config.lock?.driver || "cache";
710
+ const exposeAs = config.exposeAs || "scheduler";
711
+ const nodeRole = config.nodeRole;
712
+ let lockManager;
713
+ if (lockDriver === "cache") {
714
+ const cacheManager = core.services.get("cache");
715
+ if (!cacheManager) {
716
+ core.logger.warn(
717
+ "[OrbitHorizon] Cache driver requested but cache service not found (ensure orbit-cache is loaded first). Falling back to Memory lock."
718
+ );
719
+ lockManager = new LockManager("memory");
720
+ } else {
721
+ lockManager = new LockManager("cache", { cache: cacheManager });
722
+ }
723
+ } else {
724
+ lockManager = new LockManager("memory");
725
+ }
726
+ const scheduler = new SchedulerManager(lockManager, core.logger, core.hooks, nodeRole);
727
+ core.services.set(exposeAs, scheduler);
728
+ core.adapter.use("*", async (c, next) => {
729
+ c.set("scheduler", scheduler);
730
+ await next();
731
+ return void 0;
732
+ });
733
+ core.logger.info(`[OrbitHorizon] Initialized (Driver: ${lockDriver})`);
734
+ }
735
+ };
736
+ export {
737
+ CacheLockStore,
738
+ CronParser,
739
+ LockManager,
740
+ MemoryLockStore,
741
+ OrbitHorizon,
742
+ Process,
743
+ SchedulerManager,
744
+ TaskSchedule,
745
+ runProcess
746
+ };