@constructive-io/job-scheduler 0.3.22 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [0.4.0](https://github.com/constructive-io/jobs/compare/@constructive-io/job-scheduler@0.3.23...@constructive-io/job-scheduler@0.4.0) (2026-01-18)
7
+
8
+ **Note:** Version bump only for package @constructive-io/job-scheduler
9
+
10
+ ## [0.3.23](https://github.com/constructive-io/jobs/compare/@constructive-io/job-scheduler@0.3.22...@constructive-io/job-scheduler@0.3.23) (2026-01-18)
11
+
12
+ **Note:** Version bump only for package @constructive-io/job-scheduler
13
+
6
14
  ## [0.3.22](https://github.com/constructive-io/jobs/compare/@constructive-io/job-scheduler@0.3.21...@constructive-io/job-scheduler@0.3.22) (2026-01-09)
7
15
 
8
16
  **Note:** Version bump only for package @constructive-io/job-scheduler
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PgClientLike } from '@constructive-io/job-utils';
2
- import type { Pool } from 'pg';
2
+ import type { Pool, PoolClient } from 'pg';
3
3
  export interface ScheduledJobRow {
4
4
  id: number | string;
5
5
  task_identifier: string;
@@ -16,6 +16,9 @@ export default class Scheduler {
16
16
  pgPool: Pool;
17
17
  jobs: Record<ScheduledJobRow['id'], SchedulerJobHandle>;
18
18
  _initialized?: boolean;
19
+ listenClient?: PoolClient;
20
+ listenRelease?: () => void;
21
+ stopped?: boolean;
19
22
  constructor({ tasks, idleDelay, pgPool, workerId }: {
20
23
  tasks: string[];
21
24
  idleDelay?: number;
@@ -40,5 +43,6 @@ export default class Scheduler {
40
43
  scheduleJob(client: PgClientLike, job: ScheduledJobRow): Promise<void>;
41
44
  doNext(client: PgClientLike): Promise<void>;
42
45
  listen(): void;
46
+ stop(): Promise<void>;
43
47
  }
44
48
  export { Scheduler };
package/dist/index.js CHANGED
@@ -50,6 +50,9 @@ class Scheduler {
50
50
  pgPool;
51
51
  jobs;
52
52
  _initialized;
53
+ listenClient;
54
+ listenRelease;
55
+ stopped;
53
56
  constructor({ tasks, idleDelay = 15000, pgPool = job_pg_1.default.getPool(), workerId = 'scheduler-0' }) {
54
57
  /*
55
58
  * idleDelay: This is how long to wait between polling for jobs.
@@ -128,6 +131,8 @@ class Scheduler {
128
131
  this.jobs[id] = j;
129
132
  }
130
133
  async doNext(client) {
134
+ if (this.stopped)
135
+ return;
131
136
  if (!this._initialized) {
132
137
  return await this.initialize(client);
133
138
  }
@@ -143,7 +148,9 @@ class Scheduler {
143
148
  : this.supportedTaskNames
144
149
  });
145
150
  if (!job || !job.id) {
146
- this.doNextTimer = setTimeout(() => this.doNext(client), this.idleDelay);
151
+ if (!this.stopped) {
152
+ this.doNextTimer = setTimeout(() => this.doNext(client), this.idleDelay);
153
+ }
147
154
  return;
148
155
  }
149
156
  const start = process.hrtime();
@@ -168,13 +175,20 @@ class Scheduler {
168
175
  catch (fatalError) {
169
176
  await this.handleFatalError(client, { err, fatalError, jobId });
170
177
  }
171
- return this.doNext(client);
178
+ if (!this.stopped) {
179
+ return this.doNext(client);
180
+ }
181
+ return;
172
182
  }
173
183
  catch (err) {
174
- this.doNextTimer = setTimeout(() => this.doNext(client), this.idleDelay);
184
+ if (!this.stopped) {
185
+ this.doNextTimer = setTimeout(() => this.doNext(client), this.idleDelay);
186
+ }
175
187
  }
176
188
  }
177
189
  listen() {
190
+ if (this.stopped)
191
+ return;
178
192
  const listenForChanges = (err, client, release) => {
179
193
  if (err) {
180
194
  log.error('Error connecting with notify listener', err);
@@ -183,9 +197,17 @@ class Scheduler {
183
197
  }
184
198
  // Try again in 5 seconds
185
199
  // should this really be done in the node process?
186
- setTimeout(this.listen, 5000);
200
+ if (!this.stopped) {
201
+ setTimeout(this.listen, 5000);
202
+ }
203
+ return;
204
+ }
205
+ if (this.stopped) {
206
+ release();
187
207
  return;
188
208
  }
209
+ this.listenClient = client;
210
+ this.listenRelease = release;
189
211
  client.on('notification', () => {
190
212
  log.info('a NEW scheduled JOB!');
191
213
  if (this.doNextTimer) {
@@ -195,18 +217,48 @@ class Scheduler {
195
217
  });
196
218
  client.query('LISTEN "scheduled_jobs:insert"');
197
219
  client.on('error', (e) => {
220
+ if (this.stopped) {
221
+ release();
222
+ return;
223
+ }
198
224
  log.error('Error with database notify listener', e);
199
225
  if (e instanceof Error && e.stack) {
200
226
  log.debug(e.stack);
201
227
  }
202
228
  release();
203
- this.listen();
229
+ if (!this.stopped) {
230
+ this.listen();
231
+ }
204
232
  });
205
233
  log.info(`${this.workerId} connected and looking for scheduled jobs...`);
206
234
  this.doNext(client);
207
235
  };
208
236
  this.pgPool.connect(listenForChanges);
209
237
  }
238
+ async stop() {
239
+ this.stopped = true;
240
+ if (this.doNextTimer) {
241
+ clearTimeout(this.doNextTimer);
242
+ this.doNextTimer = undefined;
243
+ }
244
+ Object.values(this.jobs).forEach((job) => job.cancel());
245
+ this.jobs = {};
246
+ const client = this.listenClient;
247
+ const release = this.listenRelease;
248
+ this.listenClient = undefined;
249
+ this.listenRelease = undefined;
250
+ if (client && release) {
251
+ client.removeAllListeners('notification');
252
+ client.removeAllListeners('error');
253
+ try {
254
+ await client.query('UNLISTEN "scheduled_jobs:insert"');
255
+ }
256
+ catch {
257
+ // Ignore listener cleanup errors during shutdown.
258
+ }
259
+ release();
260
+ }
261
+ }
210
262
  }
211
263
  exports.default = Scheduler;
212
264
  exports.Scheduler = Scheduler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/job-scheduler",
3
- "version": "0.3.22",
3
+ "version": "0.4.0",
4
4
  "description": "job scheduler",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/jobs/tree/master/packages/job-scheduler#readme",
@@ -28,10 +28,10 @@
28
28
  "url": "https://github.com/constructive-io/jobs/issues"
29
29
  },
30
30
  "dependencies": {
31
- "@constructive-io/job-pg": "^0.3.20",
32
- "@constructive-io/job-utils": "^0.5.15",
33
- "@pgpmjs/logger": "^1.3.7",
31
+ "@constructive-io/job-pg": "^0.4.0",
32
+ "@constructive-io/job-utils": "^0.6.0",
33
+ "@pgpmjs/logger": "^1.4.0",
34
34
  "node-schedule": "1.3.2"
35
35
  },
36
- "gitHead": "cb4af2cf6c23dad24cd951c232d3e2006b81aa3d"
36
+ "gitHead": "481b3a50b4eec2da6b376c4cd1868065e1e28edb"
37
37
  }
package/src/index.ts CHANGED
@@ -25,6 +25,9 @@ export default class Scheduler {
25
25
  pgPool: Pool;
26
26
  jobs: Record<ScheduledJobRow['id'], SchedulerJobHandle>;
27
27
  _initialized?: boolean;
28
+ listenClient?: PoolClient;
29
+ listenRelease?: () => void;
30
+ stopped?: boolean;
28
31
 
29
32
  constructor({
30
33
  tasks,
@@ -135,6 +138,7 @@ export default class Scheduler {
135
138
  this.jobs[id] = j as SchedulerJobHandle;
136
139
  }
137
140
  async doNext(client: PgClientLike): Promise<void> {
141
+ if (this.stopped) return;
138
142
  if (!this._initialized) {
139
143
  return await this.initialize(client);
140
144
  }
@@ -151,10 +155,12 @@ export default class Scheduler {
151
155
  : this.supportedTaskNames
152
156
  });
153
157
  if (!job || !job.id) {
154
- this.doNextTimer = setTimeout(
155
- () => this.doNext(client),
156
- this.idleDelay
157
- );
158
+ if (!this.stopped) {
159
+ this.doNextTimer = setTimeout(
160
+ () => this.doNext(client),
161
+ this.idleDelay
162
+ );
163
+ }
158
164
  return;
159
165
  }
160
166
  const start = process.hrtime();
@@ -180,12 +186,21 @@ export default class Scheduler {
180
186
  } catch (fatalError: unknown) {
181
187
  await this.handleFatalError(client, { err, fatalError, jobId });
182
188
  }
183
- return this.doNext(client);
189
+ if (!this.stopped) {
190
+ return this.doNext(client);
191
+ }
192
+ return;
184
193
  } catch (err: unknown) {
185
- this.doNextTimer = setTimeout(() => this.doNext(client), this.idleDelay);
194
+ if (!this.stopped) {
195
+ this.doNextTimer = setTimeout(
196
+ () => this.doNext(client),
197
+ this.idleDelay
198
+ );
199
+ }
186
200
  }
187
201
  }
188
202
  listen() {
203
+ if (this.stopped) return;
189
204
  const listenForChanges = (
190
205
  err: Error | null,
191
206
  client: PoolClient,
@@ -198,9 +213,17 @@ export default class Scheduler {
198
213
  }
199
214
  // Try again in 5 seconds
200
215
  // should this really be done in the node process?
201
- setTimeout(this.listen, 5000);
216
+ if (!this.stopped) {
217
+ setTimeout(this.listen, 5000);
218
+ }
219
+ return;
220
+ }
221
+ if (this.stopped) {
222
+ release();
202
223
  return;
203
224
  }
225
+ this.listenClient = client;
226
+ this.listenRelease = release;
204
227
  client.on('notification', () => {
205
228
  log.info('a NEW scheduled JOB!');
206
229
  if (this.doNextTimer) {
@@ -210,12 +233,18 @@ export default class Scheduler {
210
233
  });
211
234
  client.query('LISTEN "scheduled_jobs:insert"');
212
235
  client.on('error', (e: unknown) => {
236
+ if (this.stopped) {
237
+ release();
238
+ return;
239
+ }
213
240
  log.error('Error with database notify listener', e);
214
241
  if (e instanceof Error && e.stack) {
215
242
  log.debug(e.stack);
216
243
  }
217
244
  release();
218
- this.listen();
245
+ if (!this.stopped) {
246
+ this.listen();
247
+ }
219
248
  });
220
249
  log.info(
221
250
  `${this.workerId} connected and looking for scheduled jobs...`
@@ -224,6 +253,32 @@ export default class Scheduler {
224
253
  };
225
254
  this.pgPool.connect(listenForChanges);
226
255
  }
256
+
257
+ async stop(): Promise<void> {
258
+ this.stopped = true;
259
+ if (this.doNextTimer) {
260
+ clearTimeout(this.doNextTimer);
261
+ this.doNextTimer = undefined;
262
+ }
263
+ Object.values(this.jobs).forEach((job) => job.cancel());
264
+ this.jobs = {};
265
+
266
+ const client = this.listenClient;
267
+ const release = this.listenRelease;
268
+ this.listenClient = undefined;
269
+ this.listenRelease = undefined;
270
+
271
+ if (client && release) {
272
+ client.removeAllListeners('notification');
273
+ client.removeAllListeners('error');
274
+ try {
275
+ await client.query('UNLISTEN "scheduled_jobs:insert"');
276
+ } catch {
277
+ // Ignore listener cleanup errors during shutdown.
278
+ }
279
+ release();
280
+ }
281
+ }
227
282
  }
228
283
 
229
284
  export { Scheduler };