@stonyx/cron 0.2.1-beta.7 → 0.2.1-beta.71

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,291 +0,0 @@
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
@@ -1,53 +0,0 @@
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.
@@ -1,139 +0,0 @@
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
@@ -1,85 +0,0 @@
1
- # Testing Guidelines
2
-
3
- ## Testing Guidelines
4
-
5
- ### Test Structure
6
- Tests are located in `stonyx-cron/test/unit/` and use QUnit modules:
7
-
8
- ```javascript
9
- import QUnit from 'qunit';
10
- import sinon from 'sinon';
11
- import { setupIntegrationTests } from "stonyx/test-helpers";
12
-
13
- const { module, test } = QUnit;
14
-
15
- module('[Unit] Cron', function (hooks) {
16
- setupIntegrationTests(hooks);
17
-
18
- let cron, clock;
19
-
20
- hooks.beforeEach(function () {
21
- clock = sinon.useFakeTimers({ shouldAdvanceTime: false });
22
- cron = new Cron();
23
- });
24
-
25
- hooks.afterEach(function () {
26
- sinon.restore();
27
- });
28
-
29
- test('test description', async function (assert) {
30
- // Test implementation
31
- });
32
- });
33
- ```
34
-
35
- ### Fake Timers Pattern (CRITICAL)
36
- **Always use fake timers for time-based tests:**
37
-
38
- ```javascript
39
- // Setup in beforeEach
40
- clock = sinon.useFakeTimers({ shouldAdvanceTime: false });
41
-
42
- // Advance time synchronously
43
- clock.tick(5000); // Advance 5 seconds
44
-
45
- // For async operations, use tickAsync
46
- clock.tick(5000);
47
- await clock.tickAsync(0); // Process async callbacks
48
-
49
- // Cleanup in afterEach
50
- sinon.restore();
51
- ```
52
-
53
- **Why `shouldAdvanceTime: false`?**
54
- Prevents real time from interfering with fake time, ensuring deterministic tests.
55
-
56
- ### Spies & Stubs Patterns
57
- ```javascript
58
- // Spy on function calls
59
- const cb = sinon.spy();
60
- cron.register('job1', cb, 5);
61
- assert.ok(cb.calledOnce, 'Callback executed once');
62
-
63
- // Stub methods
64
- const stub = sinon.stub().rejects(new Error('boom'));
65
- cron.register('jobErr', stub, 1);
66
-
67
- // Spy on existing methods
68
- const logSpy = sinon.spy(log, 'cron');
69
- cron.log('test message');
70
- assert.ok(logSpy.calledOnce, 'Log called');
71
- ```
72
-
73
- ### Test Coverage Expectations
74
- Tests should cover:
75
- - Job registration and execution
76
- - Rescheduling behavior
77
- - Unregistration
78
- - Error handling
79
- - Configuration-driven logging
80
- - Edge cases (empty heap, multiple jobs, etc.)
81
-
82
- ### Running Tests
83
- ```bash
84
- pnpm test # Runs: stonyx test
85
- ```
package/.git/config DELETED
@@ -1,18 +0,0 @@
1
- [core]
2
- repositoryformatversion = 0
3
- filemode = true
4
- bare = false
5
- logallrefupdates = true
6
- [remote "origin"]
7
- url = https://github.com/abofs/stonyx-cron
8
- fetch = +refs/heads/*:refs/remotes/origin/*
9
- [gc]
10
- auto = 0
11
- [http "https://github.com/"]
12
- extraheader = AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2hwX2hBdU5WbnJNcml2bktkZTlmaU80WW9lZk9QenZtdTFiUWtQNA==
13
- [branch "main"]
14
- remote = origin
15
- merge = refs/heads/main
16
- [user]
17
- name = github-actions[bot]
18
- email = github-actions[bot]@users.noreply.github.com
@@ -1,16 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- branches: [dev, main]
6
-
7
- concurrency:
8
- group: ci-${{ github.head_ref || github.ref }}
9
- cancel-in-progress: true
10
-
11
- permissions:
12
- contents: read
13
-
14
- jobs:
15
- test:
16
- uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
@@ -1,51 +0,0 @@
1
- name: Publish to NPM
2
-
3
- on:
4
- repository_dispatch:
5
- types: [cascade-publish]
6
- workflow_dispatch:
7
- inputs:
8
- version-type:
9
- description: 'Version type'
10
- required: true
11
- type: choice
12
- options:
13
- - patch
14
- - minor
15
- - major
16
- custom-version:
17
- description: 'Custom version (optional, overrides version-type)'
18
- required: false
19
- type: string
20
- pull_request:
21
- types: [opened, synchronize, reopened]
22
- branches: [main]
23
- push:
24
- branches: [main]
25
-
26
- concurrency:
27
- group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
28
- cancel-in-progress: false
29
-
30
- permissions:
31
- contents: write
32
- id-token: write
33
- pull-requests: write
34
-
35
- jobs:
36
- publish:
37
- if: "!contains(github.event.head_commit.message, '[skip ci]')"
38
- uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
39
- with:
40
- version-type: ${{ github.event.inputs.version-type }}
41
- custom-version: ${{ github.event.inputs.custom-version }}
42
- cascade-source: ${{ github.event.client_payload.source_package || '' }}
43
- secrets: inherit
44
-
45
- cascade:
46
- needs: publish
47
- uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
48
- with:
49
- package-name: ${{ needs.publish.outputs.package-name }}
50
- published-version: ${{ needs.publish.outputs.published-version }}
51
- secrets: inherit
package/.gitignore DELETED
@@ -1,16 +0,0 @@
1
- # dependencies
2
- /node_modules/
3
-
4
- # config
5
- config/environment.js
6
-
7
- # logs
8
- *logs/*
9
-
10
- # os / IDE
11
- .vscode/*
12
- *.vsix%
13
- *DS_Store*
14
-
15
- # npm auth (never commit tokens)
16
- .npmrc
package/.npmignore DELETED
@@ -1,5 +0,0 @@
1
- # tests
2
- test/
3
-
4
- # config
5
- .nvmrc
package/logs/error.log DELETED
@@ -1,2 +0,0 @@
1
- [1/1/1970, 12:00:01 AM] Cron job "jobErr" failed:
2
- [1/1/1970, 12:00:01 AM] Cron job "jobErr" failed: