Merge branch 'refs/heads/master' into feat/operation-logs

* refs/heads/master:
  perf: lazy load ical.js to reduce initial bundle size
This commit is contained in:
Johannes Millan 2025-12-30 20:24:46 +01:00
commit c7d37ab773
6 changed files with 106 additions and 89 deletions

35
package-lock.json generated
View file

@ -1174,18 +1174,6 @@
}
}
},
"node_modules/@angular/build/node_modules/@types/node": {
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz",
@ -1199,15 +1187,6 @@
"vite": "^6.0.0 || ^7.0.0"
}
},
"node_modules/@angular/build/node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@angular/build/node_modules/vite": {
"version": "7.1.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
@ -15795,6 +15774,16 @@
"node": ">= 0.8"
}
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"dev": true,
@ -18677,7 +18666,7 @@
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@ -25281,7 +25270,7 @@
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/sanitize-filename": {

View file

@ -210,7 +210,7 @@ export class CalendarIntegrationService {
},
})
.pipe(
map((icalStrData) =>
switchMap((icalStrData) =>
getRelevantEventsForCalendarIntegrationFromIcal(
icalStrData,
calProvider.id,

View file

@ -5,7 +5,7 @@ describe('getRelevantEventsForCalendarIntegrationFromIcal - EXDATE Support', ()
const startTimestamp = new Date('2025-01-01T00:00:00Z').getTime();
const endTimestamp = new Date('2025-01-10T23:59:59Z').getTime();
it('should exclude date specified in EXDATE', () => {
it('should exclude date specified in EXDATE', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -19,7 +19,7 @@ SUMMARY:Daily with EXDATE
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -35,7 +35,7 @@ END:VCALENDAR`;
expect(eventDates).toEqual(['2025-01-01', '2025-01-03']);
});
it('should exclude multiple dates specified in multiple EXDATE properties', () => {
it('should exclude multiple dates specified in multiple EXDATE properties', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -50,7 +50,7 @@ SUMMARY:Daily with Multiple EXDATEs
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -67,7 +67,7 @@ END:VCALENDAR`;
expect(eventDates).toEqual(['2025-01-01', '2025-01-03', '2025-01-05']);
});
it('should exclude multiple dates specified in a single EXDATE property (comma separated)', () => {
it('should exclude multiple dates specified in a single EXDATE property (comma separated)', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -81,7 +81,7 @@ SUMMARY:Daily with Comma Separated EXDATE
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,

View file

@ -6,7 +6,7 @@ describe('getRelevantEventsForCalendarIntegrationFromIcal', () => {
const endTimestamp = new Date('2025-12-31T23:59:59Z').getTime();
describe('non-recurring events', () => {
it('should import event with dtend', () => {
it('should import event with dtend', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -18,7 +18,7 @@ SUMMARY:Test Event with End
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -30,7 +30,7 @@ END:VCALENDAR`;
expect(events[0].duration).toBe(3600000); // 1 hour in ms
});
it('should import event without dtend (zero duration)', () => {
it('should import event without dtend (zero duration)', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -41,7 +41,7 @@ SUMMARY:Test Event without End
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -55,7 +55,7 @@ END:VCALENDAR`;
});
describe('recurring events', () => {
it('should import recurring event with dtend', () => {
it('should import recurring event with dtend', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -68,7 +68,7 @@ SUMMARY:Weekly Meeting with End
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -82,7 +82,7 @@ END:VCALENDAR`;
});
});
it('should import recurring event with DURATION instead of dtend', () => {
it('should import recurring event with DURATION instead of dtend', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -95,7 +95,7 @@ SUMMARY:Daily Task with Duration
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -109,7 +109,7 @@ END:VCALENDAR`;
});
});
it('should import recurring event without dtend or DURATION (zero duration)', () => {
it('should import recurring event without dtend or DURATION (zero duration)', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -121,7 +121,7 @@ SUMMARY:Daily Reminder without Duration
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -135,7 +135,7 @@ END:VCALENDAR`;
});
});
it('should handle recurring event with very short duration', () => {
it('should handle recurring event with very short duration', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -148,7 +148,7 @@ SUMMARY:15 Minute Standup
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -162,7 +162,7 @@ END:VCALENDAR`;
});
});
it('should keep descriptions for recurring events', () => {
it('should keep descriptions for recurring events', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -176,7 +176,7 @@ DESCRIPTION:Recurring description text
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -192,7 +192,7 @@ END:VCALENDAR`;
});
describe('mixed event types', () => {
it('should import mix of recurring and non-recurring events', () => {
it('should import mix of recurring and non-recurring events', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -211,7 +211,7 @@ SUMMARY:Recurring Event
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -232,7 +232,7 @@ END:VCALENDAR`;
});
describe('error handling', () => {
it('should handle events outside the time range', () => {
it('should handle events outside the time range', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -244,7 +244,7 @@ SUMMARY:Past Event
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -254,7 +254,7 @@ END:VCALENDAR`;
expect(events.length).toBe(0);
});
it('should continue processing even if one event fails', () => {
it('should continue processing even if one event fails', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -272,7 +272,7 @@ SUMMARY:Bad Event
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -286,7 +286,7 @@ END:VCALENDAR`;
});
describe('edge cases', () => {
it('should handle all-day events and set isAllDay flag', () => {
it('should handle all-day events and set isAllDay flag', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -298,7 +298,7 @@ SUMMARY:All Day Event
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -310,7 +310,7 @@ END:VCALENDAR`;
expect(events[0].isAllDay).toBe(true);
});
it('should NOT set isAllDay flag for timed events', () => {
it('should NOT set isAllDay flag for timed events', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -322,7 +322,7 @@ SUMMARY:Timed Event
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -334,7 +334,7 @@ END:VCALENDAR`;
expect(events[0].isAllDay).toBeUndefined();
});
it('should handle recurring event with UNTIL', () => {
it('should handle recurring event with UNTIL', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -347,7 +347,7 @@ SUMMARY:Daily Until Event
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -361,7 +361,7 @@ END:VCALENDAR`;
});
});
it('should handle recurring all-day event without end time and set isAllDay flag', () => {
it('should handle recurring all-day event without end time and set isAllDay flag', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -373,7 +373,7 @@ SUMMARY:Recurring All Day No End
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -388,7 +388,7 @@ END:VCALENDAR`;
});
});
it('should gracefully skip event with null dtstart', () => {
it('should gracefully skip event with null dtstart', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -399,7 +399,7 @@ DESCRIPTION:This event has no DTSTART
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -411,7 +411,7 @@ END:VCALENDAR`;
expect(events.length).toBe(0);
});
it('should handle mix of valid and invalid events gracefully', () => {
it('should handle mix of valid and invalid events gracefully', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -433,7 +433,7 @@ SUMMARY:Another Valid Event
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -449,7 +449,7 @@ END:VCALENDAR`;
});
describe('RECURRENCE-ID handling (modified recurring events)', () => {
it('should not create duplicate when recurring event has one modified instance', () => {
it('should not create duplicate when recurring event has one modified instance', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -469,7 +469,7 @@ SUMMARY:Daily Meeting (Moved)
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -491,7 +491,7 @@ END:VCALENDAR`;
expect(unmodifiedEvents.length).toBe(2);
});
it('should handle multiple modified instances in recurring event', () => {
it('should handle multiple modified instances in recurring event', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -518,7 +518,7 @@ SUMMARY:Standup (Early & Short)
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -543,7 +543,7 @@ END:VCALENDAR`;
expect(unmodifiedEvents.length).toBe(3);
});
it('should handle cancelled instance in recurring event', () => {
it('should handle cancelled instance in recurring event', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -564,7 +564,7 @@ STATUS:CANCELLED
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -580,7 +580,7 @@ END:VCALENDAR`;
expect(eventOnCancelledDate).toBeUndefined();
});
it('should handle all-day recurring event with DATE-based RECURRENCE-ID', () => {
it('should handle all-day recurring event with DATE-based RECURRENCE-ID', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -598,7 +598,7 @@ SUMMARY:All Day Event (Modified)
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -615,7 +615,7 @@ END:VCALENDAR`;
expect(unmodifiedEvents.length).toBe(2);
});
it('should handle recurring event without exceptions (no regression)', () => {
it('should handle recurring event without exceptions (no regression)', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -628,7 +628,7 @@ SUMMARY:Regular Recurring
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -642,7 +642,7 @@ END:VCALENDAR`;
});
});
it('should handle exception outside time range gracefully', () => {
it('should handle exception outside time range gracefully', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -662,7 +662,7 @@ SUMMARY:Event Series (Old Modified)
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -676,7 +676,7 @@ END:VCALENDAR`;
});
});
it('should include modified instances that are in range when master recurring series is out of range', () => {
it('should include modified instances that are in range when master recurring series is out of range', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -698,7 +698,7 @@ END:VCALENDAR`;
const laterStart = new Date('2025-01-10T00:00:00Z').getTime();
const laterEnd = new Date('2025-02-01T00:00:00Z').getTime();
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
laterStart,
@ -713,7 +713,7 @@ END:VCALENDAR`;
expect(events[0].legacyIds).toEqual(['orphan@test']);
});
it('should not include cancelled orphan exception events', () => {
it('should not include cancelled orphan exception events', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -736,7 +736,7 @@ END:VCALENDAR`;
const laterStart = new Date('2025-01-10T00:00:00Z').getTime();
const laterEnd = new Date('2025-02-01T00:00:00Z').getTime();
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
laterStart,
@ -746,7 +746,7 @@ END:VCALENDAR`;
expect(events.length).toBe(0);
});
it('should generate unique IDs for modified instances', () => {
it('should generate unique IDs for modified instances', async () => {
const icalData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
@ -773,7 +773,7 @@ SUMMARY:Standup (Early)
END:VEVENT
END:VCALENDAR`;
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -795,7 +795,7 @@ END:VCALENDAR`;
});
describe('timezone edge cases (Office 365 compatibility)', () => {
it('should handle iCal with timezone reference but malformed VTIMEZONE gracefully', () => {
it('should handle iCal with timezone reference but malformed VTIMEZONE gracefully', async () => {
// This reproduces the issue from GitHub #5722 where Office 365 calendars
// can cause "Cannot read properties of null (reading 'parent')" errors
// in ICAL.helpers.updateTimezones()
@ -829,7 +829,7 @@ END:VCALENDAR`;
// Should not throw an error
expect(() => {
const events = getRelevantEventsForCalendarIntegrationFromIcal(
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
icalData,
calProviderId,
startTimestamp,
@ -840,7 +840,7 @@ END:VCALENDAR`;
}).not.toThrow();
});
it('should handle iCal with TZID reference to unknown timezone gracefully', () => {
it('should handle iCal with TZID reference to unknown timezone gracefully', async () => {
// Some Office 365 calendars reference timezones that may not be in the
// ical.js TimezoneService, which can cause updateTimezones to fail
const icalData = `BEGIN:VCALENDAR
@ -865,7 +865,7 @@ END:VCALENDAR`;
}).not.toThrow();
});
it('should handle iCal with empty VTIMEZONE component gracefully', () => {
it('should handle iCal with empty VTIMEZONE component gracefully', async () => {
// Edge case: VTIMEZONE exists but has no subcomponents
const icalData = `BEGIN:VCALENDAR
VERSION:2.0

View file

@ -1,7 +1,6 @@
// @ts-ignore
import ICAL from 'ical.js';
import { CalendarIntegrationEvent } from '../../calendar-integration/calendar-integration.model';
import { Log } from '../../../core/log';
import { loadIcalModule } from './ical-lazy-loader';
// NOTE: this sucks and is slow, but writing a new ical parser would be very hard... :(
@ -87,14 +86,16 @@ const buildExceptionMap = (vevents: any[]): ExceptionMap => {
return exceptionMap;
};
export const getRelevantEventsForCalendarIntegrationFromIcal = (
export const getRelevantEventsForCalendarIntegrationFromIcal = async (
icalData: string,
calProviderId: string,
startTimestamp: number,
endTimestamp: number,
): CalendarIntegrationEvent[] => {
): Promise<CalendarIntegrationEvent[]> => {
const ICAL = await loadIcalModule();
let calendarIntegrationEvents: CalendarIntegrationEvent[] = [];
const allPossibleFutureEvents = getAllPossibleEventsAfterStartFromIcal(
ICAL,
icalData,
new Date(startTimestamp),
);
@ -347,7 +348,12 @@ const convertVEventToCalendarIntegrationEvent = (
};
};
const getAllPossibleEventsAfterStartFromIcal = (icalData: string, start: Date): any[] => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getAllPossibleEventsAfterStartFromIcal = (
ICAL: any,
icalData: string,
start: Date,
): any[] => {
const c = ICAL.parse(icalData);
const comp = new ICAL.Component(c);
const tzAdded: string[] = [];

View file

@ -0,0 +1,22 @@
/**
* Lazy loader for ical.js to reduce initial bundle size.
* The ical.js library is ~76KB and only needed for calendar integration.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let icalModule: any = null;
/**
* Lazily loads the ical.js module on first use.
* Subsequent calls return the cached module.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const loadIcalModule = async (): Promise<any> => {
if (!icalModule) {
// @ts-ignore - ical.js exports default
const mod = await import('ical.js');
// Handle both ESM default export and CommonJS module.exports
icalModule = mod.default || mod;
}
return icalModule;
};