@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 +8 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +57 -5
- package/package.json +5 -5
- package/src/index.ts +63 -8
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
|
-
|
|
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
|
-
|
|
178
|
+
if (!this.stopped) {
|
|
179
|
+
return this.doNext(client);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
172
182
|
}
|
|
173
183
|
catch (err) {
|
|
174
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
"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.
|
|
32
|
-
"@constructive-io/job-utils": "^0.
|
|
33
|
-
"@pgpmjs/logger": "^1.
|
|
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": "
|
|
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.
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
189
|
+
if (!this.stopped) {
|
|
190
|
+
return this.doNext(client);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
184
193
|
} catch (err: unknown) {
|
|
185
|
-
|
|
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
|
-
|
|
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.
|
|
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 };
|