44 SCHEDULE_PHASE_DENOMINATOR ,
55 calculateEffectiveScheduleTime ,
66 calculateSchedulePhase ,
7+ resolveScheduleWindow ,
78} from "@internal/schedule-engine" ;
89import { CronPattern } from "~/v3/schedules" ;
9- import { type NormalizedScheduleWindow } from "@trigger.dev/core/v3" ;
1010import { parseExpression } from "cron-parser" ;
1111import { describe , expect , it } from "vitest" ;
1212import {
@@ -77,24 +77,48 @@ function referenceResolve(
7777 deduplicationKey : input . deduplicationKey ,
7878 } ) ;
7979
80- const window : NormalizedScheduleWindow | undefined =
81- input . windowPercentage !== null
82- ? { type : "percentage" , percentage : input . windowPercentage }
83- : input . windowDurationSeconds !== null
84- ? { type : "duration" , durationSeconds : input . windowDurationSeconds }
85- : undefined ;
80+ const window = resolveScheduleWindow ( {
81+ windowDurationSeconds : input . windowDurationSeconds ,
82+ windowPercentage : input . windowPercentage ,
83+ defaultWindowDurationSeconds : input . defaultWindowDurationSeconds ,
84+ } ) . window ;
8685
8786 const { effectiveAt } = calculateEffectiveScheduleTime ( {
8887 nominalAt : nominalTimes [ 0 ] ,
8988 nextNominalAt : nominalTimes [ 1 ] ,
9089 schedulePhase : phase ,
9190 window,
91+ minimumWindowDurationSeconds : input . minimumWindowDurationSeconds ,
9292 } ) ;
9393
9494 let lastRun : Date | undefined ;
9595 if ( includeLastRun && input . active ) {
9696 try {
97- const previous = previousScheduledTimestamp ( input . cron , input . timezone , now ) ;
97+ const latestNominal = previousScheduledTimestamp (
98+ input . cron ,
99+ input . timezone ,
100+ new Date ( now . getTime ( ) + 1 )
101+ ) ;
102+ const previousNominal = previousScheduledTimestamp (
103+ input . cron ,
104+ input . timezone ,
105+ latestNominal
106+ ) ;
107+ const latestEffective = calculateEffectiveScheduleTime ( {
108+ nominalAt : latestNominal ,
109+ nextNominalAt : nominalTimes [ 0 ] ,
110+ schedulePhase : phase ,
111+ window,
112+ minimumWindowDurationSeconds : input . minimumWindowDurationSeconds ,
113+ } ) . effectiveAt ;
114+ const previousEffective = calculateEffectiveScheduleTime ( {
115+ nominalAt : previousNominal ,
116+ nextNominalAt : latestNominal ,
117+ schedulePhase : phase ,
118+ window,
119+ minimumWindowDurationSeconds : input . minimumWindowDurationSeconds ,
120+ } ) . effectiveAt ;
121+ const previous = latestEffective <= now ? latestEffective : previousEffective ;
98122 lastRun = previous . getTime ( ) > input . updatedAt . getTime ( ) ? previous : undefined ;
99123 } catch {
100124 lastRun = undefined ;
@@ -255,15 +279,24 @@ describe("resolveScheduleTimings", () => {
255279 } ) ;
256280
257281 it ( "skips lastRun when the previous slot predates the last config change" , ( ) => {
258- const [ stale ] = resolveScheduleTimings ( [ input ( { cron : "0 0 * * *" , updatedAt : now } ) ] , {
259- phaseSecret : PHASE_SECRET ,
260- includeLastRun : true ,
261- now,
262- } ) ;
282+ const [ stale ] = resolveScheduleTimings (
283+ [ input ( { cron : "0 0 * * *" , schedulePhase : 0 , updatedAt : now } ) ] ,
284+ {
285+ phaseSecret : PHASE_SECRET ,
286+ includeLastRun : true ,
287+ now,
288+ }
289+ ) ;
263290 expect ( stale . lastRun ) . toBeUndefined ( ) ;
264291
265292 const [ fresh ] = resolveScheduleTimings (
266- [ input ( { cron : "0 0 * * *" , updatedAt : new Date ( "2020-01-01T00:00:00.000Z" ) } ) ] ,
293+ [
294+ input ( {
295+ cron : "0 0 * * *" ,
296+ schedulePhase : 0 ,
297+ updatedAt : new Date ( "2020-01-01T00:00:00.000Z" ) ,
298+ } ) ,
299+ ] ,
267300 { phaseSecret : PHASE_SECRET , includeLastRun : true , now }
268301 ) ;
269302 expect ( fresh . lastRun ) . toEqual ( new Date ( "2024-06-15T00:00:00.000Z" ) ) ;
@@ -282,7 +315,7 @@ describe("resolveScheduleTimings", () => {
282315 } ) ;
283316
284317 it ( "resolves lastRun for a valid expression" , ( ) => {
285- const [ valid ] = resolveScheduleTimings ( [ input ( { cron : "0 0 * * *" } ) ] , {
318+ const [ valid ] = resolveScheduleTimings ( [ input ( { cron : "0 0 * * *" , schedulePhase : 0 } ) ] , {
286319 phaseSecret : PHASE_SECRET ,
287320 includeLastRun : true ,
288321 now,
@@ -291,6 +324,56 @@ describe("resolveScheduleTimings", () => {
291324 expect ( valid . lastRun ) . toEqual ( new Date ( "2024-06-15T00:00:00.000Z" ) ) ;
292325 } ) ;
293326
327+ it ( "resolves lastRun from the most recent effective window time" , ( ) => {
328+ const [ beforeCurrentWindow ] = resolveScheduleTimings (
329+ [
330+ input ( {
331+ cron : "0 * * * *" ,
332+ schedulePhase : SCHEDULE_PHASE_DENOMINATOR / 2 ,
333+ windowPercentage : 100 ,
334+ } ) ,
335+ ] ,
336+ {
337+ phaseSecret : PHASE_SECRET ,
338+ includeLastRun : true ,
339+ now,
340+ }
341+ ) ;
342+ const [ afterCurrentWindow ] = resolveScheduleTimings (
343+ [
344+ input ( {
345+ cron : "0 * * * *" ,
346+ schedulePhase : SCHEDULE_PHASE_DENOMINATOR / 2 ,
347+ windowPercentage : 100 ,
348+ } ) ,
349+ ] ,
350+ {
351+ phaseSecret : PHASE_SECRET ,
352+ includeLastRun : true ,
353+ now : new Date ( "2024-06-15T09:40:00.000Z" ) ,
354+ }
355+ ) ;
356+
357+ expect ( beforeCurrentWindow . lastRun ) . toEqual ( new Date ( "2024-06-15T08:30:00.000Z" ) ) ;
358+ expect ( afterCurrentWindow . lastRun ) . toEqual ( new Date ( "2024-06-15T09:30:00.000Z" ) ) ;
359+ } ) ;
360+
361+ it ( "compares updatedAt with the effective window time" , ( ) => {
362+ const [ timing ] = resolveScheduleTimings (
363+ [
364+ input ( {
365+ cron : "0 * * * *" ,
366+ schedulePhase : SCHEDULE_PHASE_DENOMINATOR / 2 ,
367+ windowPercentage : 100 ,
368+ updatedAt : new Date ( "2024-06-15T08:15:00.000Z" ) ,
369+ } ) ,
370+ ] ,
371+ { phaseSecret : PHASE_SECRET , includeLastRun : true , now }
372+ ) ;
373+
374+ expect ( timing . lastRun ) . toEqual ( new Date ( "2024-06-15T08:30:00.000Z" ) ) ;
375+ } ) ;
376+
294377 it ( "honours a caller-supplied schedulePhase over the derived one" , ( ) => {
295378 const rows = [ input ( { schedulePhase : 0 , windowPercentage : 100 , cron : "0 * * * *" } ) ] ;
296379
0 commit comments