super-simple-scheduler
v1.9.1
Published
A simple scheduler for Node.js
Downloads
2,620
Maintainers
Readme
Super Simple Scheduler
A lightweight and easy-to-use job scheduler for Node.js with support for repeated jobs, retries, and multiple storage backends.
GitHub: https://github.com/ajhollid/super-simple-scheduler
Features
- Schedule jobs with optional repeat intervals
- Fire-and-forget dispatch -- long-running jobs never block the scheduling of other jobs
- Configurable concurrency limit
- Automatic retry on failure with configurable max retries
- Simple API to add job templates and jobs
- Written in TypeScript with type definitions
- Job pausing, resuming, and removal
- Fully typed event emitter for lifecycle monitoring with intellisense
- Fast in-memory storage backend
- Graceful shutdown with in-flight job draining
- Async/await API throughout
- ES Module support
Installation
npm install super-simple-schedulerTypeScript Support
The package includes full TypeScript support with type definitions. You can import the SchedulerOptions type for better intellisense:
import { Scheduler, SchedulerOptions } from "super-simple-scheduler";
const options: SchedulerOptions = {
processEvery: 1000,
concurrency: 10,
};
const scheduler = new Scheduler(options);Quick Start
import { Scheduler, SchedulerOptions } from "super-simple-scheduler";
// Create a scheduler instance
const scheduler = new Scheduler({
processEvery: 1000, // Process jobs every 1 second
concurrency: 10, // Max concurrent jobs (default: 10)
});
// Add a job template
await scheduler.addTemplate("sendEmail", async (data) => {
// Your job logic here
console.log("Sending email to:", data.recipient);
});
// Add a job
await scheduler.addJob({
template: "sendEmail",
repeat: 60000, // Run every minute
data: { recipient: "[email protected]" },
});
// Start the scheduler
await scheduler.start();API Reference
Scheduler Class
The main scheduler class that manages job execution.
Constructor
new Scheduler(options: SchedulerOptions)Parameters:
options.processEvery(optional): Interval in milliseconds to process jobs. Default: 1000options.concurrency(optional): Maximum number of jobs that can run concurrently. Default: 10
Example:
const scheduler = new Scheduler({
processEvery: 5000, // Process every 5 seconds
concurrency: 20, // Allow up to 20 concurrent jobs
});Methods
start(): Promise<boolean>
Starts the scheduler and begins processing jobs at the configured interval.
Returns: Promise<boolean> - true if started successfully
Example:
const success = await scheduler.start();
if (success) {
console.log("Scheduler started");
}stop(): Promise<boolean>
Stops the scheduler. Clears the processing interval, then waits for any in-flight jobs to complete before closing the store.
Returns: Promise<boolean> - true if stopped successfully
Example:
const success = await scheduler.stop();
if (success) {
console.log("Scheduler stopped");
}addTemplate(name: string, template: Function): Promise<boolean>
Registers a job template function that can be referenced by jobs.
Parameters:
name: Unique identifier for the templatetemplate: Function to execute when the job runs. Can be async or sync.
Returns: Promise<boolean> - true if template was added successfully
Example:
await scheduler.addTemplate("processData", async (data) => {
// Process the data
await processUserData(data);
});
await scheduler.addTemplate("sendNotification", (data) => {
// Send notification
sendPushNotification(data.userId, data.message);
});addJob(options): Promise<boolean>
Adds a new job to the scheduler.
Parameters:
options.id(optional): Unique identifier for the job. If not provided, a UUID will be generatedoptions.template: Name of the template to use for this joboptions.repeat(optional): Interval in milliseconds between job executions. If null, job runs onceoptions.data(optional): Data to pass to the job template functionoptions.active(optional): Whether the job should be active. Default: trueoptions.startAt(optional): Timestamp when the job should start running. If not provided, job runs immediately
Returns: Promise<boolean> - true if job was added or updated successfully
Note: If a job with the same ID already exists, it will be updated with the new properties instead of failing.
Example:
// One-time job
await scheduler.addJob({
template: "sendWelcomeEmail",
data: { userId: "123", email: "[email protected]" },
});
// Repeating job
await scheduler.addJob({
id: "daily-cleanup",
template: "cleanupDatabase",
repeat: 24 * 60 * 60 * 1000, // 24 hours
data: { tables: ["logs", "temp_data"] },
});
// Job with delayed start
await scheduler.addJob({
template: "sendReminder",
startAt: Date.now() + 60000, // Start in 1 minute
repeat: 60 * 60 * 1000, // Then every hour
data: { message: "Don't forget!" },
});
// Inactive job (will be paused)
await scheduler.addJob({
template: "maintenance",
repeat: 60 * 60 * 1000, // 1 hour
active: false,
});pauseJob(id: string | number): Promise<boolean>
Pauses a job, preventing it from executing.
Parameters:
id: Job identifier
Returns: Promise<boolean> - true if job was paused successfully, false if job not found
Example:
const success = await scheduler.pauseJob("daily-cleanup");
if (success) {
console.log("Job paused");
}resumeJob(id: string | number): Promise<boolean>
Resumes a paused job, allowing it to execute again.
Parameters:
id: Job identifier
Returns: Promise<boolean> - true if job was resumed successfully, false if job not found
Example:
const success = await scheduler.resumeJob("daily-cleanup");
if (success) {
console.log("Job resumed");
}removeJob(id: string | number): Promise<boolean>
Removes a job from the scheduler.
Parameters:
id: Job identifier
Returns: Promise<boolean> - true if job was removed successfully, false if job not found
Example:
const success = await scheduler.removeJob("daily-cleanup");
if (success) {
console.log("Job removed");
}getJob(id: string | number): Promise<IJob | null>
Returns a specific job by ID.
Parameters:
id: Job identifier
Returns: Promise<IJob | null> - Job object or null if not found
Example:
const job = await scheduler.getJob("daily-cleanup");
if (job) {
console.log(
`Job ${job.id}: ${job.template} (${job.active ? "active" : "paused"})`
);
}getJobs(): Promise<IJob[]>
Returns an array of all jobs in the scheduler.
Returns: Promise<IJob[]> - Array of job objects
Example:
const jobs = await scheduler.getJobs();
console.log(`Scheduler has ${jobs.length} jobs`);
jobs.forEach((job) => {
console.log(
`Job ${job.id}: ${job.template} (${job.active ? "active" : "paused"})`
);
});flushJobs(): Promise<boolean>
Removes all jobs from the scheduler.
Returns: Promise<boolean> - true if jobs were flushed successfully
Example:
const success = await scheduler.flushJobs();
if (success) {
console.log("All jobs removed");
}updateJob(id: string | number, updates: Partial<IJob>): Promise<boolean>
Updates a job with new properties.
Parameters:
id: Job identifierupdates: Partial job object with properties to update
Returns: Promise<boolean> - true if job was updated successfully, false if job not found
Example:
const success = await scheduler.updateJob("daily-cleanup", {
repeat: 12 * 60 * 60 * 1000, // 12 hours
data: { newData: "updated" },
});
if (success) {
console.log("Job updated");
}Job Interface
Jobs have the following structure:
interface IJob {
id: string | number; // Unique identifier
template: string; // Template name
data?: unknown; // Data passed to template function
repeat?: number; // Interval in milliseconds (undefined for one-time)
maxRetries?: number; // Maximum retry attempts (default: 3)
active: boolean; // Whether job is active
startAt?: number | null; // Timestamp when job should start running
lastRunAt?: number | null; // Timestamp of last execution
lastFinishedAt?: number | null; // Timestamp of last completion
lockedAt?: number | null; // Timestamp when job is being processed
lastFailedAt?: number | null; // Timestamp of last failure
lastFailReason?: string | null; // Reason for last failure
failCount?: number; // Number of failures
runCount?: number; // Number of successful runs
}Events
The scheduler extends EventEmitter with fully typed events. All event names and their callback signatures are defined in the SchedulerEvents interface, giving you full intellisense and compile-time safety.
Scheduler Lifecycle Events
| Event | Payload | Description |
|-------|---------|-------------|
| scheduler:start | none | Emitted when the scheduler starts |
| scheduler:stop | none | Emitted when the scheduler begins stopping |
| scheduler:drain | (count: number) | Emitted when stop is waiting for in-flight jobs to complete |
| scheduler:error | (error: Error) | Emitted on configuration or dispatch errors (e.g. missing template, job not found) |
Job Lifecycle Events
| Event | Payload | Description |
|-------|---------|-------------|
| job:start | (job: IJob) | Emitted before a job begins execution |
| job:attempt | (job: IJob, attempt: number) | Emitted before each attempt (including retries) |
| job:complete | (job: IJob) | Emitted when a job succeeds |
| job:fail | (job: IJob, error: unknown, attempt: number) | Emitted on each failed attempt |
| job:exhausted | (job: IJob, error: unknown) | Emitted when a job fails all retry attempts |
| job:abort | (job: IJob, reason: string) | Emitted when a job is removed during execution |
Example:
// Scheduler lifecycle
scheduler.on("scheduler:start", () => {
console.log("Scheduler started");
});
scheduler.on("scheduler:stop", () => {
console.log("Scheduler stopping");
});
scheduler.on("scheduler:drain", (count) => {
console.log(`Waiting for ${count} in-flight job(s) to finish`);
});
scheduler.on("scheduler:error", (error) => {
console.error("Scheduler error:", error.message);
});
// Job lifecycle
scheduler.on("job:start", (job) => {
console.log(`Job ${job.id} starting`);
});
scheduler.on("job:attempt", (job, attempt) => {
console.log(`Job ${job.id} attempt ${attempt}`);
});
scheduler.on("job:complete", (job) => {
console.log(`Job ${job.id} completed`);
});
scheduler.on("job:fail", (job, error, attempt) => {
console.warn(`Job ${job.id} failed attempt ${attempt}`);
});
scheduler.on("job:exhausted", (job, error) => {
console.error(`Job ${job.id} failed all retries`);
});
scheduler.on("job:abort", (job, reason) => {
console.log(`Job ${job.id} aborted: ${reason}`);
});Events are fully typed -- your editor will autocomplete event names and type-check callback parameters. Only events defined in SchedulerEvents are allowed; passing an invalid event name is a compile-time error.
Job Dispatch Model
The scheduler uses a fire-and-forget dispatch model. Each polling cycle, processJobs dispatches all ready jobs and returns immediately -- it does not wait for jobs to finish. Jobs execute in the background and are tracked via an internal running set.
This means a long-running job will never block the scheduling of other jobs. For example, if a job takes 30 seconds to complete and the scheduler polls every 1 second, all other jobs continue to be evaluated and dispatched on schedule during that time.
Concurrency
The concurrency option (default: 10) controls the maximum number of jobs that can execute simultaneously across all polling cycles. When the limit is reached, the scheduler waits for one job to finish before dispatching the next.
Graceful Shutdown
When stop() is called, the scheduler:
- Stops polling for new jobs
- Waits for all in-flight jobs to complete
- Closes the store
This ensures locks are released and job state is fully persisted before shutdown.
Storage
The scheduler uses a fast in-memory storage backend that's perfect for most use cases:
In-Memory Store
const scheduler = new Scheduler({});Pros:
- Fast and lightweight
- Minimal external dependencies
- Perfect for development and testing
- Simple setup and configuration
Cons:
- Jobs are lost on process restart
- Not suitable for production with multiple instances
Note: For production applications that need persistence or multiple instances, consider implementing your own storage backend or using a job queue system like Bull or Agenda.
Job Execution Logic
The scheduler uses a sophisticated job execution system that determines when jobs should run based on multiple factors:
Job Execution Conditions
A job will run when all of the following conditions are met:
- Active Status: Job is active (
active: true) - Not Locked: Job is not currently being executed (
lockedAt: null) - Start Time: If
startAtis set, the current time must be >=startAt - Execution History:
- If job has never run before (
lastRunAt: null), it runs immediately - If job is one-time (
repeat: null) and has already run, it doesn't run again - If job is repeating, enough time must have passed since the last run
- If job has never run before (
Timing Examples
// Immediate execution (default)
await scheduler.addJob({
template: "sendEmail",
data: { to: "[email protected]" },
});
// Runs on next processing cycle (within 1 second)
// Delayed start
await scheduler.addJob({
template: "sendEmail",
startAt: Date.now() + 60000, // Start in 1 minute
data: { to: "[email protected]" },
});
// Runs 1 minute from now
// One-time job
await scheduler.addJob({
template: "sendEmail",
// No repeat = one-time job
data: { to: "[email protected]" },
});
// Runs once, then never again
// Repeating job
await scheduler.addJob({
template: "sendEmail",
repeat: 60000, // Every minute
data: { to: "[email protected]" },
});
// Runs every minute
// Repeating job with delayed start
await scheduler.addJob({
template: "sendEmail",
startAt: Date.now() + 60000, // Start in 1 minute
repeat: 60000, // Then every minute
data: { to: "[email protected]" },
});
// Starts in 1 minute, then runs every minuteConfiguration
Processing Interval
The scheduler processes jobs at regular intervals. The default interval is 1 second, but you can modify it:
const scheduler = new Scheduler({
processEvery: 5000, // Process every 5 seconds
});Concurrency
Control the maximum number of simultaneously executing jobs:
const scheduler = new Scheduler({
concurrency: 5, // Only 5 jobs can run at once (default: 10)
});Retry Behavior
Jobs automatically retry on failure with a configurable maximum number of attempts:
await scheduler.addJob({
template: "unreliableTask",
repeat: 60000,
maxRetries: 5, // Will retry up to 5 times on failure
data: { task: "important" },
});Error Handling
The scheduler includes comprehensive error handling via typed events:
- Failed jobs are automatically retried up to the configured
maxRetries - Each failed attempt emits
job:failwith the job, error, and attempt number - Jobs that fail all retry attempts emit
job:exhaustedand are requeued for the next cycle - Configuration errors (missing templates, invalid job IDs) emit
scheduler:error - One-time jobs are removed after successful execution
- If a job is removed while it is being retried, execution is aborted and
job:abortis emitted
Examples
Email Scheduler
import { Scheduler } from "super-simple-scheduler";
const scheduler = new Scheduler({});
// Email template
await scheduler.addTemplate("sendEmail", async (data) => {
await sendEmail(data.to, data.subject, data.body);
});
// Daily digest
await scheduler.addJob({
id: "daily-digest",
template: "sendEmail",
repeat: 24 * 60 * 60 * 1000,
data: {
to: "[email protected]",
subject: "Daily Digest",
body: "Here is your daily summary...",
},
});
await scheduler.start();Data Processing Pipeline
const scheduler = new Scheduler({});
// Data processing templates
await scheduler.addTemplate("fetchData", async (data) => {
const rawData = await fetchFromAPI(data.endpoint);
await saveToDatabase(rawData);
});
await scheduler.addTemplate("processData", async (data) => {
const rawData = await getFromDatabase();
const processed = await transformData(rawData);
await saveProcessedData(processed);
});
// Schedule jobs
await scheduler.addJob({
id: "fetch-hourly",
template: "fetchData",
repeat: 60 * 60 * 1000,
data: { endpoint: "/api/data" },
});
await scheduler.addJob({
id: "process-daily",
template: "processData",
repeat: 24 * 60 * 60 * 1000,
});
await scheduler.start();Job Management Example
const scheduler = new Scheduler({});
// Add a job
await scheduler.addJob({
id: "test-job",
template: "testTemplate",
repeat: 5000,
data: { message: "Hello World" },
});
// Add a job with delayed start
await scheduler.addJob({
id: "delayed-job",
template: "testTemplate",
startAt: Date.now() + 10000, // Start in 10 seconds
repeat: 5000,
data: { message: "Delayed Hello World" },
});
// Update the same job (will update existing job instead of failing)
await scheduler.addJob({
id: "test-job",
template: "testTemplate",
repeat: 10000, // Changed from 5000 to 10000
data: { message: "Updated message" },
});
// Pause the job
await scheduler.pauseJob("test-job");
// Check job status
const job = await scheduler.getJob("test-job");
console.log(job?.active); // false
// Resume the job
await scheduler.resumeJob("test-job");
// Get all jobs
const allJobs = await scheduler.getJobs();
console.log(`Total jobs: ${allJobs.length}`);
// Update job
await scheduler.updateJob("test-job", {
repeat: 10000, // Change to 10 seconds
data: { message: "Updated message" },
});
// Remove job
await scheduler.removeJob("test-job");Development
Building
npm run buildTesting
# Run all tests
npm test
# Run tests with coverage
npm run test-coverageDevelopment Mode
npm run devLicense
MIT
