@stonyx/cron 0.2.1-alpha.55 → 0.2.1-alpha.56

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.
Files changed (2) hide show
  1. package/dist/service.js +51 -23
  2. package/package.json +1 -1
package/dist/service.js CHANGED
@@ -100,32 +100,60 @@ export default class CronService {
100
100
  if (this.started)
101
101
  return;
102
102
  this.started = true;
103
- if (initialJobs) {
104
- for (const job of initialJobs) {
105
- // A `runningAtMs` on a rehydrated job is always stale. The claim it
106
- // records was taken by a process that is gone, so nothing will ever
107
- // settle it, and nothing reaps it — there is no lease on the field
108
- // (tracked on #35). Left in place it is a permanently dead job that
109
- // still reports healthy: `isDue` returns false forever because of the
110
- // flag, `run()` answers `'already running'` forever, `update()` never
111
- // touches `state.runningAtMs`, and `status()` counts it like any other.
112
- // The consumer's only recovery would be remove() + add(), losing the
113
- // job id and its run history.
114
- //
115
- // Same hazard, same treatment as the hand-release on the `'removed'`
116
- // path in `#executeClaimed`: a claim with no reachable settle must be
117
- // released. Assigned directly rather than via `applyResult` for the same
118
- // reason — this releases the claim and nothing else. The job did not
119
- // run, so it gets no run-log row, no `lastStatus`, and no recomputed
120
- // `nextRunAtMs`; it is rescheduled from the store's own value below.
121
- job.state.runningAtMs = undefined;
122
- this.jobs.set(job.id, job);
123
- if (job.enabled && job.state.nextRunAtMs) {
124
- this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
103
+ // `finally`, not a trailing statement. Reconciled with the same guard #53
104
+ // puts around `register`'s `runOnInit` invocation: a scheduler that is
105
+ // marked started but never armed is the terminal state both fixes exist to
106
+ // remove, reached here through the other entry point. `initialJobs` crosses
107
+ // a serialization boundary — it is whatever the consumer's store handed
108
+ // back — so `Job[]` is a compile-time claim about runtime data, and a row
109
+ // missing `state` throws mid-loop. Without this, `start()` leaves
110
+ // `started: true` (so it is now a no-op), the rows registered before the
111
+ // throw sitting in the heap, and NO timer: measured, `status()` then
112
+ // reports `{ started: true, jobCount: 1, nextWakeAtMs: <real> }` while
113
+ // nothing will ever fire. Silent and healthy-looking, again.
114
+ try {
115
+ if (initialJobs) {
116
+ for (const job of initialJobs) {
117
+ // A `runningAtMs` on a rehydrated job is always stale. The claim it
118
+ // records was taken by a process that is gone, so nothing will ever
119
+ // settle it, and nothing reaps it — there is no lease on the field
120
+ // (tracked on #35). Left in place it is a permanently dead job that
121
+ // still reports healthy: `isDue` returns false forever because of the
122
+ // flag, `run()` answers `'already running'` forever, `update()` never
123
+ // touches `state.runningAtMs`, and `status()` counts it like any other.
124
+ // The consumer's only recovery would be remove() + add(), losing the
125
+ // job id and its run history.
126
+ //
127
+ // Same hazard, same treatment as the hand-release on the `'removed'`
128
+ // path in `#executeClaimed`: a claim with no reachable settle must be
129
+ // released. Assigned directly rather than via `applyResult` for the same
130
+ // reason — this releases the claim and nothing else. The job did not
131
+ // run, so it gets no run-log row, no `lastStatus`, and no recomputed
132
+ // `nextRunAtMs`; it is rescheduled from the store's own value below.
133
+ //
134
+ // Written unconditionally, and deliberately NOT guarded on a
135
+ // `job.state.runningAtMs` read. Guarding it makes `start()` accept a
136
+ // row whose `state` is frozen — `structuredClone` + `Object.freeze`
137
+ // is an ordinary defensive rehydration — and that row is not usable
138
+ // by this class at all: `markRunning` writes the same field on every
139
+ // execution. Measured, the guard moves the failure from a throw out
140
+ // of `start()`, which the consumer's own `await` can catch, to a
141
+ // TypeError raised inside `onTimer`'s batch claim — a bare timer
142
+ // callback, so it surfaces as an unhandled rejection and is
143
+ // process-fatal under Node's default. Failing loudly at the store
144
+ // boundary is the better of the two, and the `finally` above keeps
145
+ // the rows that loaded before it scheduled.
146
+ job.state.runningAtMs = undefined;
147
+ this.jobs.set(job.id, job);
148
+ if (job.enabled && job.state.nextRunAtMs) {
149
+ this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
150
+ }
125
151
  }
126
152
  }
127
153
  }
128
- this.armTimer();
154
+ finally {
155
+ this.armTimer();
156
+ }
129
157
  }
130
158
  /**
131
159
  * Stop the service. Clears timer.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "keywords": [
4
4
  "stonyx-module"
5
5
  ],
6
- "version": "0.2.1-alpha.55",
6
+ "version": "0.2.1-alpha.56",
7
7
  "description": "Cron/job scheduler for Stonyx framework",
8
8
  "main": "dist/main.js",
9
9
  "types": "dist/main.d.ts",