Skip to content

Commit 0f9705e

Browse files
committed
feat: implement caching
1 parent a8d0b2a commit 0f9705e

File tree

3 files changed

+192
-60
lines changed

3 files changed

+192
-60
lines changed

src/content.tsx

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createRoot } from "react-dom/client";
55
import browser from "webextension-polyfill";
66
import { Button } from "./components/Button";
77
import { icsToJson } from "./utils/icsToJson";
8+
import type { ICalEvent } from "./utils/icsToJson";
89

910
interface TimeSlot {
1011
time: string;
@@ -15,18 +16,6 @@ interface ScheduleData {
1516
availableSlots: TimeSlot[];
1617
}
1718

18-
const isTimeInRange = (
19-
time: string,
20-
startTime: Date,
21-
endTime: Date,
22-
scheduleDate: Date
23-
): boolean => {
24-
const [hours, minutes] = time.split(":").map(Number);
25-
const timeDate = new Date(scheduleDate);
26-
timeDate.setHours(hours, minutes, 0, 0);
27-
return timeDate >= startTime && timeDate < endTime;
28-
};
29-
3019
const applyCalendarEvents = async (): Promise<void> => {
3120
// Get saved settings
3221
interface StorageData {
@@ -151,55 +140,93 @@ const applyCalendarEvents = async (): Promise<void> => {
151140
console.warn("No calendar events found from any source");
152141
}
153142

143+
const eventsByDate = new Map<string, ICalEvent[]>();
144+
for (const event of events) {
145+
const existing = eventsByDate.get(event.dateKey);
146+
if (existing) {
147+
existing.push(event);
148+
} else {
149+
eventsByDate.set(event.dateKey, [event]);
150+
}
151+
}
152+
153+
for (const dateEvents of eventsByDate.values()) {
154+
dateEvents.sort((a, b) => a.startTimestamp - b.startTimestamp);
155+
}
156+
154157
// Process each schedule
155158
for (const schedule of result) {
156159
const scheduleDate = new Date(schedule.date);
157160
const dayOfWeek = scheduleDate.getDay();
161+
const processedSlots = new Set<number>();
158162

159-
for (const slot of schedule.availableSlots) {
163+
const slotEntries = schedule.availableSlots.map((slot) => {
160164
const [hours, minutes] = slot.time.split(":").map(Number);
161-
const slotTime = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
165+
const slotDate = new Date(schedule.date);
166+
slotDate.setHours(hours, minutes, 0, 0);
167+
return {
168+
time: slot.time,
169+
timestamp: slotDate.getTime(),
170+
};
171+
});
162172

173+
for (const entry of slotEntries) {
163174
const shouldMarkBusy =
164-
// Weekend check
165175
(autoDeclineWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) ||
166-
// Working hours check (only if enforced)
167176
(shouldEnforceWorkingHours &&
168-
(slotTime < workStartTime || slotTime >= workEndTime));
177+
(entry.time < workStartTime || entry.time >= workEndTime));
169178

170-
if (shouldMarkBusy) {
171-
markBusyTimeSlots(new Date(`${schedule.date}T${slot.time}`));
179+
if (shouldMarkBusy && !processedSlots.has(entry.timestamp)) {
180+
processedSlots.add(entry.timestamp);
181+
markBusyTimeSlots(new Date(entry.timestamp));
172182
}
173183
}
174184

175-
// If it's a weekend and autoDeclineWeekends is enabled, skip calendar processing
176185
if (autoDeclineWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) {
177186
continue;
178187
}
179188

180-
// Process calendar events more efficiently
181-
const dateEvents = events.filter((event) => {
182-
const eventDate = new Date(event.startDate).toISOString().split("T")[0];
183-
return eventDate === schedule.date;
184-
});
185-
const busyTimeSlots = schedule.availableSlots.filter((slot) => {
186-
const [hours, minutes] = slot.time.split(":").map(Number);
187-
const slotDate = new Date(schedule.date);
188-
slotDate.setHours(hours, minutes, 0, 0);
189+
const dateEvents = eventsByDate.get(schedule.date) ?? [];
189190

190-
// Check if the slot overlaps with any event
191-
return dateEvents.some((event) => {
192-
const eventStart = new Date(event.startDate);
193-
const eventEnd = new Date(event.endDate);
194-
return isTimeInRange(slot.time, eventStart, eventEnd, slotDate);
195-
});
191+
if (dateEvents.length === 0) {
192+
console.warn("No calendar events found for", schedule.date);
193+
continue;
194+
}
195+
196+
const busySlotEntries = slotEntries.filter((entry) => {
197+
if (processedSlots.has(entry.timestamp)) {
198+
return false;
199+
}
200+
201+
for (const event of dateEvents) {
202+
if (event.endTimestamp <= entry.timestamp) {
203+
continue;
204+
}
205+
206+
if (event.startTimestamp > entry.timestamp) {
207+
break;
208+
}
209+
210+
if (
211+
entry.timestamp >= event.startTimestamp &&
212+
entry.timestamp < event.endTimestamp
213+
) {
214+
return true;
215+
}
216+
}
217+
218+
return false;
196219
});
197220

198-
if (busyTimeSlots.length === 0) {
221+
if (busySlotEntries.length === 0) {
199222
console.log("No available time slots found");
200223
} else {
201-
for (const slot of busyTimeSlots) {
202-
markBusyTimeSlots(new Date(`${schedule.date}T${slot.time}`));
224+
for (const entry of busySlotEntries) {
225+
if (processedSlots.has(entry.timestamp)) {
226+
continue;
227+
}
228+
processedSlots.add(entry.timestamp);
229+
markBusyTimeSlots(new Date(entry.timestamp));
203230
}
204231
}
205232
}

src/options.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,12 @@ import { createRoot } from "react-dom/client";
33
import browser from "webextension-polyfill";
44
import { Button } from "./components/Button";
55
import { icsToJson } from "./utils/icsToJson";
6-
7-
interface CalendarEvent {
8-
summary: string;
9-
description: string | null;
10-
location: string | null;
11-
startDate: string;
12-
endDate: string;
13-
status: string;
14-
}
6+
import type { ICalEvent } from "./utils/icsToJson";
157

168
interface CalendarSource {
179
url: string;
1810
cacheTimestamp: string;
19-
events: CalendarEvent[];
11+
events: ICalEvent[];
2012
}
2113

2214
function Options() {

src/utils/icsToJson.ts

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,122 @@
11
import ICAL from "ical.js";
22

3-
interface ICalEvent {
3+
export interface ICalEvent {
44
summary: string;
55
description: string | null;
66
location: string | null;
77
startDate: string;
88
endDate: string;
99
status: string;
10+
startTimestamp: number;
11+
endTimestamp: number;
12+
dateKey: string;
13+
}
14+
15+
type JCalData = ReturnType<typeof ICAL.parse>;
16+
17+
interface RangeCacheEntry {
18+
events: ICalEvent[];
19+
lastUsed: number;
20+
}
21+
22+
interface CalendarCacheEntry {
23+
jcalData: JCalData;
24+
rangeCache: Map<string, RangeCacheEntry>;
25+
lastUsed: number;
26+
}
27+
28+
const MAX_CALENDAR_CACHE_SIZE = 3;
29+
const MAX_RANGE_CACHE_SIZE = 6;
30+
31+
const calendarCache = new Map<string, CalendarCacheEntry>();
32+
33+
function getCalendarCacheEntry(icsData: string): CalendarCacheEntry {
34+
let entry = calendarCache.get(icsData);
35+
36+
if (!entry) {
37+
const jcalData = ICAL.parse(icsData);
38+
entry = {
39+
jcalData,
40+
rangeCache: new Map(),
41+
lastUsed: Date.now(),
42+
};
43+
calendarCache.set(icsData, entry);
44+
45+
if (calendarCache.size > MAX_CALENDAR_CACHE_SIZE) {
46+
let oldestKey: string | undefined;
47+
let oldest = Number.POSITIVE_INFINITY;
48+
49+
for (const [key, value] of calendarCache.entries()) {
50+
if (value.lastUsed < oldest) {
51+
oldest = value.lastUsed;
52+
oldestKey = key;
53+
}
54+
}
55+
56+
if (oldestKey !== undefined) {
57+
calendarCache.delete(oldestKey);
58+
}
59+
}
60+
} else {
61+
entry.lastUsed = Date.now();
62+
}
63+
64+
return entry;
65+
}
66+
67+
function getRangeCache(entry: CalendarCacheEntry, rangeKey: string) {
68+
const rangeEntry = entry.rangeCache.get(rangeKey);
69+
if (!rangeEntry) return null;
70+
rangeEntry.lastUsed = Date.now();
71+
return rangeEntry.events;
72+
}
73+
74+
function setRangeCache(
75+
entry: CalendarCacheEntry,
76+
rangeKey: string,
77+
events: ICalEvent[]
78+
) {
79+
entry.rangeCache.set(rangeKey, { events, lastUsed: Date.now() });
80+
81+
if (entry.rangeCache.size > MAX_RANGE_CACHE_SIZE) {
82+
let oldestKey: string | undefined;
83+
let oldest = Number.POSITIVE_INFINITY;
84+
85+
for (const [key, value] of entry.rangeCache.entries()) {
86+
if (value.lastUsed < oldest) {
87+
oldest = value.lastUsed;
88+
oldestKey = key;
89+
}
90+
}
91+
92+
if (oldestKey !== undefined) {
93+
entry.rangeCache.delete(oldestKey);
94+
}
95+
}
1096
}
1197

1298
export function icsToJson(
1399
icsData: string,
14100
startDate?: Date,
15101
endDate?: Date
16102
): ICalEvent[] {
17-
const jcalData = ICAL.parse(icsData);
18-
const comp = new ICAL.Component(jcalData);
19-
const events = comp.getAllSubcomponents("vevent");
20-
const result: ICalEvent[] = [];
103+
const cacheEntry = getCalendarCacheEntry(icsData);
21104

22105
// Default date range: 30 days from today
23106
const rangeStart = startDate || new Date();
24107
const rangeEnd = endDate || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
108+
const rangeStartTime = rangeStart.getTime();
109+
const rangeEndTime = rangeEnd.getTime();
110+
const rangeKey = `${rangeStartTime}-${rangeEndTime}`;
111+
112+
const cachedRange = getRangeCache(cacheEntry, rangeKey);
113+
if (cachedRange) {
114+
return cachedRange;
115+
}
116+
117+
const comp = new ICAL.Component(cacheEntry.jcalData);
118+
const events = comp.getAllSubcomponents("vevent");
119+
const result: ICalEvent[] = [];
25120

26121
for (const event of events) {
27122
const icalEvent = new ICAL.Event(event);
@@ -31,30 +126,39 @@ export function icsToJson(
31126
// Expand recurring events within the date range
32127
const iterator = icalEvent.iterator();
33128
let occurrence = iterator.next();
129+
const duration = icalEvent.endDate.subtractDate(icalEvent.startDate);
34130

35131
while (occurrence) {
36132
const occurrenceDate = occurrence.toJSDate();
133+
const occurrenceStart = occurrenceDate.getTime();
37134

38135
// Stop if we've gone past the end date
39-
if (occurrenceDate > rangeEnd) {
136+
if (occurrenceStart > rangeEndTime) {
40137
break;
41138
}
42139

43140
// Only include occurrences within our date range
44-
if (occurrenceDate >= rangeStart) {
45-
const duration = icalEvent.endDate.subtractDate(icalEvent.startDate);
141+
if (occurrenceStart >= rangeStartTime) {
46142
const occurrenceEnd = occurrence.clone();
47143
occurrenceEnd.addDuration(duration);
144+
const occurrenceEndDate = occurrenceEnd.toJSDate();
145+
const occurrenceEndTime = occurrenceEndDate.getTime();
146+
const startDateIso = occurrenceDate.toISOString();
147+
const endDateIso = occurrenceEndDate.toISOString();
148+
const dateKey = startDateIso.split("T")[0];
48149

49150
result.push({
50151
summary: icalEvent.summary || "",
51152
description: icalEvent.description || null,
52153
location: icalEvent.location || null,
53-
startDate: occurrenceDate.toISOString(),
54-
endDate: occurrenceEnd.toJSDate().toISOString(),
154+
startDate: startDateIso,
155+
endDate: endDateIso,
55156
status: String(
56157
event.getFirstPropertyValue("status") || "CONFIRMED"
57158
),
159+
startTimestamp: occurrenceStart,
160+
endTimestamp: occurrenceEndTime,
161+
dateKey,
58162
});
59163
}
60164

@@ -63,20 +167,29 @@ export function icsToJson(
63167
} else {
64168
// Handle non-recurring events
65169
const eventStartDate = icalEvent.startDate.toJSDate();
170+
const eventEndDate = icalEvent.endDate.toJSDate();
171+
const eventStartTime = eventStartDate.getTime();
66172

67173
// Only include events within our date range
68-
if (eventStartDate >= rangeStart && eventStartDate <= rangeEnd) {
174+
if (eventStartTime >= rangeStartTime && eventStartTime <= rangeEndTime) {
175+
const startDateIso = eventStartDate.toISOString();
176+
const dateKey = startDateIso.split("T")[0];
69177
result.push({
70178
summary: icalEvent.summary || "",
71179
description: icalEvent.description || null,
72180
location: icalEvent.location || null,
73181
startDate: icalEvent.startDate.toJSDate().toISOString(),
74-
endDate: icalEvent.endDate.toJSDate().toISOString(),
182+
endDate: eventEndDate.toISOString(),
75183
status: String(event.getFirstPropertyValue("status") || "CONFIRMED"),
184+
startTimestamp: eventStartTime,
185+
endTimestamp: eventEndDate.getTime(),
186+
dateKey,
76187
});
77188
}
78189
}
79190
}
80191

192+
setRangeCache(cacheEntry, rangeKey, result);
193+
81194
return result;
82195
}

0 commit comments

Comments
 (0)