@zerotal/scheduler 1.13.1 → 1.13.3

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/CHANGELOG.md CHANGED
@@ -8,6 +8,17 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.13.3] — 2026-08-31
12
+
13
+ ### Added
14
+
15
+ - **A `zt doctor` check for schedules with nothing running them.** Schedules register in
16
+ `worker` and `console`, not `web` — the right design, and it means a second process is
17
+ required that the framework starts happily without.
18
+
19
+ Registered and never seen is a **failure**; registered and stale is a warning, because a
20
+ worker mid-restart is not a missing one. The scheduler beats every minute while running.
21
+
11
22
  ## [1.10.0] — 2026-08-30
12
23
 
13
24
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/scheduler",
3
- "version": "1.13.1",
3
+ "version": "1.13.3",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -30,7 +30,7 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@zerotal/core": "1.13.1"
33
+ "@zerotal/core": "1.13.3"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "^5.8.0"
@@ -1,6 +1,7 @@
1
1
  import { ScheduledTask } from "./ScheduledTask.ts";
2
2
  import type { TaskCallback } from "./ScheduledTask.ts";
3
3
  import { frameworkLog } from "@zerotal/core/logger";
4
+ import { Heartbeat } from "@zerotal/core/heartbeat";
4
5
 
5
6
  export class SchedulerManager {
6
7
  private _tasks: Map<string, ScheduledTask> = new Map();
@@ -34,8 +35,15 @@ export class SchedulerManager {
34
35
  * stopped every schedule in the app. Which ones those are is not a thing you can
35
36
  * see from a crash loop.
36
37
  */
38
+ private _stopBeat: (() => void) | undefined;
39
+
37
40
  start(): void {
38
41
  this._started = true;
42
+ // Beat while running, so `zt doctor` can tell "schedules registered and a
43
+ // worker is running them" from "schedules registered and nothing is". The
44
+ // second is silent by nature: the web process has no way to notice, and the
45
+ // work simply does not happen.
46
+ this._stopBeat = Heartbeat.start("scheduler", { detail: `${this._tasks.size} task(s)` });
39
47
  for (const task of this._tasks.values()) {
40
48
  try {
41
49
  task.start();
@@ -51,6 +59,8 @@ export class SchedulerManager {
51
59
  }
52
60
 
53
61
  stop(): void {
62
+ this._stopBeat?.();
63
+ this._stopBeat = undefined;
54
64
  for (const task of this._tasks.values()) task.stop();
55
65
  this._started = false;
56
66
  frameworkLog("scheduler").info("All tasks stopped");
@@ -8,6 +8,8 @@ import { schedulesConcern } from "../conventions.ts";
8
8
  import { installSchedulerObservability } from "../observability.ts";
9
9
  import { installSchedulerMonitor } from "../monitor.ts";
10
10
  import { FileScheduleRunStore, installScheduleRunLog, resolveRunLogConfig } from "../runLog.ts";
11
+ import { workerLivenessCheck } from "@zerotal/core/heartbeat";
12
+ import type { DoctorCheck } from "@zerotal/core";
11
13
 
12
14
  declare module "@zerotal/core" {
13
15
  interface ContainerBindings {
@@ -99,4 +101,31 @@ export class SchedulerProvider extends ServiceProvider {
99
101
  const scheduler = this.app.container.tryMake("scheduler") as SchedulerManager | undefined;
100
102
  scheduler?.stop();
101
103
  }
104
+
105
+ /**
106
+ * Report schedules registered with nothing running them.
107
+ *
108
+ * The reported production failure this exists for: an app ran for weeks with no
109
+ * worker process, and every scheduled task silently did not execute. Nothing
110
+ * logged, because from the web process's point of view nothing was wrong.
111
+ */
112
+ override doctorChecks(): DoctorCheck[] {
113
+ return [
114
+ workerLivenessCheck({
115
+ id: "scheduler-running",
116
+ label: "Scheduler",
117
+ name: "scheduler",
118
+ // Registered tasks, read from the manager rather than from config: a
119
+ // schedule that failed to register is not work anything can do.
120
+ hasWork: () => {
121
+ const count = this.app.container.tryMake("scheduler")?.tasks.size ?? 0;
122
+ return { has: count > 0, summary: `${count} schedule(s) registered` };
123
+ },
124
+ // Generous: the beat is every 60s, and a worker mid-restart should not
125
+ // read as a missing one.
126
+ staleAfter: 15 * 60,
127
+ command: "bun zt worker",
128
+ }),
129
+ ];
130
+ }
102
131
  }