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 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
@@ -0,0 +1,2 @@
1
+ export declare const query: (text: string, params?: any[]) => Promise<import("pg").QueryResult<any>>;
2
+ export declare const closePool: () => Promise<void>;
@@ -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 {};
@@ -0,0 +1,8 @@
1
+ import * as db from "./index.js";
2
+ import { migrationQuery } from "./migration.js";
3
+ (async () => {
4
+ console.log("Running migration...");
5
+ await migrationQuery;
6
+ console.log("Migration complete!");
7
+ await db.closePool();
8
+ })();
@@ -0,0 +1,3 @@
1
+ import scheduler from "./scheduler.js";
2
+ export default scheduler;
3
+ export type { Job } from "./jobs.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import scheduler from "./scheduler.js";
2
+ export default scheduler;
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
+ }