From 13c411c842744fe46a61425ce2d09b41cd62740a Mon Sep 17 00:00:00 2001 From: kenneth Date: Sat, 14 Feb 2026 15:36:36 +0000 Subject: [PATCH] perf: cache fetched events within a refresh cycle FeedEngine calls fetchContext then fetchItems with the same context. Cache events by context.time reference to avoid duplicate CalDAV round-trips. Co-authored-by: Ona --- .../src/calendar-source.test.ts | 36 +++++++++++++++++++ .../src/calendar-source.ts | 6 ++++ 2 files changed, 42 insertions(+) diff --git a/packages/aris-source-apple-calendar/src/calendar-source.test.ts b/packages/aris-source-apple-calendar/src/calendar-source.test.ts index 4d9dd99..4544a4f 100644 --- a/packages/aris-source-apple-calendar/src/calendar-source.test.ts +++ b/packages/aris-source-apple-calendar/src/calendar-source.test.ts @@ -48,6 +48,7 @@ class MockCredentialProvider implements CalendarCredentialProvider { class MockDAVClient implements CalendarDAVClient { credentials: Record = {} + fetchCalendarsCallCount = 0 private calendars: CalendarDAVCalendar[] private objectsByCalendarUrl: Record @@ -62,6 +63,7 @@ class MockDAVClient implements CalendarDAVClient { async login(): Promise {} async fetchCalendars(): Promise { + this.fetchCalendarsCallCount++ return this.calendars } @@ -241,6 +243,40 @@ describe("CalendarSource", () => { expect(exception!.data.recurrenceId).not.toBeNull() expect(exception!.id).toContain("-") }) + + test("caches events within the same refresh cycle", async () => { + const objects: Record = { + "/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }], + } + const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects) + const source = new CalendarSource(new MockCredentialProvider(), "user-1", { + davClient: client, + }) + + const context = createContext(new Date("2026-01-15T12:00:00Z")) + + await source.fetchContext(context) + await source.fetchItems(context) + + // Same context.time reference — fetchEvents should only hit the client once + expect(client.fetchCalendarsCallCount).toBe(1) + }) + + test("refetches events for a different context time", async () => { + const objects: Record = { + "/cal/work": [{ url: "/cal/work/event1.ics", data: loadFixture("single-event.ics") }], + } + const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects) + const source = new CalendarSource(new MockCredentialProvider(), "user-1", { + davClient: client, + }) + + await source.fetchItems(createContext(new Date("2026-01-15T12:00:00Z"))) + await source.fetchItems(createContext(new Date("2026-01-15T13:00:00Z"))) + + // Different context.time references — should fetch twice + expect(client.fetchCalendarsCallCount).toBe(2) + }) }) describe("CalendarSource.fetchContext", () => { diff --git a/packages/aris-source-apple-calendar/src/calendar-source.ts b/packages/aris-source-apple-calendar/src/calendar-source.ts index 6c265cb..5ae1425 100644 --- a/packages/aris-source-apple-calendar/src/calendar-source.ts +++ b/packages/aris-source-apple-calendar/src/calendar-source.ts @@ -45,6 +45,7 @@ export class CalendarSource implements FeedSource { private readonly injectedClient: CalendarDAVClient | null private davClient: CalendarDAVClient | null = null private lastAccessToken: string | null = null + private cachedEvents: { time: Date; events: CalendarEventData[] } | null = null constructor( credentialProvider: CalendarCredentialProvider, @@ -94,6 +95,10 @@ export class CalendarSource implements FeedSource { } private async fetchEvents(context: Context): Promise { + if (this.cachedEvents && this.cachedEvents.time === context.time) { + return this.cachedEvents.events + } + const credentials = await this.credentialProvider.fetchCredentials(this.userId) if (!credentials) { return [] @@ -134,6 +139,7 @@ export class CalendarSource implements FeedSource { } } + this.cachedEvents = { time: context.time, events: allEvents } return allEvents }