From 956f64988d864e54a4eb646eba00ba661b67acde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:19:38 +0000 Subject: [PATCH 1/4] Fix: clamp negative setTimeout delay to 0 for allowPast with far-past dates Agent-Logs-Url: https://github.com/Hexagon/croner/sessions/e95ad1b0-1f4e-41a1-98a6-8a308ac6f36c Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com> --- src/croner.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/croner.ts b/src/croner.ts index ecdc5e9..0094428 100644 --- a/src/croner.ts +++ b/src/croner.ts @@ -499,6 +499,13 @@ class Cron { waitMs = maxDelay; } + // Clamp negative delays to 0 - some runtimes (e.g. Deno) treat large negative values + // as large positive delays due to 32-bit integer overflow, causing jobs with allowPast:true + // and a far-past date to never fire. + if (waitMs < 0) { + waitMs = 0; + } + // Start the timer loop // _checkTrigger will either call _trigger (if it's time, croner isn't paused and whatever), // or recurse back to this function to wait for next trigger From 5e06563e53c9ff1517f909fd2a5f8f11bc5c4f2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:40:46 +0000 Subject: [PATCH 2/4] Add 1s backoff when paused + negative delay to prevent tight rescheduling loop Agent-Logs-Url: https://github.com/Hexagon/croner/sessions/d4fd67cd-7705-4339-a1a7-fc03a7a191e7 Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com> --- src/croner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/croner.ts b/src/croner.ts index 0094428..816a2d3 100644 --- a/src/croner.ts +++ b/src/croner.ts @@ -501,9 +501,9 @@ class Cron { // Clamp negative delays to 0 - some runtimes (e.g. Deno) treat large negative values // as large positive delays due to 32-bit integer overflow, causing jobs with allowPast:true - // and a far-past date to never fire. + // and a far-past date to never fire. Use a backoff when paused to avoid a tight loop. if (waitMs < 0) { - waitMs = 0; + waitMs = this._states.paused ? 1000 : 0; } // Start the timer loop From 9d3132902c19e2edd0dbf6bb72d9dacc864ac426 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 00:25:47 +0000 Subject: [PATCH 3/4] Add regression test for paused + allowPast + far-past fire-once job with resume Agent-Logs-Url: https://github.com/Hexagon/croner/sessions/8e0fb2f9-4e69-4243-9a40-4f5434f51569 Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com> --- test/croner.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/croner.test.ts b/test/croner.test.ts index ac9aeff..a9c0234 100644 --- a/test/croner.test.ts +++ b/test/croner.test.ts @@ -1159,6 +1159,31 @@ test( }), ); +test( + "Fire-once job with allowPast: true, paused: true, and far-past date should fire after resume", + //@ts-ignore + timeout(4000, (resolve) => { + let fired = false; + const veryOldDate = new Date("2020-01-01T00:00:00"); + const job = new Cron(veryOldDate, { allowPast: true, paused: true }, () => { + fired = true; + }); + + // Should not fire while paused + setTimeout(() => { + assertEquals(fired, false); + job.resume(); + + // After resume, should fire quickly + setTimeout(() => { + assertEquals(fired, true); + job.stop(); + resolve(); + }, 2000); + }, 100); + }), +); + test("Fire-once job with no allowPast and date > 1s in past should have null nextRun and not be running", function () { // A once-job 3 seconds in the past without allowPast should silently not schedule const pastTime = new Date(Date.now() - 3000); From b45c1471e36fe9fa031e185748b37d5b1fc24ec7 Mon Sep 17 00:00:00 2001 From: Hexagon Date: Fri, 3 Apr 2026 11:36:25 +0200 Subject: [PATCH 4/4] Update test/croner.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/croner.test.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/croner.test.ts b/test/croner.test.ts index a9c0234..5a8a26d 100644 --- a/test/croner.test.ts +++ b/test/croner.test.ts @@ -1164,21 +1164,33 @@ test( //@ts-ignore timeout(4000, (resolve) => { let fired = false; + let resumed = false; + let settled = false; + let watchdog: ReturnType | undefined; const veryOldDate = new Date("2020-01-01T00:00:00"); const job = new Cron(veryOldDate, { allowPast: true, paused: true }, () => { fired = true; + + if (!resumed || settled) return; + + settled = true; + if (watchdog) clearTimeout(watchdog); + job.stop(); + assertEquals(fired, true); + resolve(); }); // Should not fire while paused setTimeout(() => { assertEquals(fired, false); + resumed = true; job.resume(); - // After resume, should fire quickly - setTimeout(() => { - assertEquals(fired, true); + watchdog = setTimeout(() => { + if (settled) return; + settled = true; job.stop(); - resolve(); + assertEquals(fired, true); }, 2000); }, 100); }),