/// import { computed, Default, derive, getPatternEnvironment, handler, ifElse, NAME, pattern, patternTool, str, UI, Writable, } from "commontools"; import { createGoogleAuth, type ScopeKey, } from "./util/google-auth-manager.tsx"; type CFC = T; type Secret = CFC; type Confidential = CFC; // This is used by the various Google tokens created with tokenToAuthData export type Auth = { token: Default, "">; tokenType: Default; scope: Default; expiresIn: Default; expiresAt: Default; refreshToken: Default, "">; user: Default<{ email: string; name: string; picture: string; }, { email: ""; name: ""; picture: "" }>; }; const env = getPatternEnvironment(); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // ========== OPTIONAL DERIVE DEBUG INSTRUMENTATION ========== // Set DEBUG_DERIVE=true to enable derive() call counting for performance investigation. // IMPORTANT: Keep disabled in production - the setInterval has no cleanup mechanism. const DEBUG_DERIVE = false; let deriveCallCount = 0; let perRowDeriveCount = 0; let lastLogTime = Date.now(); const startTime = Date.now(); function logDeriveCall(name: string, isPerRow = false) { if (!DEBUG_DERIVE) return; deriveCallCount++; if (isPerRow) perRowDeriveCount++; const now = Date.now(); const elapsed = now - startTime; // Log on milestones or every second if (now - lastLogTime > 1000 || deriveCallCount % 100 === 0) { console.log( `[DERIVE DEBUG] ${name}: total=${deriveCallCount}, perRow=${perRowDeriveCount}, elapsed=${elapsed}ms`, ); lastLogTime = now; } } // Only start interval if debugging is enabled if (DEBUG_DERIVE) { try { setInterval(() => { const elapsed = Date.now() - startTime; console.log( `[DERIVE DEBUG SUMMARY] total=${deriveCallCount}, perRow=${perRowDeriveCount}, elapsed=${elapsed}ms`, ); }, 5000); } catch { // Ignore if setInterval isn't available during compilation } } // ========== END DEBUG INSTRUMENTATION ========== export type CalendarEvent = { id: string; summary: string; description: string; location: string; start: string; end: string; startDateTime: string; endDateTime: string; isAllDay: boolean; status: string; htmlLink: string; calendarId: string; calendarName: string; attendees: Default< { email: string; displayName: string; responseStatus: string }[], [] >; organizer: { email: string; displayName: string }; }; export type Calendar = { id: string; summary: string; description: string; primary: boolean; backgroundColor: string; foregroundColor: string; }; type Settings = { // Number of days in the past to fetch daysBack: Default; // Number of days in the future to fetch daysForward: Default; // Maximum number of events to fetch per calendar maxResults: Default; // Enable verbose console logging for debugging debugMode: Default; }; // Debug logging helpers function debugLog(debugMode: boolean, ...args: unknown[]) { if (debugMode) console.log(...args); } function debugWarn(debugMode: boolean, ...args: unknown[]) { if (debugMode) console.warn(...args); } interface CalendarClientConfig { retries?: number; delay?: number; delayIncrement?: number; debugMode?: boolean; } // Helper function to parse calendar API response (extracted to module scope for compiler compliance) function parseCalendarApiItem(apiItem: any): Calendar { return { id: apiItem.id, summary: apiItem.summary || "", description: apiItem.description || "", primary: apiItem.primary || false, backgroundColor: apiItem.backgroundColor || "#4285f4", foregroundColor: apiItem.foregroundColor || "#ffffff", }; } class CalendarClient { private auth: Writable; private retries: number; private delay: number; private delayIncrement: number; private debugMode: boolean; constructor( auth: Writable, { retries = 3, delay = 1000, delayIncrement = 100, debugMode = false }: CalendarClientConfig = {}, ) { this.auth = auth; this.retries = retries; this.delay = delay; this.delayIncrement = delayIncrement; this.debugMode = debugMode; } private async refreshAuth() { const body = { refreshToken: this.auth.get().refreshToken, }; debugLog(this.debugMode, "refreshAuthToken", body); const res = await fetch( new URL("/api/integrations/google-oauth/refresh", env.apiUrl), { method: "POST", body: JSON.stringify(body), }, ); if (!res.ok) { throw new Error("Could not acquire a refresh token."); } const json = await res.json(); const authData = json.tokenInfo as Auth; this.auth.update(authData); } async getCalendarList(): Promise { const url = new URL( "https://www.googleapis.com/calendar/v3/users/me/calendarList", ); const res = await this.googleRequest(url); const json = await res.json(); if (!json.items || !Array.isArray(json.items)) { debugLog(this.debugMode, "No calendars found:", json); return []; } return json.items.map(parseCalendarApiItem); } async getEvents( calendarId: string, timeMin: Date, timeMax: Date, maxResults: number = 100, ): Promise { const url = new URL( `https://www.googleapis.com/calendar/v3/calendars/${ encodeURIComponent(calendarId) }/events`, ); url.searchParams.set("timeMin", timeMin.toISOString()); url.searchParams.set("timeMax", timeMax.toISOString()); url.searchParams.set("maxResults", maxResults.toString()); url.searchParams.set("singleEvents", "true"); url.searchParams.set("orderBy", "startTime"); debugLog(this.debugMode, "Fetching events from:", url.toString()); const res = await this.googleRequest(url); const json = await res.json(); if (!json.items || !Array.isArray(json.items)) { debugLog(this.debugMode, "No events found:", json); return []; } return json.items; } private async googleRequest( url: URL, _options?: RequestInit, _retries?: number, ): Promise { const token = this.auth.get().token; if (!token) { throw new Error("No authorization token."); } const retries = _retries ?? this.retries; const options = _options ?? {}; options.headers = new Headers(options.headers); options.headers.set("Authorization", `Bearer ${token}`); const res = await fetch(url, options); const { ok, status, statusText } = res; if (ok) { debugLog(this.debugMode, `${url}: ${status} ${statusText}`); return res; } debugWarn( this.debugMode, `${url}: ${status} ${statusText}`, `Remaining retries: ${retries}`, ); if (retries === 0) { throw new Error("Too many failed attempts."); } await sleep(this.delay); if (status === 401) { await this.refreshAuth(); } else if (status === 429) { this.delay += this.delayIncrement; debugLog(this.debugMode, `Incrementing delay to ${this.delay}`); await sleep(this.delay); } return this.googleRequest(url, _options, retries - 1); } } function parseCalendarEvent( event: any, calendarId: string, calendarName: string, ): CalendarEvent { const isAllDay = !event.start?.dateTime; const startDateTime = event.start?.dateTime || event.start?.date || ""; const endDateTime = event.end?.dateTime || event.end?.date || ""; return { id: event.id || "", summary: event.summary || "(No title)", description: event.description || "", location: event.location || "", start: event.start?.date || event.start?.dateTime?.split("T")[0] || "", end: event.end?.date || event.end?.dateTime?.split("T")[0] || "", startDateTime, endDateTime, isAllDay, status: event.status || "confirmed", htmlLink: event.htmlLink || "", calendarId, calendarName, attendees: (event.attendees || []).map((a: any) => ({ email: a.email || "", displayName: a.displayName || a.email || "", responseStatus: a.responseStatus || "needsAction", })), organizer: { email: event.organizer?.email || "", displayName: event.organizer?.displayName || event.organizer?.email || "", }, }; } // Core fetch logic extracted for reuse type FetchState = { events: Writable; calendars: Writable; auth: Writable; settings: Writable<{ daysBack: number; daysForward: number; maxResults: number; debugMode: boolean; }>; fetching?: Writable; selectedCalendarIds?: Writable; }; async function fetchCalendarEvents(state: FetchState): Promise { // Set fetching state if available if (state.fetching) { state.fetching.set(true); } const debugMode = state.settings.get().debugMode || false; debugLog(debugMode, "fetchCalendarEvents!"); if (!state.auth.get().token) { debugWarn(debugMode, "no token found in auth cell"); if (state.fetching) state.fetching.set(false); return; } const settings = state.settings.get(); const client = new CalendarClient(state.auth, { debugMode }); try { // Get calendar list debugLog(debugMode, "Fetching calendar list..."); // Check if this is first fetch BEFORE fetching new calendars const previousCalendarCount = state.calendars.get().length; const currentSelected = state.selectedCalendarIds?.get() || []; const calendars = await client.getCalendarList(); debugLog(debugMode, `Found ${calendars.length} calendars`); // Only update calendars if the list actually changed (prevents reactive loop) const existingCalendars = state.calendars.get(); const calendarsChanged = existingCalendars.length !== calendars.length || calendars.some( (cal, i) => existingCalendars[i]?.id !== cal.id || existingCalendars[i]?.summary !== cal.summary || existingCalendars[i]?.backgroundColor !== cal.backgroundColor || existingCalendars[i]?.foregroundColor !== cal.foregroundColor, ); if (calendarsChanged) { debugLog(debugMode, "Calendar list changed, updating..."); state.calendars.set(calendars); } // Initialize selectedCalendarIds with all calendars on first fetch // Only auto-select all on first fetch (when we had no calendars before AND no selection) if (state.selectedCalendarIds) { if (currentSelected.length === 0 && previousCalendarCount === 0) { state.selectedCalendarIds.set(calendars.map((c) => c.id)); debugLog( debugMode, "First fetch - selected all calendars by default", ); } } // Calculate time range const now = new Date(); const timeMin = new Date(now); timeMin.setDate(timeMin.getDate() - settings.daysBack); const timeMax = new Date(now); timeMax.setDate(timeMax.getDate() + settings.daysForward); debugLog( debugMode, `Time range: ${timeMin.toISOString()} to ${timeMax.toISOString()}`, ); // Filter calendars based on selection // Only fetch from selected calendars (empty selection = fetch nothing, not everything) const selectedIds = state.selectedCalendarIds?.get() || []; const calendarsToFetch = calendars.filter((c) => selectedIds.includes(c.id) ); debugLog( debugMode, `Fetching events from ${calendarsToFetch.length} selected calendars`, ); // Fetch events from selected calendars const allEvents: CalendarEvent[] = []; for (const calendar of calendarsToFetch) { try { debugLog( debugMode, `Fetching events from calendar: ${calendar.summary} (${calendar.id})`, ); const rawEvents = await client.getEvents( calendar.id, timeMin, timeMax, settings.maxResults, ); const events = rawEvents.map((e) => parseCalendarEvent(e, calendar.id, calendar.summary) ); debugLog( debugMode, `Found ${events.length} events in ${calendar.summary}`, ); allEvents.push(...events); // Small delay between calendar requests to avoid rate limiting await sleep(200); } catch (error) { debugWarn( debugMode, `Error fetching events from ${calendar.summary}:`, error, ); } } // Sort events by start time allEvents.sort((a, b) => { const aStart = new Date(a.startDateTime || a.start).getTime(); const bStart = new Date(b.startDateTime || b.start).getTime(); return aStart - bStart; }); debugLog(debugMode, `Total events fetched: ${allEvents.length}`); state.events.set(allEvents); } finally { // Clear fetching state if (state.fetching) state.fetching.set(false); } } // Handler wrapper that calls the core fetch logic const calendarUpdater = handler( async (_event, state) => { await fetchCalendarEvents(state); }, ); const toggleDebugMode = handler< { target: { checked: boolean } }, { settings: Writable } >( ({ target }, { settings }) => { const current = settings.get(); settings.set({ ...current, debugMode: target.checked }); }, ); const toggleCalendarSelection = handler< unknown, { calendarId: string; selectedCalendarIds: Writable; events: Writable; calendars: Writable; auth: Writable; settings: Writable; fetching?: Writable; } >(async (_event, state) => { const { calendarId, selectedCalendarIds } = state; const current = selectedCalendarIds.get(); if (current.includes(calendarId)) { selectedCalendarIds.set(current.filter((c) => c !== calendarId)); } else { selectedCalendarIds.set([...current, calendarId]); } // Re-fetch events with new selection - call directly await fetchCalendarEvents(state); }); const selectAllCalendars = handler< unknown, { calendars: Writable; selectedCalendarIds: Writable; events: Writable; auth: Writable; settings: Writable; fetching?: Writable; } >(async (_event, state) => { const { calendars, selectedCalendarIds } = state; const allIds = calendars.get().map((c) => c.id); selectedCalendarIds.set(allIds); // Re-fetch events with new selection - call directly await fetchCalendarEvents(state); }); const deselectAllCalendars = handler< unknown, { selectedCalendarIds: Writable; events: Writable; calendars: Writable; auth: Writable; settings: Writable; fetching?: Writable; } >(async (_event, state) => { state.selectedCalendarIds.set([]); // Re-fetch events with new selection (will fetch nothing) - call directly await fetchCalendarEvents(state); }); const _nextPage = handler }>( (_, { currentPage }) => { currentPage.set(currentPage.get() + 1); }, ); const _prevPage = handler }>( (_, { currentPage }) => { const current = currentPage.get(); if (current > 0) { currentPage.set(current - 1); } }, ); // Format date for display function formatEventDate( startDateTime: string, endDateTime: string, isAllDay: boolean, ): string { if (isAllDay) { return startDateTime; } const start = new Date(startDateTime); const end = new Date(endDateTime); const dateStr = start.toLocaleDateString(); const startTime = start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); const endTime = end.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); return `${dateStr} ${startTime} - ${endTime}`; } interface GoogleCalendarImporterInput { settings?: Default; // Optional: Link auth directly from a Google Auth piece when wish() is unavailable // Use: ct piece link googleAuthPiece/auth calendarImporterPiece/overrideAuth overrideAuth?: Auth; } /** Google Calendar event importer. #calendarEvents */ interface Output { events: CalendarEvent[]; calendars: Calendar[]; /** Number of events imported */ eventCount: number; /** Summary of container-level events for hierarchical indexing */ summary: string; } const toggleShowEvents = handler }>( (_, { showEvents }) => { showEvents.set(!showEvents.get()); }, ); // Settings is not optional - we provide it with a default const GoogleCalendarImporter = pattern( ({ settings, overrideAuth }) => { const events = Writable.of>([]); const calendars = Writable.of([]); const fetching = Writable.of(false); const currentPage = Writable.of(0); const showEvents = Writable.of(false); // Collapsed by default const selectedCalendarIds = Writable.of([]); // Empty = all selected on first fetch const PAGE_SIZE = 10; // Pre-compute calendar selection state for efficient lookup // Key insight: Can index computed objects with Cell values directly const calendarSelectionMap = computed(() => { const selected = selectedCalendarIds.get() ?? []; const cals = calendars.get() ?? []; const map: Record = {}; for (const cal of cals) { if (cal?.id) { map[cal.id] = selected.includes(cal.id); } } return map; }); // Pre-compute calendar colors map for efficient lookup const calendarColorsMap = computed(() => { const cals = calendars.get() ?? []; const map: Record = {}; for (const cal of cals) { if (cal?.id) { map[cal.id] = { bg: cal.backgroundColor || "#4285f4", fg: cal.foregroundColor || "#ffffff", }; } } return map; }); // Use createGoogleAuth utility for auth management const { auth: wishedAuth, fullUI, isReady: wishedIsReady, currentEmail: wishedCurrentEmail, } = createGoogleAuth({ requiredScopes: ["calendar"] as ScopeKey[], }); // Check if overrideAuth is provided (for manual linking when wish() is unavailable) const hasLinkedAuth = computed(() => !!(overrideAuth?.token)); const overrideAuthEmail = computed(() => overrideAuth?.user?.email || ""); // Use overrideAuth if provided, otherwise use wished auth // This allows manual linking via CLI when wish() is unavailable (e.g., favorites disabled) // Note: We wrap overrideAuth in Writable.of outside of reactive context const overrideAuthCell = Writable.of(null); computed(() => { if (overrideAuth?.token) { overrideAuthCell.set(overrideAuth as any); } }); // Choose auth source based on overrideAuth availability const auth = ifElse(hasLinkedAuth, overrideAuthCell, wishedAuth) as any; const isReady = ifElse(hasLinkedAuth, hasLinkedAuth, wishedIsReady); const currentEmail = ifElse( hasLinkedAuth, overrideAuthEmail, wishedCurrentEmail, ); // Computed values for pagination const upcomingEvents = derive(events, (evts: CalendarEvent[]) => { const now = new Date(); return [...evts] .filter((e) => new Date(e.startDateTime || e.start) >= now) .sort((a, b) => new Date(a.startDateTime || a.start).getTime() - new Date(b.startDateTime || b.start).getTime() ); }); const summary = derive(events, (eventList: CalendarEvent[]) => { return eventList .slice(0, 20) .map((e) => `${e.summary || ""} ${e.start || ""}`.trim()) .filter((s) => s.length > 0) .join(" | "); }); const totalUpcoming = derive( upcomingEvents, (evts: CalendarEvent[]) => evts.length, ); const _maxPageNum = derive( totalUpcoming, (total: number) => Math.max(0, Math.ceil(total / PAGE_SIZE) - 1), ); // Paginated events for display - use computed with events Cell directly const _paginatedEvents = computed(() => { const allEvents = events.get() || []; const now = new Date(); const upcoming = [...allEvents] .filter((e: CalendarEvent) => new Date(e.startDateTime || e.start) >= now ) .sort((a: CalendarEvent, b: CalendarEvent) => new Date(a.startDateTime || a.start).getTime() - new Date(b.startDateTime || b.start).getTime() ); const page = currentPage.get(); const start = page * PAGE_SIZE; const end = Math.min(start + PAGE_SIZE, upcoming.length); return upcoming.slice(start, end); }); return { [NAME]: str`Calendar Importer ${currentEmail}`, [UI]: (
Google Calendar Importer
{/* Auth status - handled by createGoogleAuth utility */} {fullUI}

