@@ -5,7 +5,7 @@ import type { PrismaClient } from "@trigger.dev/database";
55import { Worker , type JobHandlerParams } from "@trigger.dev/redis-worker" ;
66import { calculateDistributedExecutionTime } from "./distributedScheduling.js" ;
77import {
8- calculateNextNominalTimestamp ,
8+ calculateNextSchedulableOccurrence ,
99 nextScheduledTimestamps ,
1010 previousScheduledTimestamp ,
1111} from "./scheduleCalculation.js" ;
@@ -15,11 +15,7 @@ import type {
1515 TriggerScheduledTaskCallback ,
1616 TriggerScheduleParams ,
1717} from "./types.js" ;
18- import {
19- calculateEffectiveScheduleTime ,
20- calculateSchedulePhase ,
21- type NormalizedScheduleWindow ,
22- } from "./scheduleTiming.js" ;
18+ import { calculateSchedulePhase , type NormalizedScheduleWindow } from "./scheduleTiming.js" ;
2319import { scheduleWorkerCatalog } from "./workerCatalog.js" ;
2420import { tryCatch } from "@trigger.dev/core/utils" ;
2521
@@ -217,33 +213,29 @@ export class ScheduleEngine {
217213 ) ;
218214 span . setAttribute ( "schedule_phase" , schedulePhase ) ;
219215
220- const fromTimestamp = params . fromTimestamp ?? new Date ( ) ;
216+ const registrationTime = new Date ( ) ;
217+ const fromTimestamp = params . fromTimestamp ?? registrationTime ;
221218 span . setAttribute ( "from_timestamp" , fromTimestamp . toISOString ( ) ) ;
222219
223- const nominalAt = calculateNextNominalTimestamp (
224- instance . taskSchedule . generatorExpression ,
225- instance . taskSchedule . timezone ,
226- fromTimestamp
227- ) ;
228- const nextNominalAt = calculateNextNominalTimestamp (
229- instance . taskSchedule . generatorExpression ,
230- instance . taskSchedule . timezone ,
231- nominalAt
232- ) ;
233220 const {
234- effectiveAt : candidateEffectiveAt ,
221+ nominalAt,
222+ candidateEffectiveAt,
223+ effectiveAt,
235224 effectiveRangeMs,
236225 windowMs,
237226 offsetMs : candidateDelayMs ,
238227 intervalMs,
239228 windowWasCappedToInterval,
240- } = calculateEffectiveScheduleTime ( {
241- nominalAt,
242- nextNominalAt,
229+ skippedExpiredOccurrences,
230+ } = calculateNextSchedulableOccurrence ( {
231+ schedule : instance . taskSchedule . generatorExpression ,
232+ timezone : instance . taskSchedule . timezone ,
233+ afterNominal : fromTimestamp ,
234+ now : registrationTime ,
243235 schedulePhase,
244236 window : scheduleWindow ,
237+ cronSpreadEnabled : this . options . cronSpreadEnabled ,
245238 } ) ;
246- const effectiveAt = this . options . cronSpreadEnabled ? candidateEffectiveAt : nominalAt ;
247239 const appliedDelayMs = effectiveAt . getTime ( ) - nominalAt . getTime ( ) ;
248240
249241 span . setAttribute ( "cron_spread_enabled" , this . options . cronSpreadEnabled ) ;
@@ -256,6 +248,14 @@ export class ScheduleEngine {
256248 span . setAttribute ( "schedule_window_ms" , windowMs ) ;
257249 span . setAttribute ( "effective_range_ms" , effectiveRangeMs ) ;
258250 span . setAttribute ( "schedule_window_was_capped_to_interval" , windowWasCappedToInterval ) ;
251+ span . setAttribute ( "schedule_expired_occurrences_skipped" , skippedExpiredOccurrences ) ;
252+
253+ if ( skippedExpiredOccurrences ) {
254+ span . addEvent ( "schedule_expired_occurrences_skipped" , {
255+ from_nominal_time : fromTimestamp . toISOString ( ) ,
256+ selected_nominal_time : nominalAt . toISOString ( ) ,
257+ } ) ;
258+ }
259259
260260 if ( windowWasCappedToInterval ) {
261261 span . addEvent ( "schedule_window_capped_to_interval" , {
@@ -268,7 +268,7 @@ export class ScheduleEngine {
268268 } ) ;
269269 }
270270
271- const schedulingDelayMs = effectiveAt . getTime ( ) - Date . now ( ) ;
271+ const schedulingDelayMs = effectiveAt . getTime ( ) - registrationTime . getTime ( ) ;
272272 span . setAttribute ( "scheduling_delay_ms" , schedulingDelayMs ) ;
273273
274274 this . logger . debug ( "Calculated next schedule timestamps" , {
@@ -283,6 +283,7 @@ export class ScheduleEngine {
283283 appliedDelayMs,
284284 effectiveRangeMs,
285285 windowWasCappedToInterval,
286+ skippedExpiredOccurrences,
286287 schedulingDelayMs,
287288 generatorExpression : instance . taskSchedule . generatorExpression ,
288289 timezone : instance . taskSchedule . timezone ,
@@ -674,8 +675,9 @@ export class ScheduleEngine {
674675 } ) ;
675676 }
676677
677- // Register the next run. `fromTimestamp` advances on every tick so
678- // the next cron slot keeps marching forward even through skips.
678+ // Register the next run. `fromTimestamp` anchors nominal chaining;
679+ // registration preserves an upcoming effective occurrence and skips
680+ // expired intermediate ticks after downtime.
679681 // `lastScheduleTime` is the actual previous fire time the next job
680682 // will report as `payload.lastTimestamp` — only advance it when we
681683 // actually triggered, otherwise carry forward the existing value so
0 commit comments