jm2 0.1.9 → 0.1.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jm2",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "Job Manager 2 - A simple yet powerful job scheduler combining cron and at functionality",
5
5
  "type": "module",
6
6
  "main": "src/cli/index.js",
@@ -31,6 +31,10 @@ Common examples of JM2 add:
31
31
  jm2 add "weekly-backup.sh" --cron "0 0 * * 0"
32
32
  jm2 add "monthly-task.sh" --cron "0 0 1 * *"
33
33
 
34
+ # Advanced cron patterns (multiple ranges, steps)
35
+ jm2 add "check.sh" --cron "0 */5 9-18 * * *" # Every 5 min, 9AM-6PM
36
+ jm2 add "offpeak.sh" --cron "*/30 0-8,18-23 * * *" # Every 30 min, off-peak hours
37
+
34
38
  # Add a job with a name
35
39
  jm2 add "backup.sh" --name "daily-backup" --cron "0 2 * * *"
36
40
 
@@ -62,12 +62,25 @@ function shouldLog(level) {
62
62
  }
63
63
 
64
64
  /**
65
- * Format a timestamp for logging
65
+ * Format a timestamp for logging in local timezone
66
66
  * @param {Date} date - Date to format
67
67
  * @returns {string} Formatted timestamp
68
68
  */
