@stonyx/cron 0.2.0 → 0.2.1-alpha.1
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/.claude/architecture.md +215 -0
- package/.claude/extension-guide.md +291 -0
- package/.claude/improvements.md +53 -0
- package/.claude/project-structure.md +139 -0
- package/.claude/testing.md +85 -0
- package/.github/workflows/ci.yml +5 -25
- package/.github/workflows/publish.yml +51 -0
- package/.gitignore +3 -0
- package/.npmignore +3 -1
- package/README.md +3 -4
- package/logs/error.log +2 -1
- package/package.json +16 -5
- package/pnpm-lock.yaml +11 -10
- package/src/cron-parser.js +246 -0
- package/src/job.js +200 -0
- package/src/locked.js +34 -0
- package/src/normalize.js +163 -0
- package/src/run-log.js +79 -0
- package/src/schedule.js +81 -0
- package/src/service.js +303 -0
- package/.claude/settings.local.json +0 -15
- package/.git/config +0 -24
- package/stonyx-bootstrap.cjs +0 -9
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Architecture & Core Components
|
|
2
|
+
|
|
3
|
+
## Core Components Deep Dive
|
|
4
|
+
|
|
5
|
+
### Cron Class (`stonyx-cron/src/main.js`)
|
|
6
|
+
|
|
7
|
+
**Properties:**
|
|
8
|
+
```javascript
|
|
9
|
+
jobs = {}; // Object mapping job keys to job objects
|
|
10
|
+
heap = new MinHeap(); // Priority queue of jobs
|
|
11
|
+
timer = null; // setTimeout handle for next scheduled run
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Job Object Schema:**
|
|
15
|
+
```javascript
|
|
16
|
+
{
|
|
17
|
+
key: string, // Unique identifier for the job
|
|
18
|
+
callback: Function, // Async function to execute
|
|
19
|
+
interval: number, // Interval in seconds
|
|
20
|
+
nextTrigger: number // Unix timestamp in seconds when job should run
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Public Methods:**
|
|
25
|
+
|
|
26
|
+
**`register(key, callback, interval, runOnInit=false)`**
|
|
27
|
+
- Registers a new recurring job
|
|
28
|
+
- `key` (string): Unique job identifier
|
|
29
|
+
- `callback` (Function): Async function to execute on each trigger
|
|
30
|
+
- `interval` (number): Time in seconds between executions
|
|
31
|
+
- `runOnInit` (boolean): Whether to run callback immediately
|
|
32
|
+
|
|
33
|
+
**`unregister(key)`**
|
|
34
|
+
- Removes a job from the scheduler
|
|
35
|
+
- Deletes from `jobs` object and removes from heap
|
|
36
|
+
- Reschedules next run after removal
|
|
37
|
+
|
|
38
|
+
**`scheduleNextRun()`**
|
|
39
|
+
- Clears existing timer
|
|
40
|
+
- Peeks at next job in heap
|
|
41
|
+
- Calculates delay: `(nextTrigger - now) * 1000` (converts seconds to milliseconds)
|
|
42
|
+
- Sets setTimeout for next job execution
|
|
43
|
+
|
|
44
|
+
**`runDueJobs()`**
|
|
45
|
+
- Processes all jobs with `nextTrigger <= now`
|
|
46
|
+
- Executes job callbacks with error handling
|
|
47
|
+
- Reschedules each job after execution
|
|
48
|
+
- Calls `scheduleNextRun()` when done
|
|
49
|
+
|
|
50
|
+
**`setNextTrigger(job)`**
|
|
51
|
+
- Updates job's `nextTrigger` to `now + interval`
|
|
52
|
+
- Uses `getTimestamp()` which returns **seconds**, not milliseconds
|
|
53
|
+
|
|
54
|
+
**`log(text, key=null)`**
|
|
55
|
+
- Conditional logging based on `config.cron?.log`
|
|
56
|
+
- Formats messages as `Cron::${key} - ${text}:` or `Cron - ${text}:`
|
|
57
|
+
|
|
58
|
+
### MinHeap Class (`stonyx-cron/src/min-heap.js`)
|
|
59
|
+
|
|
60
|
+
**Properties:**
|
|
61
|
+
```javascript
|
|
62
|
+
items = []; // Array backing the heap
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Public Methods:**
|
|
66
|
+
|
|
67
|
+
**`push(job)`**
|
|
68
|
+
- Adds job to end of array
|
|
69
|
+
- Bubbles up to maintain heap property
|
|
70
|
+
|
|
71
|
+
**`pop()`**
|
|
72
|
+
- Removes and returns root (minimum `nextTrigger`)
|
|
73
|
+
- Replaces root with last item
|
|
74
|
+
- Bubbles down to restore heap property
|
|
75
|
+
|
|
76
|
+
**`peek()`**
|
|
77
|
+
- Returns root without removing it
|
|
78
|
+
- O(1) access to next job to run
|
|
79
|
+
|
|
80
|
+
**`remove(job)`**
|
|
81
|
+
- Finds job by reference equality (`indexOf`)
|
|
82
|
+
- Replaces with last item
|
|
83
|
+
- Bubbles both up and down to restore heap property
|
|
84
|
+
|
|
85
|
+
**`isEmpty()`**
|
|
86
|
+
- Returns `true` if `items.length === 0`
|
|
87
|
+
|
|
88
|
+
**Internal Methods:**
|
|
89
|
+
|
|
90
|
+
**`bubbleUp()`**
|
|
91
|
+
- Moves last item up tree until heap property is satisfied
|
|
92
|
+
- Compares `nextTrigger` values with parent
|
|
93
|
+
|
|
94
|
+
**`bubbleDown()`**
|
|
95
|
+
- Moves root down tree, swapping with smallest child
|
|
96
|
+
- Maintains min-heap property
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Dependencies & Integration
|
|
101
|
+
|
|
102
|
+
### Stonyx Framework Integration
|
|
103
|
+
```javascript
|
|
104
|
+
import config from 'stonyx/config'; // Configuration system
|
|
105
|
+
import log from 'stonyx/log'; // Logging system
|
|
106
|
+
import { setupIntegrationTests } from "stonyx/test-helpers"; // Test utilities
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### External Dependencies
|
|
110
|
+
```javascript
|
|
111
|
+
import { getTimestamp } from "@stonyx/utils/date"; // Time utilities
|
|
112
|
+
import QUnit from 'qunit'; // Test framework
|
|
113
|
+
import sinon from 'sinon'; // Test spies/stubs/fake timers
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Critical Time Handling Detail
|
|
117
|
+
**`getTimestamp()` returns Unix timestamps in SECONDS, not milliseconds.**
|
|
118
|
+
|
|
119
|
+
This affects:
|
|
120
|
+
- Job `interval` values (specified in seconds)
|
|
121
|
+
- Job `nextTrigger` values (stored in seconds)
|
|
122
|
+
- Delay calculation in `scheduleNextRun()`: must multiply by 1000 for `setTimeout`
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
// CORRECT: Convert seconds to milliseconds for setTimeout
|
|
126
|
+
const delay = Math.max(0, nextJob.nextTrigger - getTimestamp()) * 1000;
|
|
127
|
+
this.timer = setTimeout(() => this.runDueJobs(), delay);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Code Patterns & Conventions
|
|
133
|
+
|
|
134
|
+
### Module System
|
|
135
|
+
All modules use ES Module syntax with default exports:
|
|
136
|
+
```javascript
|
|
137
|
+
export default class Cron { ... }
|
|
138
|
+
export default class MinHeap { ... }
|
|
139
|
+
export default { log: true, logColor: '#888' }; // config
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Imports use full package paths:
|
|
143
|
+
```javascript
|
|
144
|
+
import Cron from '@stonyx/cron';
|
|
145
|
+
import MinHeap from '@stonyx/cron/min-heap';
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Logging Patterns
|
|
149
|
+
**Always check config before logging:**
|
|
150
|
+
```javascript
|
|
151
|
+
if (config.debug) this.log('job has been triggered', job.key);
|
|
152
|
+
if (config.cron?.log) log.cron(`${tag} - ${text}:`);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Use appropriate log methods:**
|
|
156
|
+
- `log.cron()` for informational cron messages
|
|
157
|
+
- `log.error()` for error conditions
|
|
158
|
+
|
|
159
|
+
### Error Handling
|
|
160
|
+
**Never let errors crash the scheduler:**
|
|
161
|
+
```javascript
|
|
162
|
+
try {
|
|
163
|
+
await job.callback();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
log.error(`Cron job "${job.key}" failed:`, err);
|
|
166
|
+
}
|
|
167
|
+
// Always reschedule job, even after error
|
|
168
|
+
this.setNextTrigger(job);
|
|
169
|
+
heap.push(job);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Time Handling
|
|
173
|
+
**Always use `getTimestamp()` for current time:**
|
|
174
|
+
```javascript
|
|
175
|
+
// CORRECT
|
|
176
|
+
const now = getTimestamp();
|
|
177
|
+
job.nextTrigger = getTimestamp() + parseInt(job.interval, 10);
|
|
178
|
+
|
|
179
|
+
// WRONG - don't use Date.now() or other time sources
|
|
180
|
+
const now = Date.now(); // WRONG: milliseconds instead of seconds
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Configuration Reference
|
|
186
|
+
|
|
187
|
+
### Module Configuration
|
|
188
|
+
**File:** `stonyx-cron/config/environment.js`
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
const { CRON_LOG } = process;
|
|
192
|
+
|
|
193
|
+
export default {
|
|
194
|
+
log: CRON_LOG ?? true, // Enable/disable cron logging
|
|
195
|
+
logColor: '#888', // Color for cron logs
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Environment Variable:**
|
|
200
|
+
- `CRON_LOG`: Set to `false` to disable cron logging
|
|
201
|
+
|
|
202
|
+
### Using Configuration in Code
|
|
203
|
+
```javascript
|
|
204
|
+
import config from 'stonyx/config';
|
|
205
|
+
|
|
206
|
+
// Access cron config
|
|
207
|
+
if (config.cron?.log) {
|
|
208
|
+
log.cron('message');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Access debug flag (from main Stonyx config)
|
|
212
|
+
if (config.debug) {
|
|
213
|
+
this.log('debug message');
|
|
214
|
+
}
|
|
215
|
+
```
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Extension Guide
|
|
2
|
+
|
|
3
|
+
## Extension Points
|
|
4
|
+
|
|
5
|
+
This section provides specific guidance for common feature additions.
|
|
6
|
+
|
|
7
|
+
### Adding Job Metadata
|
|
8
|
+
|
|
9
|
+
**Where:** `stonyx-cron/src/main.js`
|
|
10
|
+
|
|
11
|
+
**Pattern:**
|
|
12
|
+
```javascript
|
|
13
|
+
// In register method, extend job object:
|
|
14
|
+
register(key, callback, interval, runOnInit=false, metadata={}) {
|
|
15
|
+
const job = {
|
|
16
|
+
callback,
|
|
17
|
+
interval,
|
|
18
|
+
key,
|
|
19
|
+
metadata // Add metadata field
|
|
20
|
+
};
|
|
21
|
+
this.jobs[key] = job;
|
|
22
|
+
// ... rest of method
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Access metadata in runDueJobs or other methods:
|
|
26
|
+
const { metadata } = job;
|
|
27
|
+
if (metadata.priority === 'high') {
|
|
28
|
+
// Handle high priority jobs differently
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Tests to add:** Verify metadata is stored and accessible in job object.
|
|
33
|
+
|
|
34
|
+
### Adding Priority Levels
|
|
35
|
+
|
|
36
|
+
**Where:**
|
|
37
|
+
- `stonyx-cron/src/main.js` (Cron class)
|
|
38
|
+
- `stonyx-cron/src/min-heap.js` (MinHeap comparison)
|
|
39
|
+
|
|
40
|
+
**Pattern:**
|
|
41
|
+
```javascript
|
|
42
|
+
// Option 1: Secondary sort on priority
|
|
43
|
+
// In min-heap.js bubbleUp/bubbleDown, change comparison:
|
|
44
|
+
if (this.items[idx].nextTrigger < this.items[parentIdx].nextTrigger ||
|
|
45
|
+
(this.items[idx].nextTrigger === this.items[parentIdx].nextTrigger &&
|
|
46
|
+
this.items[idx].priority > this.items[parentIdx].priority)) {
|
|
47
|
+
// Swap
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Option 2: Offset nextTrigger by priority
|
|
51
|
+
// In main.js setNextTrigger:
|
|
52
|
+
setNextTrigger(job) {
|
|
53
|
+
const priorityOffset = job.priority || 0;
|
|
54
|
+
job.nextTrigger = getTimestamp() + parseInt(job.interval, 10) - priorityOffset;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Considerations:**
|
|
59
|
+
- Option 1 maintains exact timing but adds complexity to heap
|
|
60
|
+
- Option 2 is simpler but slightly alters execution timing
|
|
61
|
+
- Add `priority` field to job object schema
|
|
62
|
+
|
|
63
|
+
**Tests to add:** Jobs with higher priority run before lower priority when due at same time.
|
|
64
|
+
|
|
65
|
+
### Adding Job Statistics
|
|
66
|
+
|
|
67
|
+
**Where:** `stonyx-cron/src/main.js`
|
|
68
|
+
|
|
69
|
+
**Pattern:**
|
|
70
|
+
```javascript
|
|
71
|
+
// Extend job object with stats:
|
|
72
|
+
register(key, callback, interval, runOnInit=false) {
|
|
73
|
+
const job = {
|
|
74
|
+
callback,
|
|
75
|
+
interval,
|
|
76
|
+
key,
|
|
77
|
+
stats: {
|
|
78
|
+
runCount: 0,
|
|
79
|
+
lastRun: null,
|
|
80
|
+
lastError: null,
|
|
81
|
+
errorCount: 0
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
// ...
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Update stats in runDueJobs:
|
|
88
|
+
async runDueJobs() {
|
|
89
|
+
// ...
|
|
90
|
+
try {
|
|
91
|
+
await job.callback();
|
|
92
|
+
job.stats.runCount++;
|
|
93
|
+
job.stats.lastRun = getTimestamp();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
job.stats.errorCount++;
|
|
96
|
+
job.stats.lastError = err.message;
|
|
97
|
+
log.error(`Cron job "${job.key}" failed:`, err);
|
|
98
|
+
}
|
|
99
|
+
// ...
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add method to retrieve stats:
|
|
103
|
+
getJobStats(key) {
|
|
104
|
+
return this.jobs[key]?.stats;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Tests to add:** Verify stats increment correctly on success/error.
|
|
109
|
+
|
|
110
|
+
### Adding Persistence
|
|
111
|
+
|
|
112
|
+
**Where:** `stonyx-cron/src/main.js`
|
|
113
|
+
|
|
114
|
+
**Pattern:**
|
|
115
|
+
```javascript
|
|
116
|
+
// Add save/load methods:
|
|
117
|
+
async saveJobs() {
|
|
118
|
+
const jobData = Object.values(this.jobs).map(job => ({
|
|
119
|
+
key: job.key,
|
|
120
|
+
interval: job.interval,
|
|
121
|
+
// Don't save callback - must be re-registered
|
|
122
|
+
}));
|
|
123
|
+
// Write to file/database
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async loadJobs(callbacks) {
|
|
127
|
+
const jobData = // Read from file/database
|
|
128
|
+
jobData.forEach(({ key, interval }) => {
|
|
129
|
+
if (callbacks[key]) {
|
|
130
|
+
this.register(key, callbacks[key], interval);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Considerations:**
|
|
137
|
+
- Cannot serialize callbacks - must be re-registered on load
|
|
138
|
+
- Save job metadata, intervals, and keys
|
|
139
|
+
- Consider saving `nextTrigger` to maintain schedule across restarts
|
|
140
|
+
|
|
141
|
+
**Tests to add:** Verify jobs can be saved and restored with same intervals.
|
|
142
|
+
|
|
143
|
+
### Adding One-Time Jobs
|
|
144
|
+
|
|
145
|
+
**Where:** `stonyx-cron/src/main.js`
|
|
146
|
+
|
|
147
|
+
**Pattern:**
|
|
148
|
+
```javascript
|
|
149
|
+
// Add oneTime flag to job schema:
|
|
150
|
+
register(key, callback, interval, runOnInit=false, oneTime=false) {
|
|
151
|
+
const job = { callback, interval, key, oneTime };
|
|
152
|
+
// ...
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Modify runDueJobs to unregister one-time jobs:
|
|
156
|
+
async runDueJobs() {
|
|
157
|
+
const now = getTimestamp();
|
|
158
|
+
const { heap } = this;
|
|
159
|
+
|
|
160
|
+
while (!heap.isEmpty() && heap.peek().nextTrigger <= now) {
|
|
161
|
+
const job = heap.pop();
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await job.callback();
|
|
165
|
+
} catch (err) {
|
|
166
|
+
log.error(`Cron job "${job.key}" failed:`, err);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (job.oneTime) {
|
|
170
|
+
delete this.jobs[job.key]; // Don't reschedule
|
|
171
|
+
if (config.debug) this.log('one-time job completed', job.key);
|
|
172
|
+
} else {
|
|
173
|
+
this.setNextTrigger(job);
|
|
174
|
+
heap.push(job);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.scheduleNextRun();
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Tests to add:** One-time jobs execute once and are not rescheduled.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Common Pitfalls & Gotchas
|
|
187
|
+
|
|
188
|
+
### Time Units Confusion
|
|
189
|
+
**Pitfall:** Mixing seconds and milliseconds
|
|
190
|
+
|
|
191
|
+
**Correct:**
|
|
192
|
+
```javascript
|
|
193
|
+
// getTimestamp() returns SECONDS
|
|
194
|
+
const now = getTimestamp();
|
|
195
|
+
job.interval = 5; // 5 seconds
|
|
196
|
+
job.nextTrigger = now + 5; // 5 seconds from now
|
|
197
|
+
|
|
198
|
+
// setTimeout expects MILLISECONDS
|
|
199
|
+
const delay = (job.nextTrigger - now) * 1000;
|
|
200
|
+
setTimeout(callback, delay);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Wrong:**
|
|
204
|
+
```javascript
|
|
205
|
+
// DON'T DO THIS
|
|
206
|
+
const delay = job.nextTrigger - getTimestamp(); // Missing * 1000
|
|
207
|
+
setTimeout(callback, delay); // Will run almost immediately!
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Singleton Behavior
|
|
211
|
+
**Pitfall:** Creating multiple Cron instances unexpectedly
|
|
212
|
+
|
|
213
|
+
**Behavior:**
|
|
214
|
+
```javascript
|
|
215
|
+
const cron1 = new Cron();
|
|
216
|
+
const cron2 = new Cron();
|
|
217
|
+
console.log(cron1 === cron2); // true - same instance!
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Implication:** Registering jobs on any instance affects the same scheduler.
|
|
221
|
+
|
|
222
|
+
### Job Callback Async Handling
|
|
223
|
+
**Pitfall:** Not awaiting async callbacks
|
|
224
|
+
|
|
225
|
+
**Correct:**
|
|
226
|
+
```javascript
|
|
227
|
+
cron.register('job', async () => {
|
|
228
|
+
await someAsyncOperation();
|
|
229
|
+
}, 10);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The scheduler awaits the callback, so errors are caught properly.
|
|
233
|
+
|
|
234
|
+
**Wrong:**
|
|
235
|
+
```javascript
|
|
236
|
+
cron.register('job', () => {
|
|
237
|
+
someAsyncOperation(); // Not awaited - errors won't be caught!
|
|
238
|
+
}, 10);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Heap Reference Equality
|
|
242
|
+
**Pitfall:** Modifying job objects outside the scheduler
|
|
243
|
+
|
|
244
|
+
**Issue:**
|
|
245
|
+
```javascript
|
|
246
|
+
const job = cron.jobs['myJob'];
|
|
247
|
+
job.interval = 20; // This doesn't update the heap!
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Solution:** Always use `unregister` then `register` to update job properties.
|
|
251
|
+
|
|
252
|
+
### Test Isolation
|
|
253
|
+
**Pitfall:** Tests interfering with each other due to singleton
|
|
254
|
+
|
|
255
|
+
**Solution:**
|
|
256
|
+
```javascript
|
|
257
|
+
hooks.beforeEach(function () {
|
|
258
|
+
clock = sinon.useFakeTimers({ shouldAdvanceTime: false });
|
|
259
|
+
cron = new Cron(); // Gets singleton
|
|
260
|
+
|
|
261
|
+
// Clear previous test's jobs
|
|
262
|
+
Object.keys(cron.jobs).forEach(key => cron.unregister(key));
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
hooks.afterEach(function () {
|
|
266
|
+
sinon.restore(); // Restores real timers
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Future Enhancement Opportunities
|
|
273
|
+
|
|
274
|
+
Ideas aligned with current architecture:
|
|
275
|
+
|
|
276
|
+
1. **Job Prioritization** - Add priority levels for jobs due at same time
|
|
277
|
+
2. **Persistence Layer** - Save/restore jobs across restarts
|
|
278
|
+
3. **Job Statistics** - Track run counts, errors, execution time
|
|
279
|
+
4. **Job Dependencies** - Wait for other jobs before running
|
|
280
|
+
5. **Cron Expression Support** - Use cron syntax instead of intervals
|
|
281
|
+
6. **Job Timeout** - Cancel jobs that run too long
|
|
282
|
+
7. **Pause/Resume** - Temporarily stop/start the scheduler
|
|
283
|
+
8. **Job Groups** - Batch operations on related jobs
|
|
284
|
+
9. **Event Emitters** - Emit events on job lifecycle (start, complete, error)
|
|
285
|
+
10. **Rate Limiting** - Limit concurrent job execution
|
|
286
|
+
|
|
287
|
+
All enhancements should maintain:
|
|
288
|
+
- Zero-crash guarantee (catch all errors)
|
|
289
|
+
- O(log n) scheduling efficiency
|
|
290
|
+
- Singleton pattern
|
|
291
|
+
- Configuration-driven logging
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Improvement Opportunities
|
|
2
|
+
|
|
3
|
+
## `register()` does not validate duplicate keys
|
|
4
|
+
|
|
5
|
+
**File:** `src/main.js`, `register()` method (line 67)
|
|
6
|
+
|
|
7
|
+
When `register()` is called with a key that already exists, it overwrites the entry in `this.jobs[key]` but never removes the old job object from the heap. The old entry remains orphaned in the heap and will still trigger when its `nextTrigger` time arrives, even though it is no longer tracked in `this.jobs`.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
register(key, callback, interval, runOnInit=false) {
|
|
11
|
+
const job = { callback, interval, key };
|
|
12
|
+
this.jobs[key] = job; // overwrites old reference
|
|
13
|
+
this.setNextTrigger(job);
|
|
14
|
+
this.heap.push(job); // pushes new entry, old entry still in heap
|
|
15
|
+
// ...
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Impact:** The orphaned heap entry will fire its old callback on the old schedule. Since it is no longer in `this.jobs`, it cannot be unregistered.
|
|
20
|
+
|
|
21
|
+
**Suggested fix:** Check for an existing key and call `unregister(key)` before registering, or throw an error if the key is already registered.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## `runOnInit` callback is not awaited
|
|
26
|
+
|
|
27
|
+
**File:** `src/main.js`, `register()` method (line 79)
|
|
28
|
+
|
|
29
|
+
When `runOnInit` is `true`, the callback is invoked synchronously without `await`:
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
if (runOnInit) {
|
|
33
|
+
try {
|
|
34
|
+
callback(); // not awaited
|
|
35
|
+
} catch (err) {
|
|
36
|
+
log.error(`Cron job "${key}" failed on init:`, err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This is inconsistent with `runDueJobs()`, which does await the callback:
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
try {
|
|
45
|
+
await job.callback(); // awaited
|
|
46
|
+
} catch (err) {
|
|
47
|
+
log.error(`Cron job "${job.key}" failed:`, err);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Impact:** If the callback is async and throws, the rejection will not be caught by the `try/catch` block in `register()`. The error becomes an unhandled promise rejection instead of being logged.
|
|
52
|
+
|
|
53
|
+
**Suggested fix:** Add `await` to the `callback()` call in the `runOnInit` branch and make `register()` async, or wrap in a `.catch()` handler.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# stonyx-cron Project Structure
|
|
2
|
+
|
|
3
|
+
## Detailed Guides
|
|
4
|
+
|
|
5
|
+
- [Architecture & Core Components](./architecture.md) — Deep dive into Cron and MinHeap classes, dependencies, code patterns, and configuration reference
|
|
6
|
+
- [Testing Guidelines](./testing.md) — Test structure, fake timers, spies/stubs patterns, and running tests
|
|
7
|
+
- [Extension Guide](./extension-guide.md) — Extension points, common pitfalls, and future enhancement opportunities
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Project Overview
|
|
12
|
+
|
|
13
|
+
**stonyx-cron** is a lightweight async job scheduler for the Stonyx framework that uses a min-heap priority queue for efficient job scheduling.
|
|
14
|
+
|
|
15
|
+
**Core Purpose:**
|
|
16
|
+
- Schedule and execute async jobs at specified intervals
|
|
17
|
+
- O(log n) scheduling efficiency via min-heap priority queue
|
|
18
|
+
- Robust error handling that never crashes the scheduler
|
|
19
|
+
- Configuration-driven logging aligned with Stonyx patterns
|
|
20
|
+
|
|
21
|
+
**Technology Stack:**
|
|
22
|
+
- **Module System:** ES Modules (ESM)
|
|
23
|
+
- **Testing:** QUnit with Sinon for spies/stubs/fake timers
|
|
24
|
+
- **Dependencies:** Stonyx framework, @stonyx/utils/date
|
|
25
|
+
- **Node Version:** Specified in `.nvmrc`
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Architecture & Design Decisions
|
|
30
|
+
|
|
31
|
+
### Singleton Pattern
|
|
32
|
+
The `Cron` class uses a singleton pattern to ensure only one scheduler instance exists across the entire application. The constructor returns the existing instance if one has already been created.
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
constructor() {
|
|
36
|
+
if (Cron.instance) return Cron.instance;
|
|
37
|
+
Cron.instance = this;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Rationale:** Prevents multiple competing schedulers and ensures consistent job management.
|
|
42
|
+
|
|
43
|
+
### Min-Heap Priority Queue
|
|
44
|
+
Jobs are stored in a min-heap ordered by `nextTrigger` timestamp, allowing O(log n) insertion and O(1) peek of the next job to run.
|
|
45
|
+
|
|
46
|
+
**Heap Property:** Parent nodes have earlier `nextTrigger` values than their children, so the root is always the next job to execute.
|
|
47
|
+
|
|
48
|
+
### Async Job Execution Strategy
|
|
49
|
+
- Jobs are executed with `await job.callback()` to handle async operations
|
|
50
|
+
- Errors are caught and logged but never crash the scheduler
|
|
51
|
+
- After execution (success or failure), jobs are rescheduled and re-inserted into the heap
|
|
52
|
+
- The scheduler reschedules itself after processing all due jobs
|
|
53
|
+
|
|
54
|
+
### Configuration-Driven Logging
|
|
55
|
+
Logging follows Stonyx patterns:
|
|
56
|
+
- Check `config.debug` before debug logs
|
|
57
|
+
- Check `config.cron?.log` before cron-specific logs
|
|
58
|
+
- Use `log.cron()` for cron messages, `log.error()` for errors
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. File Structure
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
stonyx-cron/
|
|
66
|
+
├── .claude/
|
|
67
|
+
│ ├── project-structure.md - Project overview & structure
|
|
68
|
+
│ ├── architecture.md - Core components & code patterns
|
|
69
|
+
│ ├── testing.md - Testing guidelines
|
|
70
|
+
│ ├── extension-guide.md - Extension points & pitfalls
|
|
71
|
+
│ └── improvements.md - Known improvement opportunities
|
|
72
|
+
├── .github/
|
|
73
|
+
│ └── workflows/
|
|
74
|
+
│ ├── ci.yml - CI pipeline (PR checks)
|
|
75
|
+
│ └── publish.yml - NPM publish workflow
|
|
76
|
+
├── config/
|
|
77
|
+
│ └── environment.js - Cron module configuration
|
|
78
|
+
├── src/
|
|
79
|
+
│ ├── main.js - Cron class (singleton scheduler)
|
|
80
|
+
│ └── min-heap.js - MinHeap priority queue implementation
|
|
81
|
+
├── test/
|
|
82
|
+
│ └── unit/
|
|
83
|
+
│ ├── cron-test.js - Cron class unit tests
|
|
84
|
+
│ └── min-heap-test.js - MinHeap unit tests
|
|
85
|
+
├── package.json - Package metadata and exports
|
|
86
|
+
├── README.md - Project documentation
|
|
87
|
+
├── LICENSE.md - Apache 2.0 license
|
|
88
|
+
├── .npmignore - Files excluded from npm publish
|
|
89
|
+
├── .nvmrc - Node version specification
|
|
90
|
+
└── .gitignore - Git ignore rules
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 4. Package Exports
|
|
96
|
+
|
|
97
|
+
**File:** `stonyx-cron/package.json`
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"exports": {
|
|
102
|
+
".": "./src/main.js",
|
|
103
|
+
"./min-heap": "./src/min-heap.js"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Usage:**
|
|
109
|
+
```javascript
|
|
110
|
+
// Import Cron class (default export)
|
|
111
|
+
import Cron from '@stonyx/cron';
|
|
112
|
+
|
|
113
|
+
// Import MinHeap class (for advanced usage)
|
|
114
|
+
import MinHeap from '@stonyx/cron/min-heap';
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Related Resources
|
|
120
|
+
|
|
121
|
+
### Stonyx Framework
|
|
122
|
+
- Main repository: https://github.com/abofs/stonyx
|
|
123
|
+
- Configuration patterns: See `stonyx/config` documentation
|
|
124
|
+
- Logging patterns: See `stonyx/log` documentation
|
|
125
|
+
- Test helpers: See `stonyx/test-helpers` documentation
|
|
126
|
+
|
|
127
|
+
### Testing
|
|
128
|
+
- QUnit documentation: https://qunitjs.com/
|
|
129
|
+
- Sinon documentation: https://sinonjs.org/
|
|
130
|
+
- Fake timers: https://sinonjs.org/releases/latest/fake-timers/
|
|
131
|
+
|
|
132
|
+
### Data Structures
|
|
133
|
+
- Min-heap algorithm: https://en.wikipedia.org/wiki/Binary_heap
|
|
134
|
+
- Priority queue patterns: See MinHeap implementation
|
|
135
|
+
|
|
136
|
+
### Project Repository
|
|
137
|
+
- GitHub: https://github.com/abofs/stonyx-cron
|
|
138
|
+
- Issues: https://github.com/abofs/stonyx-cron/issues
|
|
139
|
+
- License: Apache 2.0
|