@stonyx/cron 0.2.1-beta.0 → 0.2.1-beta.2

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.
@@ -1,5 +1,13 @@
1
1
  # stonyx-cron Project Structure
2
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
+
3
11
  ## 1. Project Overview
4
12
 
5
13
  **stonyx-cron** is a lightweight async job scheduler for the Stonyx framework that uses a min-heap priority queue for efficient job scheduling.
@@ -56,514 +64,35 @@ Logging follows Stonyx patterns:
56
64
  ```
57
65
  stonyx-cron/
58
66
  ├── .claude/
59
- └── project-structure.md - This document
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
60
72
  ├── .github/
61
- │ └── workflows/ - CI/CD configuration
73
+ │ └── workflows/
74
+ │ ├── ci.yml - CI pipeline (PR checks)
75
+ │ └── publish.yml - NPM publish workflow
62
76
  ├── config/
63
- │ └── environment.js - Cron module configuration
77
+ │ └── environment.js - Cron module configuration
64
78
  ├── src/
65
- │ ├── main.js - Cron class (singleton scheduler)
66
- │ └── min-heap.js - MinHeap priority queue implementation
79
+ │ ├── main.js - Cron class (singleton scheduler)
80
+ │ └── min-heap.js - MinHeap priority queue implementation
67
81
  ├── test/
68
82
  │ └── 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
- }
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
562
91
  ```
563
92
 
564
93
  ---
565
94
 
566
- ## 10. Package Exports
95
+ ## 4. Package Exports
567
96
 
568
97
  **File:** `stonyx-cron/package.json`
569
98
 
@@ -587,153 +116,7 @@ import MinHeap from '@stonyx/cron/min-heap';
587
116
 
588
117
  ---
589
118
 
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
119
+ ## 5. Related Resources
737
120
 
738
121
  ### Stonyx Framework
739
122
  - Main repository: https://github.com/abofs/stonyx
@@ -754,18 +137,3 @@ All enhancements should maintain:
754
137
  - GitHub: https://github.com/abofs/stonyx-cron
755
138
  - Issues: https://github.com/abofs/stonyx-cron/issues
756
139
  - 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