diff --git a/package-lock.json b/package-lock.json index 3b7328a3..e007b627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,7 +131,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -634,7 +633,6 @@ "integrity": "sha512-uuFKKpc7OtQM+6SRqT+a4kV818o1pS+uvv/gsRhyX7g4x495jg+Q7P0+O9VNGyLXBYP0syksS7gMRDJKcekr6A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@commitlint/format": "^20.4.0", "@commitlint/lint": "^20.4.1", @@ -1411,7 +1409,6 @@ "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "expect": "30.2.0", "jest-snapshot": "30.2.0" @@ -1467,7 +1464,6 @@ "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/expect": "30.2.0", @@ -1774,7 +1770,6 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -2016,7 +2011,6 @@ "integrity": "sha512-wdnBPHKkr9HhNhXOhZD5a2LNl91+hs8CC2vsAVYxtZH3y0dV3wKn+uZSN61rdJQZ8EGxzWB3inWocBHV9+u/CQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "conventional-changelog-angular": "^8.0.0", "conventional-changelog-writer": "^8.0.0", @@ -2409,7 +2403,6 @@ "integrity": "sha512-CcyDRk7xq+ON/20YNR+1I/jP7BYKICr1uKd1HHpROSnnTdGqOTburi4jcRiTYz0cpfhxSloQO3cGhnoot7IEkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "conventional-changelog-angular": "^8.0.0", "conventional-changelog-writer": "^8.0.0", @@ -2510,7 +2503,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -2869,7 +2861,6 @@ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2935,7 +2926,6 @@ "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.18.0", @@ -3793,7 +3783,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4136,7 +4125,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4764,7 +4752,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -5271,7 +5258,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5328,7 +5314,6 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6740,7 +6725,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -8145,7 +8129,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -10415,7 +10398,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11100,7 +11082,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11666,7 +11647,6 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -12900,7 +12880,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13029,7 +13008,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/job.ts b/src/job.ts index 4e301194..47ba5efd 100644 --- a/src/job.ts +++ b/src/job.ts @@ -373,6 +373,12 @@ export class CronJob { ); } + // one-shot jobs (realDate) have no "next" execution — stop instead of rescheduling + if (this.runOnce) { + this._isActive = false; + return; + } + timeout = this.cronTime.getTimeout(); setCronTimeout(timeout); } diff --git a/src/time.ts b/src/time.ts index 137ea42e..35fafdc8 100644 --- a/src/time.ts +++ b/src/time.ts @@ -144,10 +144,6 @@ export class CronTime { } if (this.realDate) { - if (DateTime.local() > date) { - throw new CronError('WARNING: Date in past. Will never be fired.'); - } - return date; } diff --git a/tests/cron.test.ts b/tests/cron.test.ts index 7c3e266d..143e05b6 100644 --- a/tests/cron.test.ts +++ b/tests/cron.test.ts @@ -461,6 +461,97 @@ describe('cron', () => { job.stop(); expect(callback).toHaveBeenCalledTimes(4); }); + + describe('with past date', () => { + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + it('should not throw when date is 10ms in the past', () => { + const d = new Date(); + const clock = sinon.useFakeTimers(d.getTime()); + const pastDate = new Date(d.getTime() - 10); + + expect(() => { + const job = new CronJob(pastDate, callback, null, true); + clock.tick(1000); + job.stop(); + }).not.toThrow(); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should execute immediately when past date is within threshold', () => { + const d = new Date(); + const clock = sinon.useFakeTimers(d.getTime()); + const pastDate = new Date(d.getTime() - 100); + + const job = new CronJob(pastDate, callback, null, true); + clock.tick(1000); + + // 100ms is within default 250ms threshold — should fire + expect(callback).toHaveBeenCalledTimes(1); + expect(job.isActive).toBe(false); + + const message = warnSpy.mock.calls[0][0]; + expect(message).toContain('Executing immediately'); + }); + + it('should skip execution when past date exceeds threshold', () => { + const d = new Date(); + const clock = sinon.useFakeTimers(d.getTime()); + const pastDate = new Date(d.getTime() - 1000); + + const job = new CronJob(pastDate, callback, null, true); + clock.tick(1000); + + // 1000ms exceeds default 250ms threshold — should skip + expect(callback).toHaveBeenCalledTimes(0); + expect(job.isActive).toBe(false); + + const message = warnSpy.mock.calls[0][0]; + expect(message).toContain('Skipping execution'); + }); + + it('should deactivate after handling past date (no infinite loop)', () => { + const d = new Date(); + const clock = sinon.useFakeTimers(d.getTime()); + const pastDate = new Date(d.getTime() - 50); + + const job = new CronJob(pastDate, callback, null, true); + + // tick well beyond the original date — must not reschedule + clock.tick(5000); + + // should only fire once (within threshold), not repeatedly + expect(callback).toHaveBeenCalledTimes(1); + expect(job.isActive).toBe(false); + }); + + it('should handle past date using CronJob.from()', () => { + const d = new Date(); + const clock = sinon.useFakeTimers(d.getTime()); + const pastDate = new Date(d.getTime() - 100); + + const job = CronJob.from({ + cronTime: pastDate, + onTick: callback, + start: true + }); + + clock.tick(1000); + + // within default threshold — fires and deactivates + expect(callback).toHaveBeenCalledTimes(1); + expect(job.isActive).toBe(false); + }); + }); }); describe('with timezone', () => { diff --git a/tests/crontime.test.ts b/tests/crontime.test.ts index fc57c035..f54daa62 100644 --- a/tests/crontime.test.ts +++ b/tests/crontime.test.ts @@ -670,14 +670,33 @@ describe('crontime', () => { expect(actual).toEqual(expected); }); - it('should detect real date in the past', () => { - const clock = sinon.useFakeTimers(); - const d = new Date(); - clock.tick(1000); - const time = new CronTime(d); - expect(() => { - time.sendAt(); - }).toThrow(); + describe('with real date in the past', () => { + it('should not throw from sendAt()', () => { + const clock = sinon.useFakeTimers(); + const d = new Date(); + clock.tick(1000); + const ct = new CronTime(d); + expect(() => { + ct.sendAt(); + }).not.toThrow(); + }); + + it('should return the original date from sendAt()', () => { + const clock = sinon.useFakeTimers(); + const d = new Date(); + clock.tick(1000); + const ct = new CronTime(d); + const result = ct.sendAt(); + expect(result.toMillis()).toEqual(d.getTime()); + }); + + it('should return a negative value from getTimeout()', () => { + const clock = sinon.useFakeTimers(); + const d = new Date(); + clock.tick(1000); + const ct = new CronTime(d); + expect(ct.getTimeout()).toBeLessThan(0); + }); }); it('should throw when providing both exclusive parameters timeZone and utcOffset', () => {