@stonyx/cron 0.2.1-alpha.13 → 0.2.1-alpha.15
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/main.d.ts +15 -2
- package/dist/main.js +63 -9
- package/package.json +1 -1
package/dist/main.d.ts
CHANGED
|
@@ -3,14 +3,18 @@ interface CronJob extends HeapItem {
|
|
|
3
3
|
callback: () => void | Promise<void>;
|
|
4
4
|
interval: string;
|
|
5
5
|
key: string;
|
|
6
|
+
/**
|
|
7
|
+
* Timestamp (ms) at which the current invocation started; `undefined` when the
|
|
8
|
+
* job is idle. Mirrors `job.state.runningAtMs` in the service tier
|
|
9
|
+
* (`src/job.ts` `markRunning`/`applyResult`/`isDue`).
|
|
10
|
+
*/
|
|
11
|
+
runningAtMs?: number;
|
|
6
12
|
}
|
|
7
13
|
export default class Cron {
|
|
8
14
|
static instance: Cron | null;
|
|
9
15
|
jobs: Record<string, CronJob>;
|
|
10
16
|
heap: MinHeap<CronJob>;
|
|
11
17
|
timer: ReturnType<typeof setTimeout> | null;
|
|
12
|
-
/** Keys whose previous invocation has not settled yet. */
|
|
13
|
-
inFlight: Set<string>;
|
|
14
18
|
constructor();
|
|
15
19
|
init(): Promise<void>;
|
|
16
20
|
scheduleNextRun(): void;
|
|
@@ -24,8 +28,17 @@ export default class Cron {
|
|
|
24
28
|
* job stack invocations on itself).
|
|
25
29
|
*/
|
|
26
30
|
safeInvoke(job: CronJob, runOnInit?: boolean): void;
|
|
31
|
+
/** Release a job's in-flight guard. Only ever called for the job it belongs to. */
|
|
32
|
+
release(job: CronJob): void;
|
|
27
33
|
register(key: string, callback: () => void | Promise<void>, interval: string, runOnInit?: boolean): void;
|
|
28
34
|
unregister(key: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Parse a job interval (whole seconds, as a string) into a positive integer.
|
|
37
|
+
*
|
|
38
|
+
* Returns `null` when the value cannot be parsed at all, so callers can choose
|
|
39
|
+
* between failing fast (`register`) and falling back (`setNextTrigger`).
|
|
40
|
+
*/
|
|
41
|
+
parseInterval(interval: string): number | null;
|
|
29
42
|
setNextTrigger(job: CronJob): void;
|
|
30
43
|
log(text: string, key?: string | null): void;
|
|
31
44
|
}
|
package/dist/main.js
CHANGED
|
@@ -17,13 +17,20 @@ import config from 'stonyx/config';
|
|
|
17
17
|
import log from 'stonyx/log';
|
|
18
18
|
import { getTimestamp } from '@stonyx/utils/date';
|
|
19
19
|
import MinHeap from './min-heap.js';
|
|
20
|
+
/**
|
|
21
|
+
* Floor for a job interval, in whole seconds.
|
|
22
|
+
*
|
|
23
|
+
* `runDueJobs` no longer awaits the callback, so `next.nextTrigger > now` is the
|
|
24
|
+
* drain loop's only exit condition *and* the loop has no suspension point left.
|
|
25
|
+
* An interval that fails to advance `nextTrigger` therefore spins the loop
|
|
26
|
+
* forever and blocks the event loop, rather than merely scheduling too often.
|
|
27
|
+
*/
|
|
28
|
+
const MIN_INTERVAL_SECONDS = 1;
|
|
20
29
|
export default class Cron {
|
|
21
30
|
static instance;
|
|
22
31
|
jobs = {};
|
|
23
32
|
heap = new MinHeap();
|
|
24
33
|
timer = null;
|
|
25
|
-
/** Keys whose previous invocation has not settled yet. */
|
|
26
|
-
inFlight = new Set();
|
|
27
34
|
constructor() {
|
|
28
35
|
if (Cron.instance)
|
|
29
36
|
return Cron.instance;
|
|
@@ -78,27 +85,59 @@ export default class Cron {
|
|
|
78
85
|
safeInvoke(job, runOnInit = false) {
|
|
79
86
|
const { key } = job;
|
|
80
87
|
const context = runOnInit ? 'failed on init:' : 'failed:';
|
|
81
|
-
|
|
88
|
+
// The in-flight guard lives on the job object, not in a module-level set
|
|
89
|
+
// keyed by string. That is what gives each invocation an identity: the only
|
|
90
|
+
// thing that ever clears the flag is the settle handler of the invocation
|
|
91
|
+
// that set it, and that handler closes over this exact job object. A stale
|
|
92
|
+
// handler therefore cannot release a *later* invocation's guard. It also
|
|
93
|
+
// matches the in-repo idiom one tier up (`job.state.runningAtMs`).
|
|
94
|
+
//
|
|
95
|
+
// `unregister` needs no explicit clear as a result: the flag is dropped with
|
|
96
|
+
// the job object, so a re-registered key gets a fresh object and runs
|
|
97
|
+
// immediately, while the abandoned invocation can only ever release itself.
|
|
98
|
+
if (job.runningAtMs !== undefined) {
|
|
82
99
|
log.warn(`Cron job "${key}" is still running; skipping this tick`);
|
|
83
100
|
return;
|
|
84
101
|
}
|
|
85
|
-
|
|
102
|
+
job.runningAtMs = Date.now();
|
|
86
103
|
try {
|
|
87
104
|
const result = job.callback();
|
|
88
105
|
if (result && typeof result.then === 'function') {
|
|
89
106
|
Promise.resolve(result)
|
|
90
107
|
.catch(err => log.error(`Cron job "${key}" ${context}`, err))
|
|
91
|
-
.finally(() => this.
|
|
108
|
+
.finally(() => this.release(job));
|
|
92
109
|
return;
|
|
93
110
|
}
|
|
94
|
-
this.
|
|
111
|
+
this.release(job);
|
|
95
112
|
}
|
|
96
113
|
catch (err) {
|
|
97
|
-
this.
|
|
114
|
+
this.release(job);
|
|
98
115
|
log.error(`Cron job "${key}" ${context}`, err);
|
|
99
116
|
}
|
|
100
117
|
}
|
|
118
|
+
/** Release a job's in-flight guard. Only ever called for the job it belongs to. */
|
|
119
|
+
release(job) {
|
|
120
|
+
job.runningAtMs = undefined;
|
|
121
|
+
}
|
|
101
122
|
register(key, callback, interval, runOnInit = false) {
|
|
123
|
+
const seconds = this.parseInterval(interval);
|
|
124
|
+
// Fail fast rather than clamp. An unparseable interval is a programming
|
|
125
|
+
// error with exactly one likely cause — a cron expression handed to the
|
|
126
|
+
// legacy class, which takes whole seconds — and clamping it would silently
|
|
127
|
+
// run a job intended for every 5 minutes once per second, hammering whatever
|
|
128
|
+
// the callback talks to. Throwing surfaces it at the call site, at boot,
|
|
129
|
+
// before anything is scheduled. A degenerate-but-parseable interval (`'0'`,
|
|
130
|
+
// `'-5'`) is a different case: it is interpretable as "as often as possible"
|
|
131
|
+
// and is clamped to the floor with one warning.
|
|
132
|
+
if (seconds === null) {
|
|
133
|
+
throw new TypeError(`Cron job ${JSON.stringify(key)} has an invalid interval ${JSON.stringify(interval)}: `
|
|
134
|
+
+ 'expected whole seconds (e.g. \'30\'). The legacy Cron class does not accept cron '
|
|
135
|
+
+ 'expressions — use CronService for those.');
|
|
136
|
+
}
|
|
137
|
+
if (parseInt(interval, 10) < MIN_INTERVAL_SECONDS) {
|
|
138
|
+
log.warn(`Cron job ${JSON.stringify(key)} interval ${JSON.stringify(interval)} is below the `
|
|
139
|
+
+ `${MIN_INTERVAL_SECONDS}s floor; clamping to ${MIN_INTERVAL_SECONDS}s`);
|
|
140
|
+
}
|
|
102
141
|
const job = { callback, interval, key, nextTrigger: 0 };
|
|
103
142
|
this.jobs[key] = job;
|
|
104
143
|
this.setNextTrigger(job);
|
|
@@ -117,13 +156,28 @@ export default class Cron {
|
|
|
117
156
|
return;
|
|
118
157
|
delete jobs[key];
|
|
119
158
|
heap.remove(job);
|
|
120
|
-
this.inFlight.delete(key);
|
|
121
159
|
if (config.debug)
|
|
122
160
|
this.log('job has been unregistered', key);
|
|
123
161
|
this.scheduleNextRun();
|
|
124
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Parse a job interval (whole seconds, as a string) into a positive integer.
|
|
165
|
+
*
|
|
166
|
+
* Returns `null` when the value cannot be parsed at all, so callers can choose
|
|
167
|
+
* between failing fast (`register`) and falling back (`setNextTrigger`).
|
|
168
|
+
*/
|
|
169
|
+
parseInterval(interval) {
|
|
170
|
+
const seconds = parseInt(interval, 10);
|
|
171
|
+
if (!Number.isFinite(seconds))
|
|
172
|
+
return null;
|
|
173
|
+
return Math.max(MIN_INTERVAL_SECONDS, seconds);
|
|
174
|
+
}
|
|
125
175
|
setNextTrigger(job) {
|
|
126
|
-
|
|
176
|
+
// `register` rejects an unparseable interval up front; this floor is the
|
|
177
|
+
// backstop for a job object mutated after registration (`cron.jobs` is
|
|
178
|
+
// public, mutable state) and is what actually guarantees the drain loop
|
|
179
|
+
// terminates. Never let `nextTrigger` land on `NaN` or on `now`.
|
|
180
|
+
job.nextTrigger = getTimestamp() + (this.parseInterval(job.interval) ?? MIN_INTERVAL_SECONDS);
|
|
127
181
|
}
|
|
128
182
|
log(text, key = null) {
|
|
129
183
|
if (!config.cron?.log)
|