refactor: consolidate and debloat calendar sync implementation

Merge duplicated Google/Outlook engines, background handlers, and API
layers into shared calendarSync modules to cut complexity without changing
user-facing sync behavior.
This commit is contained in:
SethBurkart123
2026-06-28 10:45:45 +10:00
parent f4230e02b9
commit 7779edb063
52 changed files with 1965 additions and 3258 deletions
+109 -128
View File
@@ -3,109 +3,31 @@ import {
getStoredEventId,
lessonDateFromSeqtaKey,
normalizeEventMapEntry,
} from "@/seqta/utils/googleCalendar/eventMapEntry";
type EventMapRecord,
} from "@/seqta/utils/calendarSync/eventMap";
import {
isDateInRange,
syncWindowRange,
} from "@/seqta/utils/googleCalendar/syncDateRange";
import type {
GoogleCalendarDeleteResult,
GoogleCalendarSyncOptions,
GoogleCalendarSyncProgress,
GoogleCalendarSyncResult,
} from "@/seqta/utils/googleCalendar/types";
export const EVENT_MAP_PERSIST_EVERY = 10;
/** Max UI progress refresh rate during bulk delete/upsert (reduces Svelte re-renders). */
export const SYNC_PROGRESS_THROTTLE_MS = 1000;
export type EventMapRecord = Record<string, string | { id: string; date: string }>;
export type MappedLessonEvent = {
seqtaKey: string;
startDateTime: string;
};
type ProgressThrottleState = {
lastReportAt: number;
pending: GoogleCalendarSyncProgress | null;
timer: ReturnType<typeof setTimeout> | null;
};
const progressThrottleByCallback = new WeakMap<
NonNullable<GoogleCalendarSyncOptions["onProgress"]>,
ProgressThrottleState
>();
function getProgressThrottleState(
onProgress: NonNullable<GoogleCalendarSyncOptions["onProgress"]>,
): ProgressThrottleState {
let state = progressThrottleByCallback.get(onProgress);
if (!state) {
state = { lastReportAt: 0, pending: null, timer: null };
progressThrottleByCallback.set(onProgress, state);
}
return state;
}
function flushPendingSyncProgress(
onProgress: NonNullable<GoogleCalendarSyncOptions["onProgress"]>,
state: ProgressThrottleState,
) {
if (state.timer) {
clearTimeout(state.timer);
state.timer = null;
}
if (!state.pending) return;
onProgress(state.pending);
state.pending = null;
state.lastReportAt = Date.now();
}
/** Clears any queued progress for a callback (e.g. when a sync run ends). */
export function resetSyncProgressThrottle(
onProgress: GoogleCalendarSyncOptions["onProgress"],
) {
if (!onProgress) return;
const state = progressThrottleByCallback.get(onProgress);
if (!state) return;
if (state.timer) {
clearTimeout(state.timer);
state.timer = null;
}
state.pending = null;
}
export function reportSyncProgress(
onProgress: GoogleCalendarSyncOptions["onProgress"],
progress: GoogleCalendarSyncProgress,
) {
if (!onProgress) return;
const state = getProgressThrottleState(onProgress);
if (progress.phase === "preparing" || progress.phase === "done") {
flushPendingSyncProgress(onProgress, state);
onProgress(progress);
state.lastReportAt = Date.now();
if (progress.phase === "done") {
resetSyncProgressThrottle(onProgress);
}
return;
}
state.pending = progress;
const elapsed = Date.now() - state.lastReportAt;
if (elapsed >= SYNC_PROGRESS_THROTTLE_MS) {
flushPendingSyncProgress(onProgress, state);
return;
}
if (state.timer) return;
state.timer = setTimeout(() => {
state.timer = null;
flushPendingSyncProgress(onProgress, state);
}, SYNC_PROGRESS_THROTTLE_MS - elapsed);
onProgress?.(progress);
}
export function lessonDateForEvent(startDateTime: string, seqtaKey: string): string {
@@ -126,18 +48,6 @@ export function originEventMapEntries(
return entries;
}
function shouldPruneEntry(
mode: "full" | "incremental",
entry: { id: string; date: string },
mapKey: string,
window: ReturnType<typeof syncWindowRange>,
currentMapKeys: Set<string>,
): boolean {
if (mode === "incremental") return false;
if (entry.date) return !isDateInRange(entry.date, window);
return !currentMapKeys.has(mapKey);
}
export function entriesToPrune(
eventMap: EventMapRecord,
origin: string,
@@ -145,6 +55,8 @@ export function entriesToPrune(
weeksAhead: number,
currentMapKeys: Set<string>,
): Array<[string, string]> {
if (mode === "incremental") return [];
const window = syncWindowRange(weeksAhead);
const prefix = `${origin}::`;
const entries: Array<[string, string]> = [];
@@ -153,9 +65,10 @@ export function entriesToPrune(
if (!mapKey.startsWith(prefix)) continue;
const entry = normalizeEventMapEntry(raw);
if (!entry) continue;
if (shouldPruneEntry(mode, entry, mapKey, window, currentMapKeys)) {
entries.push([mapKey, entry.id]);
}
const stale = entry.date
? !isDateInRange(entry.date, window)
: !currentMapKeys.has(mapKey);
if (stale) entries.push([mapKey, entry.id]);
}
return entries;
@@ -178,6 +91,98 @@ export function emptyLessonsSyncResult(): GoogleCalendarSyncResult {
};
}
export function formatLessonSyncResultMessage(
result: GoogleCalendarSyncResult,
calendarLabel: string,
): string {
const created = result.created ?? 0;
const updated = result.updated ?? 0;
const deleted = result.deleted ?? 0;
const parts: string[] = [];
if (created > 0) parts.push(`${created} new`);
if (updated > 0) parts.push(`${updated} updated`);
if (deleted > 0) parts.push(`${deleted} removed`);
if (parts.length === 0) return `${calendarLabel} is up to date.`;
return `${calendarLabel} updated (${parts.join(", ")}).`;
}
export function buildDeleteSyncResult(
deleted: number,
failed: number,
): GoogleCalendarDeleteResult {
return {
success: failed === 0,
configured: true,
connected: true,
deleted,
failed,
error:
failed > 0
? `Removed ${deleted} event${deleted === 1 ? "" : "s"} with ${failed} error${failed === 1 ? "" : "s"}.`
: undefined,
};
}
export async function deleteTrackedLessonEvents(
entries: Array<[string, string]>,
eventMap: EventMapRecord,
getAccessToken: () => Promise<string>,
deleteEvent: (
accessToken: string,
eventId: string,
refreshAccessToken: () => Promise<string>,
) => Promise<void>,
writeState: (patch: { eventMap: EventMapRecord }) => Promise<unknown>,
options: {
persistProgress?: boolean;
onProgress?: GoogleCalendarSyncOptions["onProgress"];
progressOffset?: number;
progressTotal?: number;
logLabel: string;
},
): Promise<{ deleted: number; failed: number }> {
if (entries.length === 0) return { deleted: 0, failed: 0 };
const {
persistProgress = false,
onProgress,
progressOffset = 0,
progressTotal = 0,
logLabel,
} = options;
let accessToken = await getAccessToken();
let deleted = 0;
let failed = 0;
for (const [mapKey, eventId] of entries) {
try {
await deleteEvent(accessToken, eventId, async () => {
accessToken = await getAccessToken();
return accessToken;
});
delete eventMap[mapKey];
deleted += 1;
} catch (err) {
verboseLog(`[BetterSEQTA+] ${logLabel} event delete failed:`, err);
failed += 1;
}
reportSyncProgress(onProgress, {
phase: "deleting",
current: progressOffset + deleted + failed,
total: progressTotal,
message: `Removing old events (${deleted + failed}/${entries.length})…`,
});
if (persistProgress && (deleted + failed) % EVENT_MAP_PERSIST_EVERY === 0) {
await writeState({ eventMap });
}
}
return { deleted, failed };
}
export function buildLessonSyncResult(
created: number,
updated: number,
@@ -202,24 +207,6 @@ export function buildLessonSyncResult(
};
}
export async function persistFinalSyncState(
writeState: (patch: {
eventMap: EventMapRecord;
lastSyncAt: number;
lastSyncOrigin: string;
}) => Promise<unknown>,
eventMap: EventMapRecord,
lastSyncAt: number,
origin: string,
staleDeleted: number,
staleEntryCount: number,
eventCount: number,
): Promise<void> {
if (staleDeleted > 0 || staleEntryCount > 0 || eventCount > 0) {
await writeState({ eventMap, lastSyncAt, lastSyncOrigin: origin });
}
}
type UpsertLessonEventsParams<TEvent extends MappedLessonEvent> = {
events: TEvent[];
eventMap: EventMapRecord;
@@ -288,26 +275,20 @@ export async function upsertLessonEvents<TEvent extends MappedLessonEvent>(
date: lessonDateForEvent(event.startDateTime, event.seqtaKey),
};
reportSyncProgress(onProgress, {
phase: "upserting",
current: progressCurrent,
total: totalSteps,
message: progressMessage,
});
if ((i + 1) % EVENT_MAP_PERSIST_EVERY === 0 || i === events.length - 1) {
await writeState({ eventMap, lastSyncAt, lastSyncOrigin: origin });
}
} catch (err) {
verboseLog(`[BetterSEQTA+] ${logLabel} event sync failed:`, err);
failed += 1;
reportSyncProgress(onProgress, {
phase: "upserting",
current: progressCurrent,
total: totalSteps,
message: progressMessage,
});
}
reportSyncProgress(onProgress, {
phase: "upserting",
current: progressCurrent,
total: totalSteps,
message: progressMessage,
});
}
return { created, updated, failed, accessToken };