Repository: kelektiv/node-cron (ts)

Issue:
Update repo, tests, and workflow to support Node 22
### ⭐ Suggestion

Node 16 is at [end-of-life](https://github.com/nodejs/node/blob/main/CHANGELOG.md) so we should adjust our workflow to test on versions 18, 20, and 22 instead of 16, 28, and 20. it's possible some of our base code and tests will need to be updated as well

### 💻 Use Cases

_No response_

### ❌ Related Problems

_No response_
v4 Roadmap - breaking changes
This is the list of breaking changes that will be completed as part of the v4 release. So far there is no official timeline; this issue is mostly just to organize breaking changes that we want to make _eventually_.

- [x] Drop support for all but the latest 3 stable Node versions (in v3 we're planning on supporting v16 and even versions to the latest stable version)
- [x] Rename the `job.running` property to something like `job.isJobRunning` and either make it private or document it (so it is harder to confuse with [the new `job.isCallbackRunning` property](https://github.com/kelektiv/node-cron/pull/894#discussion_r1826902492))


Pull request:
refactor!: drop support for Node v16 and rename job.running to job.isActive
BREAKING CHANGES:

- Node v16 is no longer supported
- `job.running` has been renamed to `job.isActive`

## Description

## Related Issue

fixes #902 
fixes #905 

## Motivation and Context

![image](https://github.com/user-attachments/assets/9aab2c77-e920-4f84-8ec5-c8eefce0859f)

## How Has This Been Tested?

Tests have been updated to use new API and all pass. Fuzz tests also pass.

## Screenshots (if appropriate):

## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [X] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [X] My code follows the code style of this project.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [X] If my change introduces a breaking change, I have added a `!` after the type/scope in the title (see the Conventional Commits standard).

<!--- Provide a general summary of your changes in the Title above (following the Conventional Commits standard) -->
<!-- More infos: https://www.conventionalcommits.org -->
<!-- Commit types: https://github.com/insurgent-lab/conventional-changelog-preset#commit-types-->


Patch:
diff --git a/.nvmrc b/.nvmrc
index 870bbe4..ffbd52c 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1,1 +1,1 @@
-stable
\ No newline at end of file
+23.8.0
diff --git a/README.md b/README.md
index 30a0b3d..a22150a 100644
--- a/README.md
+++ b/README.md
@@ -34,27 +34,42 @@ npm install cron
 
 1. [Features](#-features)
 2. [Installation](#-installation)
-3. [Migrating from v2 to v3](#-migrating-from-v2-to-v3)
+3. [Migrating](#-migrating)
 4. [Basic Usage](#-basic-usage)
-5. [Cron Patterns](#cron-patterns)
-   - [Cron Syntax Overview](#cron-patterns)
+5. [Cron Patterns](#-cron-patterns)
+   - [Cron Syntax Overview](#-cron-patterns)
    - [Supported Ranges](#supported-ranges)
-6. [Gotchas](#gotchas)
-7. [API](#api)
+6. [API](#-api)
    - [Standalone Functions](#standalone-functions)
    - [CronJob Class](#cronjob-class)
    - [CronTime Class](#crontime-class)
+7. [Gotchas](#-gotchas)
 8. [Community](#-community)
    - [Join the Community](#-community)
 9. [Contributing](#-contributing)
    - [General Contribution](#-contributing)
    - [Submitting Bugs/Issues](#-submitting-bugsissues)
 10. [Acknowledgements](#-acknowledgements)
-11. [License](#license)
+11. [License](#-license)
 
-## 🔄 Migrating from v2 to v3
+## ⬆ Migrating
 
-With the introduction of TypeScript in version 3 and alignment with UNIX cron patterns, a few changes have been made:
+v4 dropped Node v16 and renamed the `job.running` property:
+
+<details>
+  <summary>Migrating from v3 to v4</summary>
+
+### Dropped Node version
+
+Node v16 is no longer supported. Upgrade your Node installation to Node v18 or above
+
+### Property renamed and now read-only
+
+You can no longer set the `running` property (now `isActive`). It is read-only. To start or stop a cron job, use `job.start()` and `job.stop()`.
+
+</details>
+
+v3 introduced TypeScript and tighter Unix cron pattern alignment:
 
 <details>
   <summary>Migrating from v2 to v3</summary>
@@ -112,7 +127,7 @@ const job = CronJob.from({
 
 For more advanced examples, check the [examples directory](https://github.com/kelektiv/node-cron/tree/main/examples).
 
-## Cron Patterns
+## ⏰ Cron Patterns
 
 Cron patterns are the backbone of this library. Familiarize yourself with the syntax:
 
@@ -142,13 +157,7 @@ day of week    0-7 (0 or 7 is Sunday, or use names)
 > Names can also be used for the 'month' and 'day of week' fields. Use the first three letters of the particular day or month (case does not matter). Ranges and lists of names are allowed.  
 > Examples: "mon,wed,fri", "jan-mar".
 
-## Gotchas
-
-- Both JS `Date` and Luxon `DateTime` objects don't guarantee millisecond precision due to computation delays. This module excludes millisecond precision for standard cron syntax but allows execution date specification through JS `Date` or Luxon `DateTime` objects. However, specifying a precise future execution time, such as adding a millisecond to the current time, may not always work due to these computation delays. It's observed that delays less than 4-5 ms might lead to inconsistencies. While we could limit all date granularity to seconds, we've chosen to allow greater precision but advise users of potential issues.
-
-- Using arrow functions for `onTick` binds them to the parent's `this` context. As a result, they won't have access to the cronjob's `this` context. You can read a little more in issue [#47 (comment)](https://github.com/kelektiv/node-cron/issues/47#issuecomment-459762775).
-
-## API
+## 📖 API
 
 ### Standalone Functions
 
@@ -220,6 +229,8 @@ day of week    0-7 (0 or 7 is Sunday, or use names)
 
 #### Properties
 
+- `isActive`: [READ-ONLY] Indicates if a job is active (checking to see if the callback needs to be called).
+
 - `isCallbackRunning`: [READ-ONLY] Indicates if a callback is currently executing.
 
   ```javascript
@@ -231,8 +242,8 @@ day of week    0-7 (0 or 7 is Sunday, or use names)
 
   console.log(job.isCallbackRunning); // false
   job.start();
-  console.log(job.running); // true (schedule is active)
-  console.log(job.isCallbackRunning); // false (no callback executing)
+  console.log(job.isActive); // true
+  console.log(job.isCallbackRunning); // false
   ```
 
 ### CronTime Class
@@ -247,6 +258,12 @@ day of week    0-7 (0 or 7 is Sunday, or use names)
 
 - `utcOffset`: [OPTIONAL] - Analogous to `utcOffset` from `CronJob` parameters.
 
+## 💢 Gotchas
+
+- Both JS `Date` and Luxon `DateTime` objects don't guarantee millisecond precision due to computation delays. This module excludes millisecond precision for standard cron syntax but allows execution date specification through JS `Date` or Luxon `DateTime` objects. However, specifying a precise future execution time, such as adding a millisecond to the current time, may not always work due to these computation delays. It's observed that delays less than 4-5 ms might lead to inconsistencies. While we could limit all date granularity to seconds, we've chosen to allow greater precision but advise users of potential issues.
+
+- Using arrow functions for `onTick` binds them to the parent's `this` context. As a result, they won't have access to the cronjob's `this` context. You can read a little more in issue [#47 (comment)](https://github.com/kelektiv/node-cron/issues/47#issuecomment-459762775).
+
 ## 🤝 Community
 
 Join the [Discord server](https://discord.gg/yyKns29zch)! Here you can discuss issues and get help in a more casual forum than GitHub.
@@ -265,6 +282,6 @@ This is a community effort project. In the truest sense, this project started as
 
 Special thanks to [Hiroki Horiuchi](https://github.com/horiuchi), [Lundarl Gholoi](https://github.com/winup) and [koooge](https://github.com/koooge) for their work on the [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) typings before they were imported in v2.4.0.
 
-## License
+## ⚖ License
 
 MIT
diff --git a/examples/is_job_running.js b/examples/is_job_running.js
index 453a980..9e53a5b 100644
--- a/examples/is_job_running.js
+++ b/examples/is_job_running.js
@@ -7,4 +7,4 @@ const job = new CronJob('* * * * * *', function () {
 });
 console.log('After job instantiation');
 job.start();
-console.log('is job running? ', job.running);
+console.log('is job active? ', job.isActive);
diff --git a/package-lock.json b/package-lock.json
index 7db45cc..45dfad3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,6 +41,9 @@
 				"ts-jest": "29.2.5",
 				"typescript": "5.7.2",
 				"typescript-eslint": "7.18.0"
+			},
+			"engines": {
+				"node": ">=18"
 			}
 		},
 		"node_modules/@ampproject/remapping": {
diff --git a/package.json b/package.json
index e1a0037..4ec3f25 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,9 @@
 		"Masakazu Matsushita <matsukaz@gmail.com> (matsukaz)",
 		"Christopher Lunt <me@kirisu.co.uk> (https://github.com/kirisu)"
 	],
+	"engines": {
+		"node": ">=18.x"
+	},
 	"files": [
 		"dist/**/*.js",
 		"dist/**/*.d.ts",
diff --git a/src/job.ts b/src/job.ts
index 730d926..00bb078 100644
--- a/src/job.ts
+++ b/src/job.ts
@@ -13,7 +13,6 @@ import {
 
 export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 	cronTime: CronTime;
-	running = false;
 	unrefTimeout = false;
 	lastExecution: Date | null = null;
 	runOnce = false;
@@ -24,10 +23,15 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 	waitForCompletion = false;
 	errorHandler?: CronJobParams<OC, C>['errorHandler'];
 
+	private _isActive = false;
 	private _isCallbackRunning = false;
 	private _timeout?: NodeJS.Timeout;
 	private _callbacks: CronCallback<C, WithOnComplete<OC>>[] = [];
 
+	get isActive() {
+		return this._isActive;
+	}
+
 	get isCallbackRunning() {
 		return this._isCallbackRunning;
 	}
@@ -202,7 +206,7 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 			throw new CronError('time must be an instance of CronTime.');
 		}
 
-		const wasRunning = this.running;
+		const wasRunning = this._isActive;
 		this.stop();
 
 		this.cronTime = time;
@@ -245,7 +249,7 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 	}
 
 	start() {
-		if (this.running) return;
+		if (this._isActive) return;
 
 		const MAXDELAY = 2147483647; // The maximum number of milliseconds setTimeout will wait.
 		let timeout = this.cronTime.getTimeout();
@@ -293,7 +297,7 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 				// We have arrived at the correct point in time.
 				this.lastExecution = new Date();
 
-				this.running = false;
+				this._isActive = false;
 
 				// start before calling back so the callbacks have the ability to stop the cron job
 				if (!this.runOnce) this.start();
@@ -303,7 +307,7 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 		};
 
 		if (timeout >= 0) {
-			this.running = true;
+			this._isActive = true;
 
 			// Don't try to sleep more than MAXDELAY ms at a time.
 
@@ -343,7 +347,7 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 	 */
 	stop() {
 		if (this._timeout) clearTimeout(this._timeout);
-		this.running = false;
+		this._isActive = false;
 
 		if (!this.waitForCompletion) {
 			void this._executeOnComplete();


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a712388..ec70a88 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -28,7 +28,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        node: [16, 18, 20, 22]
+        node: [18, 20, 22, 23]
 
     runs-on: ${{ matrix.os }}
     timeout-minutes: 5
diff --git a/tests/cron.fuzz.ts b/tests/cron.fuzz.ts
index 6dad222..64662f8 100644
--- a/tests/cron.fuzz.ts
+++ b/tests/cron.fuzz.ts
@@ -48,9 +48,9 @@ function testCronJob(
 			unrefTimeout
 		);
 
-		expect(job.running).toBe(start);
+		expect(job.isActive).toBe(start);
 		job.stop();
-		expect(job.running).toBe(false);
+		expect(job.isActive).toBe(false);
 
 		expect(job.cronTime.source).toBe(cronTime);
 	} catch (error) {
diff --git a/tests/cron.test.ts b/tests/cron.test.ts
index aec84c6..1de92ff 100644
--- a/tests/cron.test.ts
+++ b/tests/cron.test.ts
@@ -630,7 +630,8 @@ describe('cron', () => {
 
 	it('should not throw if at least one time is valid', () => {
 		expect(() => {
-			new CronJob('0 0 30 JAN,FEB *', callback, null, true);
+			const job = new CronJob('0 0 30 JAN,FEB *', callback, null, true);
+			job.stop();
 		}).not.toThrow();
 	});
 
@@ -1021,7 +1022,7 @@ describe('cron', () => {
 			clock.tick(60000);
 
 			expect(callback).toHaveBeenCalledTimes(1);
-			expect(job.running).toBe(false);
+			expect(job.isActive).toBe(false);
 		});
 	});
 
@@ -1331,7 +1332,7 @@ describe('cron', () => {
 
 			expect(isJobCompleted).toBe(true);
 			expect(job.isCallbackRunning).toBe(false);
-			expect(job.running).toBe(false);
+			expect(job.isActive).toBe(false);
 		});
 	});