Skip to content

Commit 6fe629c

Browse files
committed
fix: correct scheduled rebalance next run display
1 parent ab04013 commit 6fe629c

File tree

1 file changed

+116
-1
lines changed

1 file changed

+116
-1
lines changed

src/lib/timeUtils.ts

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
1360
let timeOffset: number | null = null;
1461
let 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

Comments
 (0)