@stonyx/cron 0.2.0 → 0.2.1-beta.0
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/project-structure.md +771 -0
- package/.git/config +8 -14
- package/.github/workflows/ci.yml +5 -25
- package/.github/workflows/publish.yml +35 -0
- package/.gitignore +3 -0
- package/README.md +3 -4
- package/logs/error.log +2 -1
- package/package.json +5 -1
- package/.claude/settings.local.json +0 -15
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
# stonyx-cron Project Structure
|
|
2
|
+
|
|
3
|
+
## 1. Project Overview
|
|
4
|
+
|
|
5
|
+
**stonyx-cron** is a lightweight async job scheduler for the Stonyx framework that uses a min-heap priority queue for efficient job scheduling.
|
|
6
|
+
|
|
7
|
+
**Core Purpose:**
|
|
8
|
+
- Schedule and execute async jobs at specified intervals
|
|
9
|
+
- O(log n) scheduling efficiency via min-heap priority queue
|
|
10
|
+
- Robust error handling that never crashes the scheduler
|
|
11
|
+
- Configuration-driven logging aligned with Stonyx patterns
|
|
12
|
+
|
|
13
|
+
**Technology Stack:**
|
|
14
|
+
- **Module System:** ES Modules (ESM)
|
|
15
|
+
- **Testing:** QUnit with Sinon for spies/stubs/fake timers
|
|
16
|
+
- **Dependencies:** Stonyx framework, @stonyx/utils/date
|
|
17
|
+
- **Node Version:** Specified in `.nvmrc`
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Architecture & Design Decisions
|
|
22
|
+
|
|
23
|
+
### Singleton Pattern
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
constructor() {
|
|
28
|
+
if (Cron.instance) return Cron.instance;
|
|
29
|
+
Cron.instance = this;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Rationale:** Prevents multiple competing schedulers and ensures consistent job management.
|
|
34
|
+
|
|
35
|
+
### Min-Heap Priority Queue
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
**Heap Property:** Parent nodes have earlier `nextTrigger` values than their children, so the root is always the next job to execute.
|
|
39
|
+
|
|
40
|
+
### Async Job Execution Strategy
|
|
41
|
+
- Jobs are executed with `await job.callback()` to handle async operations
|
|
42
|
+
- Errors are caught and logged but never crash the scheduler
|
|
43
|
+
- After execution (success or failure), jobs are rescheduled and re-inserted into the heap
|
|
44
|
+
- The scheduler reschedules itself after processing all due jobs
|
|
45
|
+
|
|
46
|
+
### Configuration-Driven Logging
|
|
47
|
+
Logging follows Stonyx patterns:
|
|
48
|
+
- Check `config.debug` before debug logs
|
|
49
|
+
- Check `config.cron?.log` before cron-specific logs
|
|
50
|
+
- Use `log.cron()` for cron messages, `log.error()` for errors
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 3. File Structure
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
stonyx-cron/
|
|
58
|
+
├── .claude/
|
|
59
|
+
│ └── project-structure.md - This document
|
|
60
|
+
├── .github/
|
|
61
|
+
│ └── workflows/ - CI/CD configuration
|
|
62
|
+
├── config/
|
|
63
|
+
│ └── environment.js - Cron module configuration
|
|
64
|
+
├── src/
|
|
65
|
+
│ ├── main.js - Cron class (singleton scheduler)
|
|
66
|
+
│ └── min-heap.js - MinHeap priority queue implementation
|
|
67
|
+
├── test/
|
|
68
|
+
│ └── unit/
|
|
69
|
+
│ ├── cron-test.js - Cron class unit tests
|
|
70
|
+
│ └── min-heap-test.js - MinHeap unit tests
|
|
71
|
+
├── package.json - Package metadata and exports
|
|
72
|
+
├── stonyx-bootstrap.cjs - QUnit test bootstrap
|
|
73
|
+
├── README.md - Project documentation
|
|
74
|
+
├── LICENSE.md - Apache 2.0 license
|
|
75
|
+
├── .nvmrc - Node version specification
|
|
76
|
+
└── .gitignore - Git ignore rules
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 4. Core Components Deep Dive
|
|
82
|
+
|
|
83
|
+
### Cron Class (`stonyx-cron/src/main.js`)
|
|
84
|
+
|
|
85
|
+
**Properties:**
|
|
86
|
+
```javascript
|
|
87
|
+
jobs = {}; // Object mapping job keys to job objects
|
|
88
|
+
heap = new MinHeap(); // Priority queue of jobs
|
|
89
|
+
timer = null; // setTimeout handle for next scheduled run
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Job Object Schema:**
|
|
93
|
+
```javascript
|
|
94
|
+
{
|
|
95
|
+
key: string, // Unique identifier for the job
|
|
96
|
+
callback: Function, // Async function to execute
|
|
97
|
+
interval: number, // Interval in seconds
|
|
98
|
+
nextTrigger: number // Unix timestamp in seconds when job should run
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Public Methods:**
|
|
103
|
+
|
|
104
|
+
**`register(key, callback, interval, runOnInit=false)`**
|
|
105
|
+
- Registers a new recurring job
|
|
106
|
+
- `key` (string): Unique job identifier
|
|
107
|
+
- `callback` (Function): Async function to execute on each trigger
|
|
108
|
+
- `interval` (number): Time in seconds between executions
|
|
109
|
+
- `runOnInit` (boolean): Whether to run callback immediately
|
|
110
|
+
|
|
111
|
+
**`unregister(key)`**
|
|
112
|
+
- Removes a job from the scheduler
|
|
113
|
+
- Deletes from `jobs` object and removes from heap
|
|
114
|
+
- Reschedules next run after removal
|
|
115
|
+
|
|
116
|
+
**`scheduleNextRun()`**
|
|
117
|
+
- Clears existing timer
|
|
118
|
+
- Peeks at next job in heap
|
|
119
|
+
- Calculates delay: `(nextTrigger - now) * 1000` (converts seconds to milliseconds)
|
|
120
|
+
- Sets setTimeout for next job execution
|
|
121
|
+
|
|
122
|
+
**`runDueJobs()`**
|
|
123
|
+
- Processes all jobs with `nextTrigger <= now`
|
|
124
|
+
- Executes job callbacks with error handling
|
|
125
|
+
- Reschedules each job after execution
|
|
126
|
+
- Calls `scheduleNextRun()` when done
|
|
127
|
+
|
|
128
|
+
**`setNextTrigger(job)`**
|
|
129
|
+
- Updates job's `nextTrigger` to `now + interval`
|
|
130
|
+
- Uses `getTimestamp()` which returns **seconds**, not milliseconds
|
|
131
|
+
|
|
132
|
+
**`log(text, key=null)`**
|
|
133
|
+
- Conditional logging based on `config.cron?.log`
|
|
134
|
+
- Formats messages as `Cron::${key} - ${text}:` or `Cron - ${text}:`
|
|
135
|
+
|
|
136
|
+
### MinHeap Class (`stonyx-cron/src/min-heap.js`)
|
|
137
|
+
|
|
138
|
+
**Properties:**
|
|
139
|
+
```javascript
|
|
140
|
+
items = []; // Array backing the heap
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Public Methods:**
|
|
144
|
+
|
|
145
|
+
**`push(job)`**
|
|
146
|
+
- Adds job to end of array
|
|
147
|
+
- Bubbles up to maintain heap property
|
|
148
|
+
|
|
149
|
+
**`pop()`**
|
|
150
|
+
- Removes and returns root (minimum `nextTrigger`)
|
|
151
|
+
- Replaces root with last item
|
|
152
|
+
- Bubbles down to restore heap property
|
|
153
|
+
|
|
154
|
+
**`peek()`**
|
|
155
|
+
- Returns root without removing it
|
|
156
|
+
- O(1) access to next job to run
|
|
157
|
+
|
|
158
|
+
**`remove(job)`**
|
|
159
|
+
- Finds job by reference equality (`indexOf`)
|
|
160
|
+
- Replaces with last item
|
|
161
|
+
- Bubbles both up and down to restore heap property
|
|
162
|
+
|
|
163
|
+
**`isEmpty()`**
|
|
164
|
+
- Returns `true` if `items.length === 0`
|
|
165
|
+
|
|
166
|
+
**Internal Methods:**
|
|
167
|
+
|
|
168
|
+
**`bubbleUp()`**
|
|
169
|
+
- Moves last item up tree until heap property is satisfied
|
|
170
|
+
- Compares `nextTrigger` values with parent
|
|
171
|
+
|
|
172
|
+
**`bubbleDown()`**
|
|
173
|
+
- Moves root down tree, swapping with smallest child
|
|
174
|
+
- Maintains min-heap property
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 5. Dependencies & Integration
|
|
179
|
+
|
|
180
|
+
### Stonyx Framework Integration
|
|
181
|
+
```javascript
|
|
182
|
+
import config from 'stonyx/config'; // Configuration system
|
|
183
|
+
import log from 'stonyx/log'; // Logging system
|
|
184
|
+
import { setupIntegrationTests } from "stonyx/test-helpers"; // Test utilities
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### External Dependencies
|
|
188
|
+
```javascript
|
|
189
|
+
import { getTimestamp } from "@stonyx/utils/date"; // Time utilities
|
|
190
|
+
import QUnit from 'qunit'; // Test framework
|
|
191
|
+
import sinon from 'sinon'; // Test spies/stubs/fake timers
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Critical Time Handling Detail
|
|
195
|
+
**`getTimestamp()` returns Unix timestamps in SECONDS, not milliseconds.**
|
|
196
|
+
|
|
197
|
+
This affects:
|
|
198
|
+
- Job `interval` values (specified in seconds)
|
|
199
|
+
- Job `nextTrigger` values (stored in seconds)
|
|
200
|
+
- Delay calculation in `scheduleNextRun()`: must multiply by 1000 for `setTimeout`
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
// CORRECT: Convert seconds to milliseconds for setTimeout
|
|
204
|
+
const delay = Math.max(0, nextJob.nextTrigger - getTimestamp()) * 1000;
|
|
205
|
+
this.timer = setTimeout(() => this.runDueJobs(), delay);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 6. Code Patterns & Conventions
|
|
211
|
+
|
|
212
|
+
### Module System
|
|
213
|
+
All modules use ES Module syntax with default exports:
|
|
214
|
+
```javascript
|
|
215
|
+
export default class Cron { ... }
|
|
216
|
+
export default class MinHeap { ... }
|
|
217
|
+
export default { log: true, logColor: '#888' }; // config
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Imports use full package paths:
|
|
221
|
+
```javascript
|
|
222
|
+
import Cron from '@stonyx/cron';
|
|
223
|
+
import MinHeap from '@stonyx/cron/min-heap';
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Logging Patterns
|
|
227
|
+
**Always check config before logging:**
|
|
228
|
+
```javascript
|
|
229
|
+
if (config.debug) this.log('job has been triggered', job.key);
|
|
230
|
+
if (config.cron?.log) log.cron(`${tag} - ${text}:`);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Use appropriate log methods:**
|
|
234
|
+
- `log.cron()` for informational cron messages
|
|
235
|
+
- `log.error()` for error conditions
|
|
236
|
+
|
|
237
|
+
### Error Handling
|
|
238
|
+
**Never let errors crash the scheduler:**
|
|
239
|
+
```javascript
|
|
240
|
+
try {
|
|
241
|
+
await job.callback();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
log.error(`Cron job "${job.key}" failed:`, err);
|
|
244
|
+
}
|
|
245
|
+
// Always reschedule job, even after error
|
|
246
|
+
this.setNextTrigger(job);
|
|
247
|
+
heap.push(job);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Time Handling
|
|
251
|
+
**Always use `getTimestamp()` for current time:**
|
|
252
|
+
```javascript
|
|
253
|
+
// CORRECT
|
|
254
|
+
const now = getTimestamp();
|
|
255
|
+
job.nextTrigger = getTimestamp() + parseInt(job.interval, 10);
|
|
256
|
+
|
|
257
|
+
// WRONG - don't use Date.now() or other time sources
|
|
258
|
+
const now = Date.now(); // WRONG: milliseconds instead of seconds
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## 7. Testing Guidelines
|
|
264
|
+
|
|
265
|
+
### Test Structure
|
|
266
|
+
Tests are located in `stonyx-cron/test/unit/` and use QUnit modules:
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
import QUnit from 'qunit';
|
|
270
|
+
import sinon from 'sinon';
|
|
271
|
+
import { setupIntegrationTests } from "stonyx/test-helpers";
|
|
272
|
+
|
|
273
|
+
const { module, test } = QUnit;
|
|
274
|
+
|
|
275
|
+
module('[Unit] Cron', function (hooks) {
|
|
276
|
+
setupIntegrationTests(hooks);
|
|
277
|
+
|
|
278
|
+
let cron, clock;
|
|
279
|
+
|
|
280
|
+
hooks.beforeEach(function () {
|
|
281
|
+
clock = sinon.useFakeTimers({ shouldAdvanceTime: false });
|
|
282
|
+
cron = new Cron();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
hooks.afterEach(function () {
|
|
286
|
+
sinon.restore();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('test description', async function (assert) {
|
|
290
|
+
// Test implementation
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Fake Timers Pattern (CRITICAL)
|
|
296
|
+
**Always use fake timers for time-based tests:**
|
|
297
|
+
|
|
298
|
+
```javascript
|
|
299
|
+
// Setup in beforeEach
|
|
300
|
+
clock = sinon.useFakeTimers({ shouldAdvanceTime: false });
|
|
301
|
+
|
|
302
|
+
// Advance time synchronously
|
|
303
|
+
clock.tick(5000); // Advance 5 seconds
|
|
304
|
+
|
|
305
|
+
// For async operations, use tickAsync
|
|
306
|
+
clock.tick(5000);
|
|
307
|
+
await clock.tickAsync(0); // Process async callbacks
|
|
308
|
+
|
|
309
|
+
// Cleanup in afterEach
|
|
310
|
+
sinon.restore();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Why `shouldAdvanceTime: false`?**
|
|
314
|
+
Prevents real time from interfering with fake time, ensuring deterministic tests.
|
|
315
|
+
|
|
316
|
+
### Spies & Stubs Patterns
|
|
317
|
+
```javascript
|
|
318
|
+
// Spy on function calls
|
|
319
|
+
const cb = sinon.spy();
|
|
320
|
+
cron.register('job1', cb, 5);
|
|
321
|
+
assert.ok(cb.calledOnce, 'Callback executed once');
|
|
322
|
+
|
|
323
|
+
// Stub methods
|
|
324
|
+
const stub = sinon.stub().rejects(new Error('boom'));
|
|
325
|
+
cron.register('jobErr', stub, 1);
|
|
326
|
+
|
|
327
|
+
// Spy on existing methods
|
|
328
|
+
const logSpy = sinon.spy(log, 'cron');
|
|
329
|
+
cron.log('test message');
|
|
330
|
+
assert.ok(logSpy.calledOnce, 'Log called');
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Test Coverage Expectations
|
|
334
|
+
Tests should cover:
|
|
335
|
+
- Job registration and execution
|
|
336
|
+
- Rescheduling behavior
|
|
337
|
+
- Unregistration
|
|
338
|
+
- Error handling
|
|
339
|
+
- Configuration-driven logging
|
|
340
|
+
- Edge cases (empty heap, multiple jobs, etc.)
|
|
341
|
+
|
|
342
|
+
### Running Tests
|
|
343
|
+
```bash
|
|
344
|
+
npm test # Runs: qunit --require ./stonyx-bootstrap.cjs
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## 8. Extension Points
|
|
350
|
+
|
|
351
|
+
This section provides specific guidance for common feature additions.
|
|
352
|
+
|
|
353
|
+
### Adding Job Metadata
|
|
354
|
+
|
|
355
|
+
**Where:** `stonyx-cron/src/main.js`
|
|
356
|
+
|
|
357
|
+
**Pattern:**
|
|
358
|
+
```javascript
|
|
359
|
+
// In register method, extend job object:
|
|
360
|
+
register(key, callback, interval, runOnInit=false, metadata={}) {
|
|
361
|
+
const job = {
|
|
362
|
+
callback,
|
|
363
|
+
interval,
|
|
364
|
+
key,
|
|
365
|
+
metadata // Add metadata field
|
|
366
|
+
};
|
|
367
|
+
this.jobs[key] = job;
|
|
368
|
+
// ... rest of method
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Access metadata in runDueJobs or other methods:
|
|
372
|
+
const { metadata } = job;
|
|
373
|
+
if (metadata.priority === 'high') {
|
|
374
|
+
// Handle high priority jobs differently
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Tests to add:** Verify metadata is stored and accessible in job object.
|
|
379
|
+
|
|
380
|
+
### Adding Priority Levels
|
|
381
|
+
|
|
382
|
+
**Where:**
|
|
383
|
+
- `stonyx-cron/src/main.js` (Cron class)
|
|
384
|
+
- `stonyx-cron/src/min-heap.js` (MinHeap comparison)
|
|
385
|
+
|
|
386
|
+
**Pattern:**
|
|
387
|
+
```javascript
|
|
388
|
+
// Option 1: Secondary sort on priority
|
|
389
|
+
// In min-heap.js bubbleUp/bubbleDown, change comparison:
|
|
390
|
+
if (this.items[idx].nextTrigger < this.items[parentIdx].nextTrigger ||
|
|
391
|
+
(this.items[idx].nextTrigger === this.items[parentIdx].nextTrigger &&
|
|
392
|
+
this.items[idx].priority > this.items[parentIdx].priority)) {
|
|
393
|
+
// Swap
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Option 2: Offset nextTrigger by priority
|
|
397
|
+
// In main.js setNextTrigger:
|
|
398
|
+
setNextTrigger(job) {
|
|
399
|
+
const priorityOffset = job.priority || 0;
|
|
400
|
+
job.nextTrigger = getTimestamp() + parseInt(job.interval, 10) - priorityOffset;
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Considerations:**
|
|
405
|
+
- Option 1 maintains exact timing but adds complexity to heap
|
|
406
|
+
- Option 2 is simpler but slightly alters execution timing
|
|
407
|
+
- Add `priority` field to job object schema
|
|
408
|
+
|
|
409
|
+
**Tests to add:** Jobs with higher priority run before lower priority when due at same time.
|
|
410
|
+
|
|
411
|
+
### Adding Job Statistics
|
|
412
|
+
|
|
413
|
+
**Where:** `stonyx-cron/src/main.js`
|
|
414
|
+
|
|
415
|
+
**Pattern:**
|
|
416
|
+
```javascript
|
|
417
|
+
// Extend job object with stats:
|
|
418
|
+
register(key, callback, interval, runOnInit=false) {
|
|
419
|
+
const job = {
|
|
420
|
+
callback,
|
|
421
|
+
interval,
|
|
422
|
+
key,
|
|
423
|
+
stats: {
|
|
424
|
+
runCount: 0,
|
|
425
|
+
lastRun: null,
|
|
426
|
+
lastError: null,
|
|
427
|
+
errorCount: 0
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
// ...
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Update stats in runDueJobs:
|
|
434
|
+
async runDueJobs() {
|
|
435
|
+
// ...
|
|
436
|
+
try {
|
|
437
|
+
await job.callback();
|
|
438
|
+
job.stats.runCount++;
|
|
439
|
+
job.stats.lastRun = getTimestamp();
|
|
440
|
+
} catch (err) {
|
|
441
|
+
job.stats.errorCount++;
|
|
442
|
+
job.stats.lastError = err.message;
|
|
443
|
+
log.error(`Cron job "${job.key}" failed:`, err);
|
|
444
|
+
}
|
|
445
|
+
// ...
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Add method to retrieve stats:
|
|
449
|
+
getJobStats(key) {
|
|
450
|
+
return this.jobs[key]?.stats;
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Tests to add:** Verify stats increment correctly on success/error.
|
|
455
|
+
|
|
456
|
+
### Adding Persistence
|
|
457
|
+
|
|
458
|
+
**Where:** `stonyx-cron/src/main.js`
|
|
459
|
+
|
|
460
|
+
**Pattern:**
|
|
461
|
+
```javascript
|
|
462
|
+
// Add save/load methods:
|
|
463
|
+
async saveJobs() {
|
|
464
|
+
const jobData = Object.values(this.jobs).map(job => ({
|
|
465
|
+
key: job.key,
|
|
466
|
+
interval: job.interval,
|
|
467
|
+
// Don't save callback - must be re-registered
|
|
468
|
+
}));
|
|
469
|
+
// Write to file/database
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async loadJobs(callbacks) {
|
|
473
|
+
const jobData = // Read from file/database
|
|
474
|
+
jobData.forEach(({ key, interval }) => {
|
|
475
|
+
if (callbacks[key]) {
|
|
476
|
+
this.register(key, callbacks[key], interval);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Considerations:**
|
|
483
|
+
- Cannot serialize callbacks - must be re-registered on load
|
|
484
|
+
- Save job metadata, intervals, and keys
|
|
485
|
+
- Consider saving `nextTrigger` to maintain schedule across restarts
|
|
486
|
+
|
|
487
|
+
**Tests to add:** Verify jobs can be saved and restored with same intervals.
|
|
488
|
+
|
|
489
|
+
### Adding One-Time Jobs
|
|
490
|
+
|
|
491
|
+
**Where:** `stonyx-cron/src/main.js`
|
|
492
|
+
|
|
493
|
+
**Pattern:**
|
|
494
|
+
```javascript
|
|
495
|
+
// Add oneTime flag to job schema:
|
|
496
|
+
register(key, callback, interval, runOnInit=false, oneTime=false) {
|
|
497
|
+
const job = { callback, interval, key, oneTime };
|
|
498
|
+
// ...
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Modify runDueJobs to unregister one-time jobs:
|
|
502
|
+
async runDueJobs() {
|
|
503
|
+
const now = getTimestamp();
|
|
504
|
+
const { heap } = this;
|
|
505
|
+
|
|
506
|
+
while (!heap.isEmpty() && heap.peek().nextTrigger <= now) {
|
|
507
|
+
const job = heap.pop();
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
await job.callback();
|
|
511
|
+
} catch (err) {
|
|
512
|
+
log.error(`Cron job "${job.key}" failed:`, err);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (job.oneTime) {
|
|
516
|
+
delete this.jobs[job.key]; // Don't reschedule
|
|
517
|
+
if (config.debug) this.log('one-time job completed', job.key);
|
|
518
|
+
} else {
|
|
519
|
+
this.setNextTrigger(job);
|
|
520
|
+
heap.push(job);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
this.scheduleNextRun();
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Tests to add:** One-time jobs execute once and are not rescheduled.
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## 9. Configuration Reference
|
|
533
|
+
|
|
534
|
+
### Module Configuration
|
|
535
|
+
**File:** `stonyx-cron/config/environment.js`
|
|
536
|
+
|
|
537
|
+
```javascript
|
|
538
|
+
const { CRON_LOG } = process;
|
|
539
|
+
|
|
540
|
+
export default {
|
|
541
|
+
log: CRON_LOG ?? true, // Enable/disable cron logging
|
|
542
|
+
logColor: '#888', // Color for cron logs
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**Environment Variable:**
|
|
547
|
+
- `CRON_LOG`: Set to `false` to disable cron logging
|
|
548
|
+
|
|
549
|
+
### Using Configuration in Code
|
|
550
|
+
```javascript
|
|
551
|
+
import config from 'stonyx/config';
|
|
552
|
+
|
|
553
|
+
// Access cron config
|
|
554
|
+
if (config.cron?.log) {
|
|
555
|
+
log.cron('message');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Access debug flag (from main Stonyx config)
|
|
559
|
+
if (config.debug) {
|
|
560
|
+
this.log('debug message');
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## 10. Package Exports
|
|
567
|
+
|
|
568
|
+
**File:** `stonyx-cron/package.json`
|
|
569
|
+
|
|
570
|
+
```json
|
|
571
|
+
{
|
|
572
|
+
"exports": {
|
|
573
|
+
".": "./src/main.js",
|
|
574
|
+
"./min-heap": "./src/min-heap.js"
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**Usage:**
|
|
580
|
+
```javascript
|
|
581
|
+
// Import Cron class (default export)
|
|
582
|
+
import Cron from '@stonyx/cron';
|
|
583
|
+
|
|
584
|
+
// Import MinHeap class (for advanced usage)
|
|
585
|
+
import MinHeap from '@stonyx/cron/min-heap';
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## 11. Development Workflow
|
|
591
|
+
|
|
592
|
+
### Local Development
|
|
593
|
+
```bash
|
|
594
|
+
# Install dependencies
|
|
595
|
+
npm install
|
|
596
|
+
|
|
597
|
+
# Run tests
|
|
598
|
+
npm test
|
|
599
|
+
|
|
600
|
+
# Run tests with specific Node version
|
|
601
|
+
nvm use
|
|
602
|
+
npm test
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Test-Driven Development
|
|
606
|
+
1. Write failing test in `stonyx-cron/test/unit/`
|
|
607
|
+
2. Implement feature in `stonyx-cron/src/`
|
|
608
|
+
3. Run `npm test` to verify
|
|
609
|
+
4. Refactor if needed
|
|
610
|
+
|
|
611
|
+
### CI/CD
|
|
612
|
+
- GitHub Actions workflows in `.github/workflows/`
|
|
613
|
+
- Runs tests on push/PR
|
|
614
|
+
- Publishes to npm on version tags
|
|
615
|
+
|
|
616
|
+
### Publishing
|
|
617
|
+
```bash
|
|
618
|
+
# Bump version
|
|
619
|
+
npm version patch|minor|major
|
|
620
|
+
|
|
621
|
+
# Publish to npm (if you have access)
|
|
622
|
+
npm publish
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## 12. Common Pitfalls & Gotchas
|
|
628
|
+
|
|
629
|
+
### Time Units Confusion
|
|
630
|
+
**Pitfall:** Mixing seconds and milliseconds
|
|
631
|
+
|
|
632
|
+
**Correct:**
|
|
633
|
+
```javascript
|
|
634
|
+
// getTimestamp() returns SECONDS
|
|
635
|
+
const now = getTimestamp();
|
|
636
|
+
job.interval = 5; // 5 seconds
|
|
637
|
+
job.nextTrigger = now + 5; // 5 seconds from now
|
|
638
|
+
|
|
639
|
+
// setTimeout expects MILLISECONDS
|
|
640
|
+
const delay = (job.nextTrigger - now) * 1000;
|
|
641
|
+
setTimeout(callback, delay);
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**Wrong:**
|
|
645
|
+
```javascript
|
|
646
|
+
// DON'T DO THIS
|
|
647
|
+
const delay = job.nextTrigger - getTimestamp(); // Missing * 1000
|
|
648
|
+
setTimeout(callback, delay); // Will run almost immediately!
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Singleton Behavior
|
|
652
|
+
**Pitfall:** Creating multiple Cron instances unexpectedly
|
|
653
|
+
|
|
654
|
+
**Behavior:**
|
|
655
|
+
```javascript
|
|
656
|
+
const cron1 = new Cron();
|
|
657
|
+
const cron2 = new Cron();
|
|
658
|
+
console.log(cron1 === cron2); // true - same instance!
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Implication:** Registering jobs on any instance affects the same scheduler.
|
|
662
|
+
|
|
663
|
+
### Job Callback Async Handling
|
|
664
|
+
**Pitfall:** Not awaiting async callbacks
|
|
665
|
+
|
|
666
|
+
**Correct:**
|
|
667
|
+
```javascript
|
|
668
|
+
cron.register('job', async () => {
|
|
669
|
+
await someAsyncOperation();
|
|
670
|
+
}, 10);
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
The scheduler awaits the callback, so errors are caught properly.
|
|
674
|
+
|
|
675
|
+
**Wrong:**
|
|
676
|
+
```javascript
|
|
677
|
+
cron.register('job', () => {
|
|
678
|
+
someAsyncOperation(); // Not awaited - errors won't be caught!
|
|
679
|
+
}, 10);
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Heap Reference Equality
|
|
683
|
+
**Pitfall:** Modifying job objects outside the scheduler
|
|
684
|
+
|
|
685
|
+
**Issue:**
|
|
686
|
+
```javascript
|
|
687
|
+
const job = cron.jobs['myJob'];
|
|
688
|
+
job.interval = 20; // This doesn't update the heap!
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Solution:** Always use `unregister` then `register` to update job properties.
|
|
692
|
+
|
|
693
|
+
### Test Isolation
|
|
694
|
+
**Pitfall:** Tests interfering with each other due to singleton
|
|
695
|
+
|
|
696
|
+
**Solution:**
|
|
697
|
+
```javascript
|
|
698
|
+
hooks.beforeEach(function () {
|
|
699
|
+
clock = sinon.useFakeTimers({ shouldAdvanceTime: false });
|
|
700
|
+
cron = new Cron(); // Gets singleton
|
|
701
|
+
|
|
702
|
+
// Clear previous test's jobs
|
|
703
|
+
Object.keys(cron.jobs).forEach(key => cron.unregister(key));
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
hooks.afterEach(function () {
|
|
707
|
+
sinon.restore(); // Restores real timers
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## 13. Future Enhancement Opportunities
|
|
714
|
+
|
|
715
|
+
Ideas aligned with current architecture:
|
|
716
|
+
|
|
717
|
+
1. **Job Prioritization** - Add priority levels for jobs due at same time
|
|
718
|
+
2. **Persistence Layer** - Save/restore jobs across restarts
|
|
719
|
+
3. **Job Statistics** - Track run counts, errors, execution time
|
|
720
|
+
4. **Job Dependencies** - Wait for other jobs before running
|
|
721
|
+
5. **Cron Expression Support** - Use cron syntax instead of intervals
|
|
722
|
+
6. **Job Timeout** - Cancel jobs that run too long
|
|
723
|
+
7. **Pause/Resume** - Temporarily stop/start the scheduler
|
|
724
|
+
8. **Job Groups** - Batch operations on related jobs
|
|
725
|
+
9. **Event Emitters** - Emit events on job lifecycle (start, complete, error)
|
|
726
|
+
10. **Rate Limiting** - Limit concurrent job execution
|
|
727
|
+
|
|
728
|
+
All enhancements should maintain:
|
|
729
|
+
- Zero-crash guarantee (catch all errors)
|
|
730
|
+
- O(log n) scheduling efficiency
|
|
731
|
+
- Singleton pattern
|
|
732
|
+
- Configuration-driven logging
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## 14. Related Resources
|
|
737
|
+
|
|
738
|
+
### Stonyx Framework
|
|
739
|
+
- Main repository: https://github.com/abofs/stonyx
|
|
740
|
+
- Configuration patterns: See `stonyx/config` documentation
|
|
741
|
+
- Logging patterns: See `stonyx/log` documentation
|
|
742
|
+
- Test helpers: See `stonyx/test-helpers` documentation
|
|
743
|
+
|
|
744
|
+
### Testing
|
|
745
|
+
- QUnit documentation: https://qunitjs.com/
|
|
746
|
+
- Sinon documentation: https://sinonjs.org/
|
|
747
|
+
- Fake timers: https://sinonjs.org/releases/latest/fake-timers/
|
|
748
|
+
|
|
749
|
+
### Data Structures
|
|
750
|
+
- Min-heap algorithm: https://en.wikipedia.org/wiki/Binary_heap
|
|
751
|
+
- Priority queue patterns: See MinHeap implementation
|
|
752
|
+
|
|
753
|
+
### Project Repository
|
|
754
|
+
- GitHub: https://github.com/abofs/stonyx-cron
|
|
755
|
+
- Issues: https://github.com/abofs/stonyx-cron/issues
|
|
756
|
+
- License: Apache 2.0
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## Document Maintenance
|
|
761
|
+
|
|
762
|
+
**Last Updated:** 2026-01-31
|
|
763
|
+
**Version:** 1.0.0
|
|
764
|
+
**Maintainer:** Project owner (update when structure changes significantly)
|
|
765
|
+
|
|
766
|
+
**When to Update:**
|
|
767
|
+
- Major architectural changes
|
|
768
|
+
- New core components added
|
|
769
|
+
- Extension patterns change
|
|
770
|
+
- New configuration options
|
|
771
|
+
- Breaking changes to API
|
package/.git/config
CHANGED
|
@@ -3,22 +3,16 @@
|
|
|
3
3
|
filemode = true
|
|
4
4
|
bare = false
|
|
5
5
|
logallrefupdates = true
|
|
6
|
-
ignorecase = true
|
|
7
|
-
precomposeunicode = true
|
|
8
6
|
[remote "origin"]
|
|
9
|
-
url =
|
|
7
|
+
url = https://github.com/abofs/stonyx-cron
|
|
10
8
|
fetch = +refs/heads/*:refs/remotes/origin/*
|
|
11
|
-
[
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
[branch "stone/cron-updates"]
|
|
16
|
-
remote = origin
|
|
17
|
-
merge = refs/heads/stone/cron-updates
|
|
18
|
-
vscode-merge-base = origin/stone/cron-updates
|
|
19
|
-
gk-last-accessed = 2026-01-26T20:25:14.543Z
|
|
20
|
-
gk-last-modified = 2026-01-26T20:25:14.543Z
|
|
9
|
+
[gc]
|
|
10
|
+
auto = 0
|
|
11
|
+
[http "https://github.com/"]
|
|
12
|
+
extraheader = AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2hzX3ZlU0VYM1FDZkhtbWl4bU1kRDlBRVRBTjBDUW5HQzBPVUxScg==
|
|
21
13
|
[branch "main"]
|
|
22
14
|
remote = origin
|
|
23
15
|
merge = refs/heads/main
|
|
24
|
-
|
|
16
|
+
[user]
|
|
17
|
+
name = github-actions[bot]
|
|
18
|
+
email = github-actions[bot]@users.noreply.github.com
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -2,35 +2,15 @@ name: CI
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- dev
|
|
7
|
-
- main
|
|
5
|
+
branches: [dev, main]
|
|
8
6
|
|
|
9
7
|
concurrency:
|
|
10
8
|
group: ci-${{ github.head_ref || github.ref }}
|
|
11
9
|
cancel-in-progress: true
|
|
12
10
|
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
13
14
|
jobs:
|
|
14
15
|
test:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- name: Checkout code
|
|
19
|
-
uses: actions/checkout@v3
|
|
20
|
-
|
|
21
|
-
- name: Setup pnpm
|
|
22
|
-
uses: pnpm/action-setup@v4
|
|
23
|
-
with:
|
|
24
|
-
version: 9
|
|
25
|
-
|
|
26
|
-
- name: Set up Node.js
|
|
27
|
-
uses: actions/setup-node@v3
|
|
28
|
-
with:
|
|
29
|
-
node-version: 22.18.0
|
|
30
|
-
cache: 'pnpm'
|
|
31
|
-
|
|
32
|
-
- name: Install dependencies
|
|
33
|
-
run: pnpm install --frozen-lockfile
|
|
34
|
-
|
|
35
|
-
- name: Run tests
|
|
36
|
-
run: pnpm test
|
|
16
|
+
uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version-type:
|
|
7
|
+
description: 'Version type'
|
|
8
|
+
required: true
|
|
9
|
+
type: choice
|
|
10
|
+
options:
|
|
11
|
+
- patch
|
|
12
|
+
- minor
|
|
13
|
+
- major
|
|
14
|
+
custom-version:
|
|
15
|
+
description: 'Custom version (optional, overrides version-type)'
|
|
16
|
+
required: false
|
|
17
|
+
type: string
|
|
18
|
+
pull_request:
|
|
19
|
+
types: [opened, synchronize, reopened]
|
|
20
|
+
branches: [main, dev]
|
|
21
|
+
push:
|
|
22
|
+
branches: [main]
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
contents: write
|
|
26
|
+
id-token: write
|
|
27
|
+
pull-requests: write
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
publish:
|
|
31
|
+
uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
|
|
32
|
+
with:
|
|
33
|
+
version-type: ${{ github.event.inputs.version-type }}
|
|
34
|
+
custom-version: ${{ github.event.inputs.custom-version }}
|
|
35
|
+
secrets: inherit
|
package/.gitignore
CHANGED
package/README.md
CHANGED
|
@@ -20,19 +20,18 @@ cron.register('exampleJob', async () => {
|
|
|
20
20
|
|
|
21
21
|
## How it works
|
|
22
22
|
|
|
23
|
-
`stonyx-cron` uses a min-heap internally to efficiently track the next job to run. Each job has a scheduled trigger time, and the heap ensures the job with the earliest trigger is always at the top.
|
|
23
|
+
`stonyx-cron` uses a min-heap internally to efficiently track the next job to run. Each job has a scheduled trigger time, and the heap ensures the job with the earliest trigger is always at the top.
|
|
24
24
|
|
|
25
25
|
When a job is executed, its next trigger time is updated, and it is re-inserted into the heap. This allows `Cron` to always know which job should run next without scanning all jobs, keeping scheduling efficient even with many jobs.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
### Public Methods
|
|
27
|
+
## Public Methods
|
|
29
28
|
|
|
30
29
|
| Method | Parameters | Description |
|
|
31
30
|
| :----------: | :----------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------- |
|
|
32
31
|
| `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. |
|
|
33
32
|
| `unregister` | `key: string` | Remove a previously registered job. |
|
|
34
33
|
|
|
35
|
-
> All other methods and classes (like `MinHeap`) are used internally
|
|
34
|
+
> All other methods and classes (like `MinHeap`) are used internally and are not intended for direct use.
|
|
36
35
|
|
|
37
36
|
## Configuration
|
|
38
37
|
|
package/logs/error.log
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
[
|
|
1
|
+
[1/1/1970, 12:00:01 AM] Cron job "jobErr" failed:
|
|
2
|
+
[1/1/1970, 12:00:01 AM] Cron job "jobErr" failed:
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"keywords": [
|
|
4
4
|
"stonyx-module"
|
|
5
5
|
],
|
|
6
|
-
"version": "0.2.0",
|
|
6
|
+
"version": "0.2.1-beta.0",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "src/main.js",
|
|
9
9
|
"type": "module",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
".": "./src/main.js",
|
|
15
15
|
"./min-heap": "./src/min-heap.js"
|
|
16
16
|
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"provenance": true
|
|
20
|
+
},
|
|
17
21
|
"repository": {
|
|
18
22
|
"type": "git",
|
|
19
23
|
"url": "git+https://github.com/abofs/stonyx-cron.git"
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(node --version:*)",
|
|
5
|
-
"Bash(source ~/.nvm/nvm.sh)",
|
|
6
|
-
"Bash(nvm use)",
|
|
7
|
-
"Bash(pnpm install:*)",
|
|
8
|
-
"Bash(pnpm store:*)",
|
|
9
|
-
"Bash(npm view:*)",
|
|
10
|
-
"Bash(npm publish:*)",
|
|
11
|
-
"Bash(npm version:*)",
|
|
12
|
-
"Bash(curl:*)"
|
|
13
|
-
]
|
|
14
|
-
}
|
|
15
|
-
}
|