fix: calendar cache date mismtach (#20446)

* fix: calendar cache date mismtach

* Update tests
This commit is contained in:
Omar López
2025-03-29 17:00:26 -07:00
committed by GitHub
parent b2ac86c759
commit d80ed07093
3 changed files with 143 additions and 25 deletions
@@ -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 {
@@ -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"`);
});
});
@@ -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();
}