pnscheduler 0.0.1
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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +18 -0
- package/dist/db/migration.d.ts +1 -0
- package/dist/db/migration.js +18 -0
- package/dist/db/migrationScript.d.ts +1 -0
- package/dist/db/migrationScript.js +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/jobs.d.ts +19 -0
- package/dist/jobs.js +42 -0
- package/dist/scheduler.d.ts +31 -0
- package/dist/scheduler.js +125 -0
- package/package.json +45 -0
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Nil Mustard (https://github.com/nilmus)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# PNScheduler
|
2
|
+
|
3
|
+
Persistent NodeJS Scheduler - A simple job scheduler for Node.js applications, with Postgres-based persistence.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
$ npm install pnscheduler
|
9
|
+
```
|
10
|
+
|
11
|
+
## Features
|
12
|
+
|
13
|
+
- Schedule jobs at a specific time
|
14
|
+
- Jobs persist between restarts
|
15
|
+
- Configurable grace periods
|
16
|
+
|
17
|
+
## Set Up
|
18
|
+
|
19
|
+
First of all, set the PGCONNECTIONSTRING environment variable for your Postgres database:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
#.env
|
23
|
+
PGCONNECTIONSTRING=<connection_string>
|
24
|
+
```
|
25
|
+
|
26
|
+
Secondly, run the migration script to create the database tables:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
$ node --env-file=.env node_modules/pnscheduler/dist/db/migrationScript.js
|
30
|
+
```
|
31
|
+
|
32
|
+
## Quick Start
|
33
|
+
|
34
|
+
Just define a job, schedule it, and start the scheduler.
|
35
|
+
|
36
|
+
```typescript
|
37
|
+
import scheduler from 'pnscheduler';
|
38
|
+
|
39
|
+
// Define a simple job
|
40
|
+
scheduler.defineJob("greeting", () => {
|
41
|
+
console.log("Hello, world!");
|
42
|
+
});
|
43
|
+
|
44
|
+
// Schedule it for tomorrow
|
45
|
+
await scheduler.scheduleJob("greeting", new Date(Date.now() + 86400000));
|
46
|
+
|
47
|
+
// Start the scheduler
|
48
|
+
scheduler.start();
|
49
|
+
```
|
50
|
+
|
51
|
+
## Usage Examples
|
52
|
+
|
53
|
+
### Defining Jobs
|
54
|
+
|
55
|
+
```typescript
|
56
|
+
// Job without parameters
|
57
|
+
scheduler.defineJob("simple-job", () => {
|
58
|
+
console.log("Hello without params!");
|
59
|
+
});
|
60
|
+
|
61
|
+
// Job with parameters
|
62
|
+
scheduler.defineJob("parameterized-job",
|
63
|
+
({ something }: { something: string }) => {
|
64
|
+
console.log(something);
|
65
|
+
}
|
66
|
+
);
|
67
|
+
```
|
68
|
+
|
69
|
+
### Scheduling Jobs
|
70
|
+
|
71
|
+
```typescript
|
72
|
+
// Basic scheduling
|
73
|
+
await scheduler.scheduleJob("simple-job", new Date(2084, 0, 1));
|
74
|
+
|
75
|
+
// With parameters
|
76
|
+
await scheduler.scheduleJob("parameterized-job", new Date(2084, 0, 1), {
|
77
|
+
something: "Hello, world!"
|
78
|
+
});
|
79
|
+
|
80
|
+
// With grace period (5 minutes)
|
81
|
+
await scheduler.scheduleJob("simple-job", new Date(2084, 0, 1), null, 300_000);
|
82
|
+
```
|
83
|
+
|
84
|
+
### Job Management
|
85
|
+
|
86
|
+
```typescript
|
87
|
+
// Unscheduling a job
|
88
|
+
const job = await scheduler.scheduleJob("simple-job", new Date(2084, 0, 1));
|
89
|
+
await scheduler.unscheduleJob(job);
|
90
|
+
|
91
|
+
// Manual job execution
|
92
|
+
await scheduler.executeJob(job);
|
93
|
+
|
94
|
+
// Finding scheduled jobs
|
95
|
+
const scheduledJobs = await scheduler.findScheduledJobs();
|
96
|
+
console.log(scheduledJobs);
|
97
|
+
|
98
|
+
// Finding due jobs
|
99
|
+
const dueJobs = await scheduler.findDueJobs();
|
100
|
+
console.log(dueJobs);
|
101
|
+
```
|
102
|
+
|
103
|
+
### Scheduler Control
|
104
|
+
|
105
|
+
```typescript
|
106
|
+
// Start the scheduler (30s check interval)
|
107
|
+
scheduler.start(3000);
|
108
|
+
|
109
|
+
// Stop the scheduler
|
110
|
+
scheduler.stop();
|
111
|
+
```
|
112
|
+
|
113
|
+
## API Reference
|
114
|
+
|
115
|
+
### Scheduler Methods
|
116
|
+
|
117
|
+
- `defineJob(name: string, handler: Function): void`
|
118
|
+
- `scheduleJob(name: string, date: Date, params?: any, gracePeriod?: number): Promise<Job>`
|
119
|
+
- `unscheduleJob(job: Job): Promise<void>`
|
120
|
+
- `executeJob(job: Job): Promise<void>`
|
121
|
+
- `findScheduledJobs(): Promise<Job[]>`
|
122
|
+
- `start(interval: number): void`
|
123
|
+
- `stop(): void`
|
124
|
+
|
125
|
+
### Scheduler Properties
|
126
|
+
|
127
|
+
- `jobRegistry` - As an alternative to `defineJob`, you can also define jobs by assigning directly to this property,
|
128
|
+
e.g. `scheduler.jobRegistry = {"job-name", () => {}}`.
|
129
|
+
- `verbose` - Set this to true to enable verbose logging,
|
130
|
+
e.g. `scheduler.verbose = true;`
|
131
|
+
|
132
|
+
|
133
|
+
## Contributing
|
134
|
+
|
135
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
136
|
+
|
137
|
+
## License
|
138
|
+
|
139
|
+
MIT License
|
package/dist/db/index.js
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
import { Pool } from "pg";
|
2
|
+
function validateEnv() {
|
3
|
+
if (!process.env.PGCONNECTIONSTRING) {
|
4
|
+
throw new Error("env variable PGCONNECTIONSTRING is not set");
|
5
|
+
}
|
6
|
+
}
|
7
|
+
let pool = null;
|
8
|
+
const getPool = () => {
|
9
|
+
if (!pool) {
|
10
|
+
validateEnv();
|
11
|
+
pool = new Pool({ connectionString: process.env.PGCONNECTIONSTRING });
|
12
|
+
}
|
13
|
+
return pool;
|
14
|
+
};
|
15
|
+
export const query = (text, params) => {
|
16
|
+
return getPool().query(text, params);
|
17
|
+
};
|
18
|
+
export const closePool = () => getPool().end();
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const migrationQuery: Promise<import("pg").QueryResult<any>>;
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import * as db from "./index.js";
|
2
|
+
export const migrationQuery = db.query(`
|
3
|
+
DROP SCHEMA IF EXISTS pnscheduler CASCADE;
|
4
|
+
|
5
|
+
CREATE SCHEMA IF NOT EXISTS pnscheduler;
|
6
|
+
|
7
|
+
CREATE TYPE pnscheduler.status_type AS ENUM ('pending', 'executed', 'failed', 'skipped');
|
8
|
+
|
9
|
+
CREATE TABLE IF NOT EXISTS pnscheduler.jobs (
|
10
|
+
id SERIAL PRIMARY KEY,
|
11
|
+
name VARCHAR(255) NOT NULL,
|
12
|
+
execution_date TIMESTAMPTZ NOT NULL,
|
13
|
+
grace_period INTEGER,
|
14
|
+
params JSONB,
|
15
|
+
status pnscheduler.status_type DEFAULT 'pending' NOT NULL,
|
16
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
17
|
+
)
|
18
|
+
`);
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/jobs.d.ts
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
export type JobStatus = "pending" | "executed" | "failed" | "skipped";
|
2
|
+
type JobRow = {
|
3
|
+
id: number;
|
4
|
+
name: string;
|
5
|
+
execution_date: Date;
|
6
|
+
grace_period: number | null;
|
7
|
+
params: any | null;
|
8
|
+
status: JobStatus;
|
9
|
+
};
|
10
|
+
export declare class Job {
|
11
|
+
id: number;
|
12
|
+
name: string;
|
13
|
+
executionDate: Date;
|
14
|
+
gracePeriod: number | null;
|
15
|
+
params: any;
|
16
|
+
status: JobStatus;
|
17
|
+
constructor({ id, name, execution_date, grace_period, params, status, }: JobRow);
|
18
|
+
}
|
19
|
+
export {};
|
package/dist/jobs.js
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
function validateJobRow(row) {
|
2
|
+
if (typeof row !== "object" || row === null)
|
3
|
+
throw new Error("Invalid job row");
|
4
|
+
if (typeof row.id !== "number")
|
5
|
+
throw new Error(`Invalid job row column: id - ${row.id}`);
|
6
|
+
if (typeof row.name !== "string")
|
7
|
+
throw new Error(`Invalid job row column: name - ${row.name}`);
|
8
|
+
if (!(row.execution_date instanceof Date))
|
9
|
+
throw new Error(`Invalid job row column: execution_date - ${row.execution_date}`);
|
10
|
+
if (typeof row.grace_period !== "number" && row.grace_period !== null)
|
11
|
+
throw new Error(`Invalid job row column: grace_period - ${row.grace_period}`);
|
12
|
+
if (row.params !== null && typeof row.params !== "object")
|
13
|
+
throw new Error(`Invalid job row column: params - ${row.params}`);
|
14
|
+
if (typeof row.status !== "string" ||
|
15
|
+
!["pending", "executed", "failed", "skipped"].includes(row.status))
|
16
|
+
throw new Error(`Invalid job row column: status - ${row.status}`);
|
17
|
+
return row;
|
18
|
+
}
|
19
|
+
export class Job {
|
20
|
+
id;
|
21
|
+
name;
|
22
|
+
executionDate;
|
23
|
+
gracePeriod;
|
24
|
+
params;
|
25
|
+
status;
|
26
|
+
constructor({ id, name, execution_date, grace_period, params, status, }) {
|
27
|
+
const row = validateJobRow({
|
28
|
+
id,
|
29
|
+
name,
|
30
|
+
execution_date,
|
31
|
+
grace_period,
|
32
|
+
params,
|
33
|
+
status,
|
34
|
+
});
|
35
|
+
this.id = row.id;
|
36
|
+
this.name = row.name;
|
37
|
+
this.executionDate = row.execution_date;
|
38
|
+
this.gracePeriod = row.grace_period;
|
39
|
+
this.params = row.params;
|
40
|
+
this.status = row.status;
|
41
|
+
}
|
42
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import { Job } from "./jobs.js";
|
2
|
+
declare class PNScheduler {
|
3
|
+
#private;
|
4
|
+
constructor();
|
5
|
+
verbose: boolean;
|
6
|
+
jobRegistry: Record<string, Function>;
|
7
|
+
defineJob(name: string, func: Function): void;
|
8
|
+
/**
|
9
|
+
* Schedules a job to be executed at a specific date and time, with specific parameters.
|
10
|
+
*
|
11
|
+
* @param name The name of the job, as defined with the "defineJob" method.
|
12
|
+
* @param executionDate The date and time at which to run the job.
|
13
|
+
* @param params The params (as an object) to pass to the job function.
|
14
|
+
* @param gracePeriod The grace period past the execution date (in seconds) before a job is marked as "skipped". Defaults to null.
|
15
|
+
*/
|
16
|
+
scheduleJob(name: string, executionDate: Date, params?: any, gracePeriod?: number | null): Promise<Job>;
|
17
|
+
unscheduleJob(job: Job): Promise<void>;
|
18
|
+
executeJob(job: Job): Promise<void>;
|
19
|
+
findDueJobs(): Promise<Job[]>;
|
20
|
+
findScheduledJobs(): Promise<Job[]>;
|
21
|
+
/**
|
22
|
+
* Starts the scheduler and sets up a recurring interval to scan for and run scheduled jobs.
|
23
|
+
*
|
24
|
+
* @param checkFrequency The frequency (in seconds) at which to check for scheduled jobs. Defaults to 1.
|
25
|
+
*/
|
26
|
+
start(checkFrequency?: number): void;
|
27
|
+
stop(): Promise<void>;
|
28
|
+
reset(): Promise<void>;
|
29
|
+
}
|
30
|
+
declare const _default: PNScheduler;
|
31
|
+
export default _default;
|
@@ -0,0 +1,125 @@
|
|
1
|
+
import * as db from "./db/index.js";
|
2
|
+
import { Job } from "./jobs.js";
|
3
|
+
class PNScheduler {
|
4
|
+
constructor() { }
|
5
|
+
verbose = false; // When true, print logs to console
|
6
|
+
jobRegistry = {};
|
7
|
+
#interval;
|
8
|
+
#log(message) {
|
9
|
+
if (this.verbose) {
|
10
|
+
console.log(message);
|
11
|
+
}
|
12
|
+
}
|
13
|
+
defineJob(name, func) {
|
14
|
+
this.#log(`Defining job "${name}"...`);
|
15
|
+
this.jobRegistry[name] = func;
|
16
|
+
}
|
17
|
+
/**
|
18
|
+
* Schedules a job to be executed at a specific date and time, with specific parameters.
|
19
|
+
*
|
20
|
+
* @param name The name of the job, as defined with the "defineJob" method.
|
21
|
+
* @param executionDate The date and time at which to run the job.
|
22
|
+
* @param params The params (as an object) to pass to the job function.
|
23
|
+
* @param gracePeriod The grace period past the execution date (in seconds) before a job is marked as "skipped". Defaults to null.
|
24
|
+
*/
|
25
|
+
async scheduleJob(name, executionDate, params, gracePeriod = null) {
|
26
|
+
this.#log(`Scheduling job "${name}" to be executed at ${executionDate}`);
|
27
|
+
if (!this.jobRegistry[name]) {
|
28
|
+
throw new Error(`Job "${name}" is not defined — before scheduling a job, define it with the "defineJob" method`);
|
29
|
+
}
|
30
|
+
const res = await db.query(`
|
31
|
+
INSERT INTO pnscheduler.jobs (name, execution_date, grace_period, params)
|
32
|
+
VALUES ($1, $2, $3, $4)
|
33
|
+
RETURNING *
|
34
|
+
`, [name, executionDate, gracePeriod, JSON.stringify(params)]);
|
35
|
+
const jobRow = res.rows[0];
|
36
|
+
return new Job(jobRow);
|
37
|
+
}
|
38
|
+
async unscheduleJob(job) {
|
39
|
+
this.#log(`Unscheduling job ${job.name} with id ${job.id}...`);
|
40
|
+
await db.query(`DELETE FROM pnscheduler.jobs WHERE id = $1`, [job.id]);
|
41
|
+
}
|
42
|
+
async executeJob(job) {
|
43
|
+
this.#log(`Executing job "${job.name}" with id ${job.id}...`);
|
44
|
+
const func = this.jobRegistry[job.name];
|
45
|
+
if (!func) {
|
46
|
+
console.error(`Failed execution of job "${job.name}" — job is not defined`);
|
47
|
+
await this.#markJobStatusAs(job, "failed");
|
48
|
+
return;
|
49
|
+
}
|
50
|
+
try {
|
51
|
+
await func(job.params);
|
52
|
+
}
|
53
|
+
catch (err) {
|
54
|
+
console.error(`Error executing job "${job.name}" with id ${job.id}:`, err);
|
55
|
+
await this.#markJobStatusAs(job, "failed");
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
await this.#markJobStatusAs(job, "executed");
|
59
|
+
}
|
60
|
+
async #markJobStatusAs(job, status) {
|
61
|
+
this.#log(`Marking job "${job.name}" with id ${job.id} as ${status}...`);
|
62
|
+
await db.query(`UPDATE pnscheduler.jobs SET status = $1 WHERE id = $2`, [
|
63
|
+
status,
|
64
|
+
job.id,
|
65
|
+
]);
|
66
|
+
}
|
67
|
+
async findDueJobs() {
|
68
|
+
this.#log("Finding due jobs...");
|
69
|
+
const res = await db.query(`
|
70
|
+
WITH updated_jobs AS (
|
71
|
+
UPDATE pnscheduler.jobs
|
72
|
+
SET status = 'skipped'
|
73
|
+
WHERE status = 'pending'
|
74
|
+
AND execution_date <= NOW()
|
75
|
+
AND grace_period IS NOT NULL
|
76
|
+
AND execution_date + (grace_period * INTERVAL '1 second') < NOW()
|
77
|
+
)
|
78
|
+
SELECT *
|
79
|
+
FROM pnscheduler.jobs
|
80
|
+
WHERE status = 'pending'
|
81
|
+
AND execution_date <= NOW()
|
82
|
+
AND (
|
83
|
+
grace_period IS NULL
|
84
|
+
OR execution_date + (grace_period * INTERVAL '1 second') >= NOW()
|
85
|
+
);
|
86
|
+
`);
|
87
|
+
return res.rows.map((job) => new Job(job));
|
88
|
+
}
|
89
|
+
async findScheduledJobs() {
|
90
|
+
this.#log("Finding scheduled jobs...");
|
91
|
+
const res = await db.query(`
|
92
|
+
SELECT *
|
93
|
+
FROM pnscheduler.jobs
|
94
|
+
WHERE status = 'pending'
|
95
|
+
`);
|
96
|
+
return res.rows.map((job) => new Job(job));
|
97
|
+
}
|
98
|
+
async #scanAndRun() {
|
99
|
+
const jobs = await this.findDueJobs();
|
100
|
+
this.#log(`Found ${jobs.length} scheduled jobs: ${jobs
|
101
|
+
.map((job) => job.name)
|
102
|
+
.join(", ")}`);
|
103
|
+
for (const job of jobs) {
|
104
|
+
await this.executeJob(job);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
/**
|
108
|
+
* Starts the scheduler and sets up a recurring interval to scan for and run scheduled jobs.
|
109
|
+
*
|
110
|
+
* @param checkFrequency The frequency (in seconds) at which to check for scheduled jobs. Defaults to 1.
|
111
|
+
*/
|
112
|
+
start(checkFrequency = 1) {
|
113
|
+
this.#log("Starting scheduler...");
|
114
|
+
this.#interval = setInterval(this.#scanAndRun.bind(this), 1000 * checkFrequency);
|
115
|
+
}
|
116
|
+
async stop() {
|
117
|
+
this.#log("Stopping scheduler...");
|
118
|
+
clearInterval(this.#interval);
|
119
|
+
}
|
120
|
+
async reset() {
|
121
|
+
this.#log("Resetting scheduler...");
|
122
|
+
await db.query("DELETE FROM pnscheduler.jobs");
|
123
|
+
}
|
124
|
+
}
|
125
|
+
export default new PNScheduler();
|
package/package.json
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
{
|
2
|
+
"name": "pnscheduler",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"description": "Persistent NodeJS Scheduler - A simple job scheduler for Node.js applications, with Postgres-based persistence.",
|
5
|
+
"keywords": [
|
6
|
+
"node",
|
7
|
+
"nodejs",
|
8
|
+
"scheduler",
|
9
|
+
"postgres",
|
10
|
+
"postgresql",
|
11
|
+
"persistent",
|
12
|
+
"job"
|
13
|
+
],
|
14
|
+
"homepage": "https://github.com/nilmus/pnscheduler",
|
15
|
+
"repository": {
|
16
|
+
"type": "git",
|
17
|
+
"url": "git://github.com/nilmus/pnscheduler.git",
|
18
|
+
"directory": "packages/pg"
|
19
|
+
},
|
20
|
+
"license": "MIT",
|
21
|
+
"author": "Nil Mustard",
|
22
|
+
"type": "module",
|
23
|
+
"main": "dist/index.js",
|
24
|
+
"types": "dist/index.d.ts",
|
25
|
+
"files": [
|
26
|
+
"dist",
|
27
|
+
"README.md",
|
28
|
+
"LICENSE"
|
29
|
+
],
|
30
|
+
"scripts": {
|
31
|
+
"build": "tsc",
|
32
|
+
"prepublishOnly": "npm run build",
|
33
|
+
"test": "vitest",
|
34
|
+
"migrate": "node --env-file=.env src/db/migrationScript.ts",
|
35
|
+
"start": "node --env-file=.env index.ts"
|
36
|
+
},
|
37
|
+
"dependencies": {
|
38
|
+
"pg": "^8.16.3"
|
39
|
+
},
|
40
|
+
"devDependencies": {
|
41
|
+
"@types/pg": "^8.15.5",
|
42
|
+
"typescript": "^5.9.3",
|
43
|
+
"vitest": "^3.2.4"
|
44
|
+
}
|
45
|
+
}
|