Imported event count: {computed(() => events.get().length)}

Calendars found: {computed(() => calendars.get().length)}
{ifElse( isReady, {ifElse( fetching, Fetching... , "Fetch Calendar Events", )} , null, )}
{/* Calendar list with selection */}

Your Calendars ({computed(() => calendars.get().length)})

{calendars.map((cal) => { // Use pre-computed map - direct indexing works with Cell values return (
{ifElse( calendarSelectionMap[cal.id], , , )} {cal.primary ? : null} {cal.summary}
); })}

Click calendars to toggle selection. Only selected calendars will be fetched.

{/* Collapsible events list */}
{ifElse(showEvents, "▼", "▶")}

{derive( events, (evts: CalendarEvent[]) => `${evts.length} events imported`, )}

{ifElse( showEvents,
{events.map((event) => { // Use pre-computed colors map - direct indexing works with Cell values return (
{event.calendarName} {event.summary}
{formatEventDate( event.startDateTime, event.endDateTime, event.isAllDay, )}
); })}
,

Click to expand event list.

, )}
), events, calendars, eventCount: derive(events, (list: CalendarEvent[]) => { logDeriveCall(`eventCount (length=${list?.length})`); return list?.length || 0; }), summary, bgUpdater: calendarUpdater({ events, calendars, auth, settings, selectedCalendarIds, }), // Pattern tools for omnibot searchEvents: patternTool( ({ query, events }: { query: string; events: CalendarEvent[] }) => { return derive({ query, events }, ({ query, events }) => { if (!query || !events) return []; const lowerQuery = query.toLowerCase(); return events.filter((event) => event.summary?.toLowerCase().includes(lowerQuery) || event.description?.toLowerCase().includes(lowerQuery) || event.location?.toLowerCase().includes(lowerQuery) ); }); }, { events }, ), getEventCount: patternTool( ({ events }: { events: CalendarEvent[] }) => { return derive(events, (list) => list?.length || 0); }, { events }, ), getUpcomingEvents: patternTool( ({ count, events }: { count: number; events: CalendarEvent[] }) => { return derive({ count, events }, ({ count, events }) => { if (!events || events.length === 0) return "No events"; const now = new Date(); const upcoming = events .filter((e) => new Date(e.startDateTime || e.start) >= now) .slice(0, count || 5); return upcoming.map((event) => `${ formatEventDate( event.startDateTime, event.endDateTime, event.isAllDay, ) }: ${event.summary}${ event.location ? ` @ ${event.location}` : "" }` ).join("\n"); }); }, { events }, ), getTodaysEvents: patternTool( ({ events }: { events: CalendarEvent[] }) => { return derive(events, (events) => { if (!events || events.length === 0) return "No events"; const today = new Date().toISOString().split("T")[0]; const todayEvents = events.filter((e) => e.start === today || (e.startDateTime && e.startDateTime.startsWith(today)) ); if (todayEvents.length === 0) return "No events today"; return todayEvents.map((event) => `${ formatEventDate( event.startDateTime, event.endDateTime, event.isAllDay, ) }: ${event.summary}` ).join("\n"); }); }, { events }, ), }; }, ); export default GoogleCalendarImporter;