diff --git a/packages/features/calendar-cache/calendar-cache.repository.ts b/packages/features/calendar-cache/calendar-cache.repository.ts index 916e3fd819..f58913ea7c 100644 --- a/packages/features/calendar-cache/calendar-cache.repository.ts +++ b/packages/features/calendar-cache/calendar-cache.repository.ts @@ -11,30 +11,21 @@ import { getTimeMax, getTimeMin } from "./lib/datesForCache"; const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] }); -/** Enable or disable the expanded cache. Enabled by default. */ -const ENABLE_EXPANDED_CACHE = process.env.ENABLE_EXPANDED_CACHE !== "0"; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ONE_MONTH_IN_MS = 30 * MS_PER_DAY; const CACHING_TIME = ONE_MONTH_IN_MS; -export function parseKeyForCache(args: FreeBusyArgs): string { - const { timeMin: _timeMin, timeMax: _timeMax, items } = args; +function parseKeyForCache(args: FreeBusyArgs): string { // Ensure that calendarIds are unique - const uniqueItems = uniqueBy(items, ["id"]); - const { timeMin, timeMax } = handleMinMax(_timeMin, _timeMax); - const key = JSON.stringify({ timeMin, timeMax, items: uniqueItems }); + const uniqueItems = uniqueBy(args.items, ["id"]); + const key = JSON.stringify({ + timeMin: getTimeMin(args.timeMin), + timeMax: getTimeMax(args.timeMax), + items: uniqueItems, + }); return key; } -/** - * By expanding the cache to whole months, we can save round trips to the third party APIs. - * In this case we already have the data in the database, so we can just return it. - */ -function handleMinMax(min: string, max: string) { - if (!ENABLE_EXPANDED_CACHE) return { timeMin: min, timeMax: max }; - return { timeMin: getTimeMin(min), timeMax: getTimeMax(max) }; -} - type FreeBusyArgs = { timeMin: string; timeMax: string; items: { id: string }[] }; export class CalendarCacheRepository implements ICalendarCacheRepository { diff --git a/packages/features/calendar-cache/lib/datesForCache.test.ts b/packages/features/calendar-cache/lib/datesForCache.test.ts new file mode 100644 index 0000000000..0a5a0c9114 --- /dev/null +++ b/packages/features/calendar-cache/lib/datesForCache.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; + +import { getTimeMin, getTimeMax } from "./datesForCache"; + +describe("getTimeMin", () => { + // Tested on multiple dates + // vi.setSystemTime("2030-04-21T00:00:13Z"); + it("should return start of current month when no date is passed", () => { + const result = getTimeMin(); + const expected = new Date(); + expected.setUTCDate(1); + expected.setUTCHours(0, 0, 0, 0); + expect(result).toBe(expected.toISOString()); + }); + + it("should return start of month for a given date", () => { + const result = getTimeMin("2025-03-15T10:30:00Z"); + expect(result).toMatchInlineSnapshot(`"2025-03-01T00:00:00.000Z"`); + }); + + it("should handle dates at the start of month", () => { + const result = getTimeMin("2026-03-01T00:00:00Z"); + expect(result).toMatchInlineSnapshot(`"2026-03-01T00:00:00.000Z"`); + }); + + it("should handle DST changes", () => { + const result = getTimeMin("2024-10-27T01:30:00Z"); + expect(result).toMatchInlineSnapshot(`"2024-10-01T00:00:00.000Z"`); + }); +}); + +describe("getTimeMax", () => { + it("should return the start of the overnext month when no date is passed", () => { + const result = getTimeMax(); + const expected = new Date(); + expected.setUTCMonth(expected.getUTCMonth() + 2); + expected.setUTCDate(1); + expected.setUTCHours(0, 0, 0, 0); + expect(result).toBe(expected.toISOString()); + }); + + it("should return the start of overnext month for dates between start of current month and end of next month", () => { + const testDate = "2024-03-15T10:30:00Z"; + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2024-05-01T00:00:00.000Z"`); + }); + + it("should return end of month for dates beyond overnext month", () => { + const testDate = "2024-05-15T10:30:00Z"; + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2024-07-01T00:00:00.000Z"`); + }); + + it("should handle dates at the end of month", () => { + const testDate = "2024-03-31T23:59:59Z"; + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2024-05-01T00:00:00.000Z"`); + }); + + it("should handle October correctly (31 days)", () => { + const testDate = "2024-10-15T10:30:00Z"; + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2024-12-01T00:00:00.000Z"`); + }); + + it("should handle DST changes", () => { + const testDate = "2024-10-27T01:30:00Z"; // DST change in Europe + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2024-12-01T00:00:00.000Z"`); + }); + + it("should handle dates around DST changes in next month", () => { + const testDate = "2024-09-15T10:30:00Z"; // September, next month includes DST change + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2024-11-01T00:00:00.000Z"`); + }); +}); diff --git a/packages/features/calendar-cache/lib/datesForCache.ts b/packages/features/calendar-cache/lib/datesForCache.ts index 6eecb0db90..acad8ef769 100644 --- a/packages/features/calendar-cache/lib/datesForCache.ts +++ b/packages/features/calendar-cache/lib/datesForCache.ts @@ -1,16 +1,66 @@ /** Expand the start date to the beginning of the current month */ export const getTimeMin = (timeMin?: string) => { const date = timeMin ? new Date(timeMin) : new Date(); - date.setDate(1); - date.setHours(0, 0, 0, 0); - return date.toISOString(); + // Set to UTC to avoid timezone issues + const result = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)); + result.setUTCHours(0, 0, 0, 0); + return result.toISOString(); }; -/** Expand the end date to the end of the next month */ +/** + * Expand the end date to the start of the overnext month if date + * is between start of current month and end of next month, + * otherwise return start of overnext month from the passed date + * @example + * Today: March 15, 2024 ▼-------------▼ getTimeMax returns May 1st 00:00:00 + * Mar Apr May Jun + * ├────────┼────────┼────────┼────────┤ + * Current Month █████████| | | | + * Next Month | █████████| | | + **/ export function getTimeMax(timeMax?: string) { - const date = timeMax ? new Date(timeMax) : new Date(); - date.setMonth(date.getMonth() + 1); - date.setDate(0); - date.setHours(0, 0, 0, 0); - return date.toISOString(); + const now = new Date(); + const currentMonth = now.getUTCMonth(); + const currentYear = now.getUTCFullYear(); + + if (!timeMax) { + // If no date passed, return start of the overnext month from current date + const result = new Date(Date.UTC(currentYear, currentMonth + 2, 1)); + result.setUTCHours(0, 0, 0, 0); + return result.toISOString(); + } + + const date = new Date(timeMax); + const dateMonth = date.getUTCMonth(); + const dateYear = date.getUTCFullYear(); + + // If the date is within current month or next month, return start of overnext month + // Otherwise, return start of overnext month from the date + let targetYear = dateYear; + let targetMonth = dateMonth; + + // Check if date is within current month or next month + const isWithinCurrentOrNextMonth = + (dateYear === currentYear && dateMonth <= currentMonth + 1) || + (dateYear === currentYear + 1 && currentMonth === 11 && dateMonth === 0); + + if (isWithinCurrentOrNextMonth) { + // For dates within current month or next month, return start of overnext month + targetMonth = dateMonth + 2; + if (targetMonth > 11) { + targetMonth = 0; + targetYear++; + } + } else { + // For dates beyond overnext month, return start of overnext month from the date + targetMonth = dateMonth + 2; + if (targetMonth > 11) { + targetMonth = 0; + targetYear++; + } + } + + const result = new Date(Date.UTC(targetYear, targetMonth, 1)); + result.setUTCHours(0, 0, 0, 0); + return result.toISOString(); }