11import 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
1298export 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