@monque/core 1.5.2 → 1.6.0

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.
@@ -21,19 +21,26 @@ interface TimerCallbacks {
21
21
  poll: () => Promise<void>;
22
22
  /** Update heartbeats for claimed jobs */
23
23
  updateHeartbeats: () => Promise<void>;
24
+ /** Whether change streams are currently active */
25
+ isChangeStreamActive: () => boolean;
24
26
  }
25
27
 
26
28
  /**
27
29
  * Manages scheduler lifecycle timers and job cleanup.
28
30
  *
29
- * Owns poll interval, heartbeat interval, cleanup interval, and the
31
+ * Owns poll scheduling, heartbeat interval, cleanup interval, and the
30
32
  * cleanupJobs logic. Extracted from Monque to keep the facade thin.
31
33
  *
34
+ * Uses adaptive poll scheduling: when change streams are active, polls at
35
+ * `safetyPollInterval` (safety net only). When change streams are inactive,
36
+ * polls at `pollInterval` (primary discovery mechanism).
37
+ *
32
38
  * @internal Not part of public API.
33
39
  */
34
40
  export class LifecycleManager {
35
41
  private readonly ctx: SchedulerContext;
36
- private pollIntervalId: ReturnType<typeof setInterval> | null = null;
42
+ private callbacks: TimerCallbacks | null = null;
43
+ private pollTimeoutId: ReturnType<typeof setTimeout> | null = null;
37
44
  private heartbeatIntervalId: ReturnType<typeof setInterval> | null = null;
38
45
  private cleanupIntervalId: ReturnType<typeof setInterval> | null = null;
39
46
 
@@ -44,18 +51,13 @@ export class LifecycleManager {
44
51
  /**
45
52
  * Start all lifecycle timers.
46
53
  *
47
- * Sets up poll interval, heartbeat interval, and (if configured)
54
+ * Sets up adaptive poll scheduling, heartbeat interval, and (if configured)
48
55
  * cleanup interval. Runs an initial poll immediately.
49
56
  *
50
57
  * @param callbacks - Functions to invoke on each timer tick
51
58
  */
52
59
  startTimers(callbacks: TimerCallbacks): void {
53
- // Set up polling as backup (runs at configured interval)
54
- this.pollIntervalId = setInterval(() => {
55
- callbacks.poll().catch((error: unknown) => {
56
- this.ctx.emit('job:error', { error: toError(error) });
57
- });
58
- }, this.ctx.options.pollInterval);
60
+ this.callbacks = callbacks;
59
61
 
60
62
  // Start heartbeat interval for claimed jobs
61
63
  this.heartbeatIntervalId = setInterval(() => {
@@ -80,26 +82,26 @@ export class LifecycleManager {
80
82
  }, interval);
81
83
  }
82
84
 
83
- // Run initial poll immediately to pick up any existing jobs
84
- callbacks.poll().catch((error: unknown) => {
85
- this.ctx.emit('job:error', { error: toError(error) });
86
- });
85
+ // Run initial poll immediately, then schedule the next one adaptively
86
+ this.executePollAndScheduleNext();
87
87
  }
88
88
 
89
89
  /**
90
90
  * Stop all lifecycle timers.
91
91
  *
92
- * Clears poll, heartbeat, and cleanup intervals.
92
+ * Clears poll timeout, heartbeat interval, and cleanup interval.
93
93
  */
94
94
  stopTimers(): void {
95
+ this.callbacks = null;
96
+
95
97
  if (this.cleanupIntervalId) {
96
98
  clearInterval(this.cleanupIntervalId);
97
99
  this.cleanupIntervalId = null;
98
100
  }
99
101
 
100
- if (this.pollIntervalId) {
101
- clearInterval(this.pollIntervalId);
102
- this.pollIntervalId = null;
102
+ if (this.pollTimeoutId) {
103
+ clearTimeout(this.pollTimeoutId);
104
+ this.pollTimeoutId = null;
103
105
  }
104
106
 
105
107
  if (this.heartbeatIntervalId) {
@@ -108,6 +110,59 @@ export class LifecycleManager {
108
110
  }
109
111
  }
110
112
 
113
+ /**
114
+ * Reset the poll timer to reschedule the next poll.
115
+ *
116
+ * Called after change-stream-triggered polls to ensure the safety poll timer
117
+ * is recalculated (not fired redundantly from an old schedule).
118
+ */
119
+ resetPollTimer(): void {
120
+ this.scheduleNextPoll();
121
+ }
122
+
123
+ /**
124
+ * Execute a poll and schedule the next one adaptively.
125
+ */
126
+ private executePollAndScheduleNext(): void {
127
+ if (!this.callbacks) {
128
+ return;
129
+ }
130
+
131
+ this.callbacks
132
+ .poll()
133
+ .catch((error: unknown) => {
134
+ this.ctx.emit('job:error', { error: toError(error) });
135
+ })
136
+ .then(() => {
137
+ this.scheduleNextPoll();
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Schedule the next poll using adaptive timing.
143
+ *
144
+ * When change streams are active, uses `safetyPollInterval` (longer, safety net only).
145
+ * When change streams are inactive, uses `pollInterval` (shorter, primary discovery).
146
+ */
147
+ private scheduleNextPoll(): void {
148
+ if (this.pollTimeoutId) {
149
+ clearTimeout(this.pollTimeoutId);
150
+ this.pollTimeoutId = null;
151
+ }
152
+
153
+ if (!this.ctx.isRunning() || !this.callbacks) {
154
+ return;
155
+ }
156
+
157
+ const delay = this.callbacks.isChangeStreamActive()
158
+ ? this.ctx.options.safetyPollInterval
159
+ : this.ctx.options.pollInterval;
160
+
161
+ this.pollTimeoutId = setTimeout(() => {
162
+ this.executePollAndScheduleNext();
163
+ }, delay);
164
+ }
165
+
111
166
  /**
112
167
  * Clean up old completed and failed jobs based on retention policy.
113
168
  *
@@ -30,6 +30,7 @@ export interface ResolvedMonqueOptions
30
30
  > {
31
31
  // Ensure resolved options use the new naming convention
32
32
  workerConcurrency: number;
33
+ safetyPollInterval: number;
33
34
  }
34
35
  /**
35
36
  * Shared context provided to all internal Monque services.
@@ -59,6 +60,9 @@ export interface SchedulerContext {
59
60
  /** Type-safe event emitter */
60
61
  emit: <K extends keyof MonqueEventMap>(event: K, payload: MonqueEventMap[K]) => boolean;
61
62
 
63
+ /** Notify the local scheduler about a pending job transition */
64
+ notifyPendingJob: (name: string, nextRunAt: Date) => void;
65
+
62
66
  /** Convert MongoDB document to typed PersistedJob */
63
67
  documentToPersistedJob: <T>(doc: WithId<Document>) => PersistedJob<T>;
64
68
  }
@@ -196,4 +196,18 @@ export interface MonqueOptions {
196
196
  * @default 5000
197
197
  */
198
198
  statsCacheTtlMs?: number;
199
+
200
+ /**
201
+ * Interval in milliseconds between safety polls when change streams are active.
202
+ *
203
+ * When change streams are connected, the scheduler uses them as the primary
204
+ * notification mechanism and only polls at this longer interval as a safety net
205
+ * to catch any missed events. When change streams are unavailable, the scheduler
206
+ * falls back to the standard `pollInterval`.
207
+ *
208
+ * This is separate from `heartbeatInterval`, which updates job liveness signals.
209
+ *
210
+ * @default 30000 (30 seconds)
211
+ */
212
+ safetyPollInterval?: number;
199
213
  }