@stonyx/cron 0.2.1-alpha.41 → 0.2.1-alpha.42

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/README.md CHANGED
@@ -35,6 +35,8 @@ When a job is executed, its next trigger time is updated, and it is re-inserted
35
35
  | `register` | `key: string, callback: Function, interval: number, runOnInit?: boolean` | Register a new job with a given interval in seconds. If `runOnInit` is true, the job runs immediately upon registration. |
36
36
  | `unregister` | `key: string` | Remove a previously registered job. |
37
37
 
38
+ > Job callbacks are invoked fire-and-forget: two *different* jobs due in the same tick may overlap, and a job whose previous invocation has not settled is skipped (and logged) when it next comes due rather than running concurrently with itself. A callback that throws — synchronously or by rejecting — is logged, never propagated.
39
+
38
40
  > `MinHeap` is also exported as a public subpath (`@stonyx/cron/min-heap`) and can be imported directly for advanced usage.
39
41
 
40
42
  ## Configuration
package/dist/main.d.ts CHANGED
@@ -3,6 +3,7 @@ interface CronJob extends HeapItem {
3
3
  callback: () => void | Promise<void>;
4
4
  interval: string;
5
5
  key: string;
6
+ running: boolean;
6
7
  }
7
8
  export default class Cron {
8
9
  static instance: Cron | null;
@@ -15,6 +16,15 @@ export default class Cron {
15
16
  runDueJobs(): Promise<void>;
16
17
  register(key: string, callback: () => void | Promise<void>, interval: string, runOnInit?: boolean): void;
17
18
  unregister(key: string): void;
19
+ /**
20
+ * Invoke a consumer callback without ever blocking the caller.
21
+ *
22
+ * Synchronous throws and rejected thenables are both routed to `log.error`
23
+ * with `errorMessage`; a callback that never settles simply never clears its
24
+ * in-flight flag. The job is skipped (and logged) while a previous invocation
25
+ * is still running, so fire-and-forget cannot stack invocations on one job.
26
+ */
27
+ invokeJob(job: CronJob, errorMessage: string): void;
18
28
  setNextTrigger(job: CronJob): void;
19
29
  log(text: string, key?: string | null): void;
20
30
  }
package/dist/main.js CHANGED
@@ -55,19 +55,17 @@ export default class Cron {
55
55
  const job = heap.pop();
56
56
  if (config.debug)
57
57
  this.log('job has been triggered', job.key);
58
- try {
59
- await job.callback();
60
- }
61
- catch (err) {
62
- log.error(`Cron job "${job.key}" failed:`, err);
63
- }
58
+ // Reschedule before invoking: a consumer callback is never awaited here,
59
+ // so a callback that hangs or rejects can no longer starve the drain loop
60
+ // or leave the job orphaned outside the heap.
64
61
  this.setNextTrigger(job);
65
62
  heap.push(job);
63
+ this.invokeJob(job, `Cron job "${job.key}" failed:`);
66
64
  }
67
65
  this.scheduleNextRun();
68
66
  }
69
67
  register(key, callback, interval, runOnInit = false) {
70
- const job = { callback, interval, key, nextTrigger: 0 };
68
+ const job = { callback, interval, key, nextTrigger: 0, running: false };
71
69
  this.jobs[key] = job;
72
70
  this.setNextTrigger(job);
73
71
  this.heap.push(job);
@@ -75,12 +73,7 @@ export default class Cron {
75
73
  this.log(`job has been registered with interval: ${interval}`, key);
76
74
  }
77
75
  if (runOnInit) {
78
- try {
79
- callback();
80
- }
81
- catch (err) {
82
- log.error(`Cron job "${key}" failed on init:`, err);
83
- }
76
+ this.invokeJob(job, `Cron job "${key}" failed on init:`);
84
77
  }
85
78
  this.scheduleNextRun();
86
79
  }
@@ -95,6 +88,39 @@ export default class Cron {
95
88
  this.log('job has been unregistered', key);
96
89
  this.scheduleNextRun();
97
90
  }
91
+ /**
92
+ * Invoke a consumer callback without ever blocking the caller.
93
+ *
94
+ * Synchronous throws and rejected thenables are both routed to `log.error`
95
+ * with `errorMessage`; a callback that never settles simply never clears its
96
+ * in-flight flag. The job is skipped (and logged) while a previous invocation
97
+ * is still running, so fire-and-forget cannot stack invocations on one job.
98
+ */
99
+ invokeJob(job, errorMessage) {
100
+ if (job.running) {
101
+ this.log('job is still running, skipping this run', job.key);
102
+ return;
103
+ }
104
+ job.running = true;
105
+ const settle = () => { job.running = false; };
106
+ let result;
107
+ try {
108
+ result = job.callback();
109
+ }
110
+ catch (err) {
111
+ log.error(errorMessage, err);
112
+ settle();
113
+ return;
114
+ }
115
+ if (!result || typeof result.then !== 'function') {
116
+ settle();
117
+ return;
118
+ }
119
+ result.then(settle, (err) => {
120
+ log.error(errorMessage, err);
121
+ settle();
122
+ });
123
+ }
98
124
  setNextTrigger(job) {
99
125
  job.nextTrigger = getTimestamp() + parseInt(job.interval, 10);
100
126
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "keywords": [
4
4
  "stonyx-module"
5
5
  ],
6
- "version": "0.2.1-alpha.41",
6
+ "version": "0.2.1-alpha.42",
7
7
  "description": "Cron/job scheduler for Stonyx framework",
8
8
  "main": "dist/main.js",
9
9
  "types": "dist/main.d.ts",