@stonyx/cron 0.2.1-beta.7 → 0.2.1-beta.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/cron-parser.d.ts +30 -0
- package/dist/cron-parser.js +200 -0
- package/dist/job.d.ts +72 -0
- package/dist/job.js +172 -0
- package/dist/locked.d.ts +13 -0
- package/dist/locked.js +27 -0
- package/dist/main.d.ts +21 -0
- package/dist/main.js +107 -0
- package/dist/min-heap.d.ts +13 -0
- package/dist/min-heap.js +67 -0
- package/dist/normalize.d.ts +49 -0
- package/dist/normalize.js +148 -0
- package/dist/run-log.d.ts +44 -0
- package/dist/run-log.js +60 -0
- package/dist/schedule.d.ts +23 -0
- package/dist/schedule.js +65 -0
- package/dist/service.d.ts +85 -0
- package/dist/service.js +271 -0
- package/package.json +53 -9
- package/.claude/architecture.md +0 -215
- package/.claude/extension-guide.md +0 -291
- package/.claude/improvements.md +0 -53
- package/.claude/project-structure.md +0 -139
- package/.claude/testing.md +0 -85
- package/.git/config +0 -18
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/.gitignore +0 -16
- package/.npmignore +0 -5
- package/logs/error.log +0 -2
- package/pnpm-lock.yaml +0 -370
- package/src/main.js +0 -112
- package/src/min-heap.js +0 -73
package/dist/service.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronService - the main API for advanced job scheduling.
|
|
3
|
+
*
|
|
4
|
+
* Manages jobs in memory with a min-heap for efficient next-job lookup.
|
|
5
|
+
* All state mutations are serialized via async locking.
|
|
6
|
+
*/
|
|
7
|
+
import config from 'stonyx/config';
|
|
8
|
+
import log from 'stonyx/log';
|
|
9
|
+
import MinHeap from './min-heap.js';
|
|
10
|
+
import { createJob, updateJob, markRunning, applyResult, isDue } from './job.js';
|
|
11
|
+
import { locked } from './locked.js';
|
|
12
|
+
import { normalizeJobInput, recoverFlatParams } from './normalize.js';
|
|
13
|
+
import RunLog from './run-log.js';
|
|
14
|
+
const MAX_TIMER_DELAY_MS = 60_000;
|
|
15
|
+
export default class CronService {
|
|
16
|
+
jobs;
|
|
17
|
+
heap;
|
|
18
|
+
timer;
|
|
19
|
+
running;
|
|
20
|
+
runLog;
|
|
21
|
+
started;
|
|
22
|
+
// Pluggable callbacks for consumers
|
|
23
|
+
onJobDue;
|
|
24
|
+
constructor() {
|
|
25
|
+
this.jobs = new Map();
|
|
26
|
+
this.heap = new MinHeap();
|
|
27
|
+
this.timer = null;
|
|
28
|
+
this.running = false;
|
|
29
|
+
this.runLog = new RunLog();
|
|
30
|
+
this.started = false;
|
|
31
|
+
this.onJobDue = null;
|
|
32
|
+
}
|
|
33
|
+
// -- Lifecycle -------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Start the service. Loads jobs from store (if any), arms timer.
|
|
36
|
+
*/
|
|
37
|
+
async start(initialJobs) {
|
|
38
|
+
if (this.started)
|
|
39
|
+
return;
|
|
40
|
+
this.started = true;
|
|
41
|
+
if (initialJobs) {
|
|
42
|
+
for (const job of initialJobs) {
|
|
43
|
+
this.jobs.set(job.id, job);
|
|
44
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
45
|
+
this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.armTimer();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Stop the service. Clears timer.
|
|
53
|
+
*/
|
|
54
|
+
stop() {
|
|
55
|
+
this.started = false;
|
|
56
|
+
if (this.timer)
|
|
57
|
+
clearTimeout(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
// -- CRUD ------------------------------------------------------------
|
|
61
|
+
/**
|
|
62
|
+
* Get service status.
|
|
63
|
+
*/
|
|
64
|
+
status() {
|
|
65
|
+
const peek = this.heap.peek();
|
|
66
|
+
return {
|
|
67
|
+
started: this.started,
|
|
68
|
+
jobCount: this.jobs.size,
|
|
69
|
+
nextWakeAtMs: peek ? peek.nextTrigger : undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* List jobs, optionally including disabled ones.
|
|
74
|
+
*/
|
|
75
|
+
list(opts) {
|
|
76
|
+
const includeDisabled = opts?.includeDisabled ?? false;
|
|
77
|
+
const jobs = [...this.jobs.values()];
|
|
78
|
+
const filtered = includeDisabled ? jobs : jobs.filter(j => j.enabled);
|
|
79
|
+
return filtered.sort((a, b) => (a.state.nextRunAtMs ?? Infinity) - (b.state.nextRunAtMs ?? Infinity));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get a single job by ID.
|
|
83
|
+
*/
|
|
84
|
+
get(id) {
|
|
85
|
+
return this.jobs.get(id) || null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Add a new job. Input is normalized for AI compatibility.
|
|
89
|
+
*/
|
|
90
|
+
async add(rawInput) {
|
|
91
|
+
return locked(() => {
|
|
92
|
+
const input = normalizeJobInput(recoverFlatParams(rawInput));
|
|
93
|
+
const job = createJob(input);
|
|
94
|
+
this.jobs.set(job.id, job);
|
|
95
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
96
|
+
this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
|
|
97
|
+
this.armTimer();
|
|
98
|
+
}
|
|
99
|
+
return job;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Update an existing job.
|
|
104
|
+
*/
|
|
105
|
+
async update(id, patch) {
|
|
106
|
+
return locked(() => {
|
|
107
|
+
const job = this.jobs.get(id);
|
|
108
|
+
if (!job)
|
|
109
|
+
throw new Error(`Job not found: ${id}`);
|
|
110
|
+
const oldNextRun = job.state.nextRunAtMs;
|
|
111
|
+
updateJob(job, patch);
|
|
112
|
+
// Update heap entry
|
|
113
|
+
this.removeFromHeap(id);
|
|
114
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
115
|
+
this.heap.push({ key: id, nextTrigger: job.state.nextRunAtMs });
|
|
116
|
+
}
|
|
117
|
+
if (job.state.nextRunAtMs !== oldNextRun) {
|
|
118
|
+
this.armTimer();
|
|
119
|
+
}
|
|
120
|
+
return job;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Remove a job.
|
|
125
|
+
*/
|
|
126
|
+
async remove(id) {
|
|
127
|
+
return locked(() => {
|
|
128
|
+
const job = this.jobs.get(id);
|
|
129
|
+
if (!job)
|
|
130
|
+
throw new Error(`Job not found: ${id}`);
|
|
131
|
+
this.jobs.delete(id);
|
|
132
|
+
this.removeFromHeap(id);
|
|
133
|
+
this.runLog.removeJob(id);
|
|
134
|
+
this.armTimer();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Manually trigger a job.
|
|
139
|
+
*/
|
|
140
|
+
async run(id, mode = 'force') {
|
|
141
|
+
const job = this.jobs.get(id);
|
|
142
|
+
if (!job)
|
|
143
|
+
throw new Error(`Job not found: ${id}`);
|
|
144
|
+
if (mode === 'due' && !isDue(job, Date.now())) {
|
|
145
|
+
return { status: 'skipped', reason: 'not due' };
|
|
146
|
+
}
|
|
147
|
+
return this.executeJob(job);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get run history for a job.
|
|
151
|
+
*/
|
|
152
|
+
runs(id, limit) {
|
|
153
|
+
return this.runLog.get(id, limit);
|
|
154
|
+
}
|
|
155
|
+
// -- Timer Engine ----------------------------------------------------
|
|
156
|
+
armTimer() {
|
|
157
|
+
if (this.timer)
|
|
158
|
+
clearTimeout(this.timer);
|
|
159
|
+
if (!this.started)
|
|
160
|
+
return;
|
|
161
|
+
const peek = this.heap.peek();
|
|
162
|
+
if (!peek)
|
|
163
|
+
return;
|
|
164
|
+
const delay = Math.min(Math.max(peek.nextTrigger - Date.now(), 0), MAX_TIMER_DELAY_MS);
|
|
165
|
+
this.timer = setTimeout(() => this.onTimer(), delay);
|
|
166
|
+
}
|
|
167
|
+
async onTimer() {
|
|
168
|
+
if (this.running) {
|
|
169
|
+
// Already processing - re-arm at max delay to prevent scheduler death
|
|
170
|
+
this.timer = setTimeout(() => this.onTimer(), MAX_TIMER_DELAY_MS);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.running = true;
|
|
174
|
+
try {
|
|
175
|
+
await locked(async () => {
|
|
176
|
+
const nowMs = Date.now();
|
|
177
|
+
const dueJobs = this.findDueJobs(nowMs);
|
|
178
|
+
for (const job of dueJobs) {
|
|
179
|
+
markRunning(job);
|
|
180
|
+
}
|
|
181
|
+
for (const job of dueJobs) {
|
|
182
|
+
await this.executeJob(job);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
this.running = false;
|
|
188
|
+
this.armTimer();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
findDueJobs(nowMs) {
|
|
192
|
+
const due = [];
|
|
193
|
+
while (!this.heap.isEmpty()) {
|
|
194
|
+
const peek = this.heap.peek();
|
|
195
|
+
if (!peek || peek.nextTrigger > nowMs)
|
|
196
|
+
break;
|
|
197
|
+
this.heap.pop();
|
|
198
|
+
const job = this.jobs.get(peek.key);
|
|
199
|
+
if (job && isDue(job, nowMs)) {
|
|
200
|
+
due.push(job);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return due;
|
|
204
|
+
}
|
|
205
|
+
async executeJob(job) {
|
|
206
|
+
const startMs = Date.now();
|
|
207
|
+
let status = 'ok';
|
|
208
|
+
let error;
|
|
209
|
+
let summary;
|
|
210
|
+
try {
|
|
211
|
+
if (this.onJobDue) {
|
|
212
|
+
const result = await this.onJobDue(job);
|
|
213
|
+
if (result) {
|
|
214
|
+
status = result.status || 'ok';
|
|
215
|
+
error = result.error;
|
|
216
|
+
summary = result.summary;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
status = 'error';
|
|
222
|
+
error = err instanceof Error ? err.message : String(err);
|
|
223
|
+
this.log(`Job "${job.name}" (${job.id}) failed: ${error}`);
|
|
224
|
+
}
|
|
225
|
+
const durationMs = Date.now() - startMs;
|
|
226
|
+
const validStatus = (status === 'ok' || status === 'error' || status === 'skipped') ? status : 'error';
|
|
227
|
+
applyResult(job, validStatus, error, durationMs);
|
|
228
|
+
// Log the run
|
|
229
|
+
this.runLog.record({
|
|
230
|
+
jobId: job.id,
|
|
231
|
+
status,
|
|
232
|
+
error,
|
|
233
|
+
summary,
|
|
234
|
+
runAtMs: startMs,
|
|
235
|
+
durationMs,
|
|
236
|
+
nextRunAtMs: job.state.nextRunAtMs,
|
|
237
|
+
});
|
|
238
|
+
// Handle one-shot auto-delete
|
|
239
|
+
if (job.deleteAfterRun && status === 'ok' && !job.enabled) {
|
|
240
|
+
this.jobs.delete(job.id);
|
|
241
|
+
this.runLog.removeJob(job.id);
|
|
242
|
+
return { status, summary, deleted: true };
|
|
243
|
+
}
|
|
244
|
+
// Re-insert into heap if still active
|
|
245
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
246
|
+
this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
|
|
247
|
+
}
|
|
248
|
+
return { status, error, summary, durationMs };
|
|
249
|
+
}
|
|
250
|
+
// -- Helpers ---------------------------------------------------------
|
|
251
|
+
removeFromHeap(id) {
|
|
252
|
+
// MinHeap doesn't support remove-by-key efficiently,
|
|
253
|
+
// so we rebuild. Fine for typical job counts (< 1000).
|
|
254
|
+
const remaining = [];
|
|
255
|
+
while (!this.heap.isEmpty()) {
|
|
256
|
+
const item = this.heap.pop();
|
|
257
|
+
if (!item)
|
|
258
|
+
break;
|
|
259
|
+
if (item.key !== id)
|
|
260
|
+
remaining.push(item);
|
|
261
|
+
}
|
|
262
|
+
for (const item of remaining) {
|
|
263
|
+
this.heap.push(item);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
log(message) {
|
|
267
|
+
if (!config.cron?.log)
|
|
268
|
+
return;
|
|
269
|
+
log.cron(`Cron — ${message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
package/package.json
CHANGED
|
@@ -3,16 +3,53 @@
|
|
|
3
3
|
"keywords": [
|
|
4
4
|
"stonyx-module"
|
|
5
5
|
],
|
|
6
|
-
"version": "0.2.1-beta.
|
|
6
|
+
"version": "0.2.1-beta.70",
|
|
7
7
|
"description": "Cron/job scheduler for Stonyx framework",
|
|
8
|
-
"main": "
|
|
8
|
+
"main": "dist/main.js",
|
|
9
|
+
"types": "dist/main.d.ts",
|
|
9
10
|
"type": "module",
|
|
10
11
|
"files": [
|
|
11
|
-
"
|
|
12
|
+
"dist",
|
|
13
|
+
"config",
|
|
14
|
+
"README.md"
|
|
12
15
|
],
|
|
13
16
|
"exports": {
|
|
14
|
-
".":
|
|
15
|
-
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/main.d.ts",
|
|
19
|
+
"default": "./dist/main.js"
|
|
20
|
+
},
|
|
21
|
+
"./service": {
|
|
22
|
+
"types": "./dist/service.d.ts",
|
|
23
|
+
"default": "./dist/service.js"
|
|
24
|
+
},
|
|
25
|
+
"./cron-parser": {
|
|
26
|
+
"types": "./dist/cron-parser.d.ts",
|
|
27
|
+
"default": "./dist/cron-parser.js"
|
|
28
|
+
},
|
|
29
|
+
"./schedule": {
|
|
30
|
+
"types": "./dist/schedule.d.ts",
|
|
31
|
+
"default": "./dist/schedule.js"
|
|
32
|
+
},
|
|
33
|
+
"./job": {
|
|
34
|
+
"types": "./dist/job.d.ts",
|
|
35
|
+
"default": "./dist/job.js"
|
|
36
|
+
},
|
|
37
|
+
"./normalize": {
|
|
38
|
+
"types": "./dist/normalize.d.ts",
|
|
39
|
+
"default": "./dist/normalize.js"
|
|
40
|
+
},
|
|
41
|
+
"./locked": {
|
|
42
|
+
"types": "./dist/locked.d.ts",
|
|
43
|
+
"default": "./dist/locked.js"
|
|
44
|
+
},
|
|
45
|
+
"./run-log": {
|
|
46
|
+
"types": "./dist/run-log.d.ts",
|
|
47
|
+
"default": "./dist/run-log.js"
|
|
48
|
+
},
|
|
49
|
+
"./min-heap": {
|
|
50
|
+
"types": "./dist/min-heap.d.ts",
|
|
51
|
+
"default": "./dist/min-heap.js"
|
|
52
|
+
}
|
|
16
53
|
},
|
|
17
54
|
"publishConfig": {
|
|
18
55
|
"access": "public",
|
|
@@ -32,14 +69,21 @@
|
|
|
32
69
|
},
|
|
33
70
|
"homepage": "https://github.com/abofs/stonyx-cron#readme",
|
|
34
71
|
"devDependencies": {
|
|
35
|
-
"@stonyx/utils": "0.2.3-beta.
|
|
72
|
+
"@stonyx/utils": "0.2.3-beta.25",
|
|
73
|
+
"@types/node": "^25.5.2",
|
|
74
|
+
"@types/qunit": "^2.19.13",
|
|
75
|
+
"@types/sinon": "^21.0.1",
|
|
36
76
|
"qunit": "^2.24.1",
|
|
37
|
-
"sinon": "^21.0.0"
|
|
77
|
+
"sinon": "^21.0.0",
|
|
78
|
+
"tsx": "^4.21.0",
|
|
79
|
+
"typescript": "^5.8.3"
|
|
38
80
|
},
|
|
39
81
|
"dependencies": {
|
|
40
|
-
"stonyx": "0.2.3-beta.
|
|
82
|
+
"stonyx": "0.2.3-beta.68"
|
|
41
83
|
},
|
|
42
84
|
"scripts": {
|
|
43
|
-
"
|
|
85
|
+
"build": "tsc",
|
|
86
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
87
|
+
"test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
|
|
44
88
|
}
|
|
45
89
|
}
|
package/.claude/architecture.md
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
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
|
-
```
|