69
69
  function formatTimestamp(date = new Date()) {
70
- return date.toISOString();
70
+ const year = date.getFullYear();
71
+ const month = String(date.getMonth() + 1).padStart(2, '0');
72
+ const day = String(date.getDate()).padStart(2, '0');
73
+ const hours = String(date.getHours()).padStart(2, '0');
74
+ const minutes = String(date.getMinutes()).padStart(2, '0');
75
+ const seconds = String(date.getSeconds()).padStart(2, '0');
76
+ const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
77
+
78
+ const tzOffset = -date.getTimezoneOffset();
79
+ const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, '0');
80
+ const tzMinutes = String(Math.abs(tzOffset) % 60).padStart(2, '0');
81
+ const tzSign = tzOffset >= 0 ? '+' : '-';
82
+
83
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}${tzSign}${tzHours}:${tzMinutes}`;
71
84
  }
72
85
 
73
86
  /**
@@ -178,8 +178,6 @@ class DarwinService extends PlatformService {
178
178
  </array>
179
179
  <key>RunAtLoad</key>
180
180
  <true/>
181
- <key>KeepAlive</key>
182
- <true/>
183
181
  <key>StandardOutPath</key>
184
182
  <string>${stdoutPath}</string>
185
183
  <key>StandardErrorPath</key>
@@ -344,8 +342,6 @@ Type=forking
344
342
  ExecStart=${nodePath} ${jm2Path} start
345
343
  ExecStop=${nodePath} ${jm2Path} stop
346
344
  ExecReload=${nodePath} ${jm2Path} restart
347
- Restart=always
348
- RestartSec=10
349
345
  Environment="JM2_DATA_DIR=${dataDir}"
350
346
  StandardOutput=append:${join(logDir, 'service-out.log')}
351
347
  StandardError=append:${join(logDir, 'service-err.log')}
@@ -31,6 +31,7 @@ export class Scheduler {
31
31
  }
32
32
 
33
33
  this.running = true;
34
+ this.lastTickTime = Date.now();
34
35
  this.logger.info('Scheduler starting...');
35
36
 
36
37
  // Load jobs from storage
@@ -132,7 +133,7 @@ export class Scheduler {
132
133
  // Calculate next run time for active jobs
133
134
  let nextRun = null;
134
135
  if (job.status === JobStatus.ACTIVE) {
135
- nextRun = this.calculateNextRun(job);
136
+ nextRun = this.calculateNextRun(job, new Date());
136
137
  }
137
138
 
138
139
  this.jobs.set(job.id, {
@@ -147,15 +148,16 @@ export class Scheduler {
147
148
  /**
148
149
  * Calculate the next run time for a job
149
150
  * @param {object} job - Job object
151
+ * @param {Date} fromDate - Date to calculate from
150
152
  * @returns {Date|null} Next run time or null
151
153
  */
152
- calculateNextRun(job) {
154
+ calculateNextRun(job, fromDate) {
153
155
  if (job.status !== JobStatus.ACTIVE) {
154
156
  return null;
155
157
  }
156
158
 
157
159
  if (job.type === JobType.CRON && job.cron) {
158
- return getNextRunTime(job.cron);
160
+ return getNextRunTime(job.cron, fromDate);
159
161
  }
160
162
 
161
163
  if (job.type === JobType.ONCE && job.runAt) {
@@ -167,6 +169,62 @@ export class Scheduler {
167
169
  return null;
168
170
  }
169
171
 
172
+ /**
173
+ * Calculate the next run time for a cron job, accounting for missed runs
174
+ * This ensures that after sleep/wake, we find the very next occurrence
175
+ * @param {object} job - Job object
176
+ * @param {Date} originalRunTime - The time the job was originally scheduled to run
177
+ * @returns {Date|null} Next run time or null
178
+ */
179
+ calculateNextRunAfterExecution(job, originalRunTime) {
180
+ if (job.status !== JobStatus.ACTIVE || job.type !== JobType.CRON || !job.cron) {
181
+ return this.calculateNextRun(job, new Date());
182
+ }
183
+
184
+ const now = new Date();
185
+ let nextRun = this.calculateNextRun(job, originalRunTime);
186
+
187
+ // Keep calculating until we find a time in the future
188
+ // This handles the case where the system woke from sleep and we missed multiple runs
189
+ while (nextRun && nextRun <= now) {
190
+ nextRun = this.calculateNextRun(job, nextRun);
191
+ }
192
+
193
+ return nextRun;
194
+ }
195
+
196
+ /**
197
+ * Recalculate next run times for periodic jobs that have drifted into the past
198
+ * This handles system sleep/wake scenarios where nextRun becomes stale
199
+ * Only recalculates jobs that are significantly overdue (missed multiple runs)
200
+ * @param {Date} now - Current time
201
+ */
202
+ recalculateStalePeriodicJobs(now) {
203
+ for (const [id, job] of this.jobs) {
204
+ if (
205
+ job.status === JobStatus.ACTIVE &&
206
+ job.type === JobType.CRON &&
207
+ job.cron &&
208
+ job.nextRun
209
+ ) {
210
+ const timeSinceNextRun = now.getTime() - job.nextRun.getTime();
211
+ const isSignificantlyOverdue = timeSinceNextRun > this.checkIntervalMs * 2;
212
+
213
+ if (isSignificantlyOverdue) {
214
+ // Job is significantly overdue - recalculate from now to find next future occurrence
215
+ const newNextRun = this.calculateNextRun(job, now);
216
+ if (newNextRun && newNextRun !== job.nextRun) {
217
+ this.logger.debug(
218
+ `Recalculating next run for job ${id} (${job.name || 'unnamed'}): ` +
219
+ `${job.nextRun.toISOString()} → ${newNextRun.toISOString()}`
220
+ );
221
+ this.updateJobNextRun(id, newNextRun);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+
170
228
  /**
171
229
  * Get jobs that are due to run
172
230
  * @returns {Array} Array of jobs that should run now
@@ -249,11 +307,32 @@ export class Scheduler {
249
307
  return [];
250
308
  }
251
309
 
310
+ // Detect sleep/wake events by checking if too much time has passed since last tick
311
+ const now = Date.now();
312
+ const timeSinceLastTick = this.lastTickTime ? now - this.lastTickTime : 0;
313
+ const sleepThreshold = this.checkIntervalMs * 5; // If more than 5 intervals passed, likely woke from sleep
314
+
315
+ if (timeSinceLastTick > sleepThreshold) {
316
+ const secondsAsleep = Math.round(timeSinceLastTick / 1000);
317
+ this.logger.info(`System wake detected - was asleep for ${secondsAsleep}s, catching up on due jobs`);
318
+ }
319
+
320
+ this.lastTickTime = now;
321
+
322
+ const nowDate = new Date();
323
+
324
+ // Recalculate next run for periodic jobs that have drifted into the past
325
+ // This handles system sleep/wake scenarios
326
+ this.recalculateStalePeriodicJobs(nowDate);
327
+
252
328
  const dueJobs = this.getDueJobs();
253
329
 
254
330
  for (const job of dueJobs) {
255
331
  this.logger.debug(`Job ${job.id} (${job.name || 'unnamed'}) is due`);
256
332
 
333
+ // Store the original scheduled time before execution
334
+ const originalNextRun = job.nextRun ? new Date(job.nextRun) : new Date();
335
+
257
336
  // Execute the job
258
337
  this.executeJob(job);
259
338
 
@@ -261,8 +340,12 @@ export class Scheduler {
261
340
  if (job.type === JobType.ONCE) {
262
341
  this.updateJobStatus(job.id, JobStatus.COMPLETED);
263
342
  } else {
264
- // For cron jobs, recalculate next run time
265
- const nextRun = this.calculateNextRun({ ...job, status: JobStatus.ACTIVE });
343
+ // For cron jobs, recalculate next run time from the original scheduled time
344
+ // This ensures we don't miss runs after system wake from sleep
345
+ const nextRun = this.calculateNextRunAfterExecution(
346
+ { ...job, status: JobStatus.ACTIVE },
347
+ originalNextRun
348
+ );
266
349
  this.updateJobNextRun(job.id, nextRun);
267
350
  }
268
351
  }
@@ -291,7 +374,7 @@ export class Scheduler {
291
374
  }
292
375
 
293
376
  // Calculate initial next run
294
- const nextRun = this.calculateNextRun(jobData);
377
+ const nextRun = this.calculateNextRun(jobData, new Date());
295
378
  const job = { ...jobData, nextRun };
296
379
 
297
380
  // Add to memory
@@ -368,7 +451,7 @@ export class Scheduler {
368
451
  };
369
452
 
370
453
  // Recalculate next run if needed
371
- updatedJob.nextRun = this.calculateNextRun(updatedJob);
454
+ updatedJob.nextRun = this.calculateNextRun(updatedJob, new Date());
372
455
 
373
456
  this.jobs.set(jobId, updatedJob);
374
457
  this.persistJobs();
@@ -390,7 +473,7 @@ export class Scheduler {
390
473
  // Recalculate next run when activating
391
474
  const job = this.jobs.get(jobId);
392
475
  if (job) {
393
- updates.nextRun = this.calculateNextRun({ ...job, status });
476
+ updates.nextRun = this.calculateNextRun({ ...job, status }, new Date());
394
477
  }
395
478
  } else {
396
479
  updates.nextRun = null;