@skelm/scheduler 0.4.1 → 0.4.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.
@@ -12,6 +12,7 @@ export declare class Scheduler {
12
12
  private webhookServer;
13
13
  private pollJobs;
14
14
  private queueJobs;
15
+ private readonly inFlight;
15
16
  private isRunning;
16
17
  private readonly runStore;
17
18
  private readonly pipelineLoader;
@@ -39,8 +40,19 @@ export declare class Scheduler {
39
40
  stopWebhookServer(): Promise<void>;
40
41
  /** Start all triggers */
41
42
  start(): Promise<void>;
42
- /** Stop all triggers */
43
+ /**
44
+ * Stop all triggers. Clears the interval timers, then waits up to 30s
45
+ * for any in-flight executeTrigger callbacks to settle so a SIGTERM
46
+ * does not leave fire-and-forget executions racing the process exit.
47
+ *
48
+ * Runs unconditionally — `register()` arms timers immediately without
49
+ * setting isRunning, so a stop() that gated on isRunning would leak
50
+ * those timers when the scheduler is constructed without start().
51
+ */
43
52
  stop(): Promise<void>;
53
+ /** Track a fire-and-forget execution so stop() can drain it. */
54
+ private track;
55
+ private drainInFlight;
44
56
  private startCronTrigger;
45
57
  private stopCronTrigger;
46
58
  private startIntervalTrigger;
package/dist/scheduler.js CHANGED
@@ -11,6 +11,7 @@ export class Scheduler {
11
11
  webhookServer = null;
12
12
  pollJobs = new Map();
13
13
  queueJobs = new Map();
14
+ inFlight = new Set();
14
15
  isRunning = false;
15
16
  runStore;
16
17
  pipelineLoader;
@@ -154,10 +155,16 @@ export class Scheduler {
154
155
  }
155
156
  }
156
157
  }
157
- /** Stop all triggers */
158
+ /**
159
+ * Stop all triggers. Clears the interval timers, then waits up to 30s
160
+ * for any in-flight executeTrigger callbacks to settle so a SIGTERM
161
+ * does not leave fire-and-forget executions racing the process exit.
162
+ *
163
+ * Runs unconditionally — `register()` arms timers immediately without
164
+ * setting isRunning, so a stop() that gated on isRunning would leak
165
+ * those timers when the scheduler is constructed without start().
166
+ */
158
167
  async stop() {
159
- if (!this.isRunning)
160
- return;
161
168
  this.isRunning = false;
162
169
  // Clear all jobs
163
170
  for (const [id, job] of this.cronJobs) {
@@ -176,14 +183,28 @@ export class Scheduler {
176
183
  clearInterval(job);
177
184
  this.queueJobs.delete(id);
178
185
  }
186
+ await this.drainInFlight(30_000);
187
+ }
188
+ /** Track a fire-and-forget execution so stop() can drain it. */
189
+ track(promise) {
190
+ this.inFlight.add(promise);
191
+ promise.finally(() => this.inFlight.delete(promise));
192
+ }
193
+ async drainInFlight(timeoutMs) {
194
+ if (this.inFlight.size === 0)
195
+ return;
196
+ await Promise.race([
197
+ Promise.allSettled([...this.inFlight]),
198
+ new Promise((resolve) => setTimeout(resolve, timeoutMs).unref?.()),
199
+ ]);
179
200
  }
180
201
  startCronTrigger(trigger) {
181
202
  // Simple cron implementation using setInterval
182
203
  // In production, use 'cron' package for proper cron expression parsing
183
204
  const intervalMs = this.parseCronToInterval(trigger.schedule);
184
- const job = setInterval(async () => {
205
+ const job = setInterval(() => {
185
206
  if (trigger.enabled && this.triggers.get(trigger.id)?.status === 'active') {
186
- await this.executeTrigger(trigger);
207
+ this.track(this.executeTrigger(trigger));
187
208
  }
188
209
  }, intervalMs);
189
210
  this.cronJobs.set(trigger.id, job);
@@ -196,9 +217,9 @@ export class Scheduler {
196
217
  }
197
218
  }
198
219
  startIntervalTrigger(trigger) {
199
- const job = setInterval(async () => {
220
+ const job = setInterval(() => {
200
221
  if (trigger.enabled && this.triggers.get(trigger.id)?.status === 'active') {
201
- await this.executeTrigger(trigger);
222
+ this.track(this.executeTrigger(trigger));
202
223
  }
203
224
  }, trigger.intervalMs);
204
225
  this.intervalJobs.set(trigger.id, job);
@@ -211,9 +232,9 @@ export class Scheduler {
211
232
  }
212
233
  }
213
234
  startPollTrigger(trigger) {
214
- const job = setInterval(async () => {
235
+ const job = setInterval(() => {
215
236
  if (trigger.enabled && this.triggers.get(trigger.id)?.status === 'active') {
216
- await this.executePollTrigger(trigger);
237
+ this.track(this.executePollTrigger(trigger));
217
238
  }
218
239
  }, trigger.intervalMs);
219
240
  this.pollJobs.set(trigger.id, job);
@@ -226,9 +247,9 @@ export class Scheduler {
226
247
  }
227
248
  }
228
249
  startQueueTrigger(trigger) {
229
- const job = setInterval(async () => {
250
+ const job = setInterval(() => {
230
251
  if (trigger.enabled && this.triggers.get(trigger.id)?.status === 'active') {
231
- await this.executeQueueTrigger(trigger);
252
+ this.track(this.executeQueueTrigger(trigger));
232
253
  }
233
254
  }, this.config.queuePollIntervalMs);
234
255
  this.queueJobs.set(trigger.id, job);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skelm/scheduler",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "license": "MIT",
5
5
  "author": "Scott Glover <scottgl@gmail.com>",
6
6
  "homepage": "https://skelm.dev/",
@@ -44,7 +44,7 @@
44
44
  "test": "vitest run"
45
45
  },
46
46
  "dependencies": {
47
- "@skelm/core": "^0.4.1"
47
+ "@skelm/core": "^0.4.2"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^22.15.0",