@@ -9,6 +9,53 @@ interface WorldTimeAPIResponse {
99 week_number : number ;
1010}
1111
12+ const WEEKDAY_NAMES = [ 'Sun' , 'Mon' , 'Tue' , 'Wed' , 'Thu' , 'Fri' , 'Sat' ] ;
13+ const MS_IN_DAY = 24 * 60 * 60 * 1000 ;
14+
15+ interface TimezoneDateInfo {
16+ year : number ;
17+ month : number ;
18+ day : number ;
19+ weekday : number ;
20+ }
21+
22+ const getIntlFormatter = ( timezone : string ) => new Intl . DateTimeFormat ( 'en-US' , {
23+ timeZone : timezone ,
24+ year : 'numeric' ,
25+ month : '2-digit' ,
26+ day : '2-digit' ,
27+ weekday : 'short' ,
28+ } ) ;
29+
30+ function getTimezoneDateInfo ( date : Date , timezone : string ) : TimezoneDateInfo {
31+ const parts = getIntlFormatter ( timezone ) . formatToParts ( date ) ;
32+ const getPart = ( type : string ) => parts . find ( p => p . type === type ) ?. value ;
33+
34+ const year = parseInt ( getPart ( 'year' ) ?? '1970' , 10 ) ;
35+ const month = parseInt ( getPart ( 'month' ) ?? '01' , 10 ) ;
36+ const day = parseInt ( getPart ( 'day' ) ?? '01' , 10 ) ;
37+ const weekdayName = getPart ( 'weekday' ) ?? 'Sun' ;
38+ const weekday = WEEKDAY_NAMES . indexOf ( weekdayName ) ;
39+
40+ return {
41+ year,
42+ month,
43+ day,
44+ weekday : weekday === - 1 ? 0 : weekday ,
45+ } ;
46+ }
47+
48+ function addDaysUTC ( date : Date , days : number ) : Date {
49+ const result = new Date ( date ) ;
50+ result . setUTCDate ( result . getUTCDate ( ) + days ) ;
51+ return result ;
52+ }
53+
54+ function getWeekStartDayNumber ( info : TimezoneDateInfo ) : number {
55+ const dayNumber = Math . floor ( Date . UTC ( info . year , info . month - 1 , info . day ) / MS_IN_DAY ) ;
56+ return dayNumber - info . weekday ;
57+ }
58+
1259// Cache for storing fetched time and calculating offset
1360let timeOffset : number | null = null ;
1461let lastFetchTime : number | null = null ;
@@ -117,6 +164,59 @@ export async function createScheduledDateUTC(
117164 ) ) ;
118165}
119166
167+ async function findNextWeeklyRun (
168+ now : Date ,
169+ schedule : {
170+ day_of_week : number [ ] ;
171+ timezone : string ;
172+ interval_value : number ;
173+ created_at : string ;
174+ last_executed_at ?: string ;
175+ } ,
176+ hours : number ,
177+ minutes : number
178+ ) : Promise < Date | null > {
179+ if ( ! schedule . day_of_week . length ) return null ;
180+
181+ const sortedDays = [ ...new Set ( schedule . day_of_week ) ] . sort ( ( a , b ) => a - b ) ;
182+ const currentInfo = getTimezoneDateInfo ( now , schedule . timezone ) ;
183+
184+ const anchorSource = schedule . last_executed_at
185+ ? new Date ( schedule . last_executed_at )
186+ : new Date ( schedule . created_at ) ;
187+ const anchorInfo = getTimezoneDateInfo ( anchorSource , schedule . timezone ) ;
188+ const anchorWeekStart = getWeekStartDayNumber ( anchorInfo ) ;
189+
190+ const applicableInterval = Math . max ( 1 , schedule . interval_value ) ;
191+ const maxWeeksToCheck = Math . max ( applicableInterval * 4 , 8 ) ;
192+
193+ for ( let weekOffset = 0 ; weekOffset < maxWeeksToCheck ; weekOffset ++ ) {
194+ for ( const targetDay of sortedDays ) {
195+ const dayDelta = ( ( targetDay - currentInfo . weekday + 7 ) % 7 ) + weekOffset * 7 ;
196+ const candidateBase = addDaysUTC ( now , dayDelta ) ;
197+ const candidate = await createScheduledDateUTC ( candidateBase , hours , minutes , schedule . timezone ) ;
198+
199+ if ( candidate <= now ) {
200+ continue ;
201+ }
202+
203+ if ( applicableInterval > 1 ) {
204+ const candidateInfo = getTimezoneDateInfo ( candidate , schedule . timezone ) ;
205+ const candidateWeekStart = getWeekStartDayNumber ( candidateInfo ) ;
206+ const weeksDiff = Math . floor ( ( candidateWeekStart - anchorWeekStart ) / 7 ) ;
207+
208+ if ( weeksDiff < 0 || weeksDiff % applicableInterval !== 0 ) {
209+ continue ;
210+ }
211+ }
212+
213+ return candidate ;
214+ }
215+ }
216+
217+ return null ;
218+ }
219+
120220/**
121221 * Calculates the next run time for a schedule using true UTC time
122222 */
@@ -130,13 +230,28 @@ export async function calculateNextRunUTC(
130230 day_of_month ?: number [ ] ;
131231 enabled : boolean ;
132232 last_executed_at ?: string ;
233+ created_at : string ;
133234 }
134235) : Promise < Date | null > {
135236 if ( ! schedule . enabled ) return null ;
136237
137238 // Get true UTC time
138239 const now = await getTrueUTCTime ( ) ;
139240 const [ hours , minutes ] = schedule . time_of_day . split ( ':' ) . map ( Number ) ;
241+
242+ if ( schedule . interval_unit === 'weeks' && schedule . day_of_week && schedule . day_of_week . length > 0 ) {
243+ const nextWeeklyRun = await findNextWeeklyRun ( now , {
244+ day_of_week : schedule . day_of_week ,
245+ timezone : schedule . timezone ,
246+ interval_value : schedule . interval_value ,
247+ created_at : schedule . created_at ,
248+ last_executed_at : schedule . last_executed_at ,
249+ } , hours , minutes ) ;
250+
251+ if ( nextWeeklyRun ) {
252+ return nextWeeklyRun ;
253+ }
254+ }
140255
141256 // If never executed, calculate from current date
142257 if ( ! schedule . last_executed_at ) {
@@ -199,4 +314,4 @@ export async function calculateNextRunUTC(
199314 }
200315
201316 return nextRun ;
202- }
317+ }
0 commit comments