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

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 +58 -23
  2. package/package.json +1 -1
package/dist/service.js CHANGED
@@ -65,6 +65,13 @@ function forLog(value, maxLength) {
65
65
  * reject instead of returning an `ExecuteResult`: a contract violation in the
66
66
  * function written to prevent exactly that. Reading and coercing `message`
67
67
  * inside the `try` is what closes it.
68
+ *
69
+ * `Cron` in `main.ts` carries its own `describeError` and the two deliberately
70
+ * differ: it renders for a log line only, so it prefers `err.stack`; this one
71
+ * is also returned to the caller as `ExecuteResult.error` and persisted in a
72
+ * per-job run log, where a stack would be an unbounded blob in every stored
73
+ * failure. Recorded in `docs/architecture.md` under Error Handling — do not
74
+ * merge them into a shared helper without reading that first.
68
75
  */
69
76
  function describeError(err) {
70
77
  try {
@@ -100,32 +107,60 @@ export default class CronService {
100
107
  if (this.started)
101
108
  return;
102
109
  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 });
110
+ // `finally`, not a trailing statement. Reconciled with the same guard #53
111
+ // puts around `register`'s `runOnInit` invocation: a scheduler that is
112
+ // marked started but never armed is the terminal state both fixes exist to
113
+ // remove, reached here through the other entry point. `initialJobs` crosses
114
+ // a serialization boundary — it is whatever the consumer's store handed
115
+ // back — so `Job[]` is a compile-time claim about runtime data, and a row
116
+ // missing `state` throws mid-loop. Without this, `start()` leaves
117
+ // `started: true` (so it is now a no-op), the rows registered before the
118
+ // throw sitting in the heap, and NO timer: measured, `status()` then
119
+ // reports `{ started: true, jobCount: 1, nextWakeAtMs: <real> }` while
120
+ // nothing will ever fire. Silent and healthy-looking, again.
121
+ try {
122
+ if (initialJobs) {
123
+ for (const job of initialJobs) {
124
+ // A `runningAtMs` on a rehydrated job is always stale. The claim it
125
+ // records was taken by a process that is gone, so nothing will ever
126
+ // settle it, and nothing reaps it — there is no lease on the field
127
+ // (tracked on #35). Left in place it is a permanently dead job that
128
+ // still reports healthy: `isDue` returns false forever because of the
129
+ // flag, `run()` answers `'already running'` forever, `update()` never
130
+ // touches `state.runningAtMs`, and `status()` counts it like any other.
131
+ // The consumer's only recovery would be remove() + add(), losing the
132
+ // job id and its run history.
133
+ //
134
+ // Same hazard, same treatment as the hand-release on the `'removed'`
135
+ // path in `#executeClaimed`: a claim with no reachable settle must be
136
+ // released. Assigned directly rather than via `applyResult` for the same
137
+ // reason — this releases the claim and nothing else. The job did not
138
+ // run, so it gets no run-log row, no `lastStatus`, and no recomputed
139
+ // `nextRunAtMs`; it is rescheduled from the store's own value below.
140
+ //
141
+ // Written unconditionally, and deliberately NOT guarded on a
142
+ // `job.state.runningAtMs` read. Guarding it makes `start()` accept a
143
+ // row whose `state` is frozen — `structuredClone` + `Object.freeze`
144
+ // is an ordinary defensive rehydration — and that row is not usable
145
+ // by this class at all: `markRunning` writes the same field on every
146
+ // execution. Measured, the guard moves the failure from a throw out
147
+ // of `start()`, which the consumer's own `await` can catch, to a
148
+ // TypeError raised inside `onTimer`'s batch claim — a bare timer
149
+ // callback, so it surfaces as an unhandled rejection and is
150
+ // process-fatal under Node's default. Failing loudly at the store
151
+ // boundary is the better of the two, and the `finally` above keeps
152
+ // the rows that loaded before it scheduled.
153
+ job.state.runningAtMs = undefined;
154
+ this.jobs.set(job.id, job);
155
+ if (job.enabled && job.state.nextRunAtMs) {
156
+ this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
157
+ }
125
158
  }
126
159
  }
127
160
  }
128
- this.armTimer();
161
+ finally {
162
+ this.armTimer();
163
+ }
129
164
  }
130
165
  /**
131
166
  * 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.57",
7
7
  "description": "Cron/job scheduler for Stonyx framework",
8
8
  "main": "dist/main.js",
9
9
  "types": "dist/main.d.ts",