mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat: tweak calendar syncing for final release
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||
|
||||
jest.mock("@/utils/verboseLog", () => ({
|
||||
verboseLog: jest.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
reportSyncProgress,
|
||||
resetSyncProgressThrottle,
|
||||
SYNC_PROGRESS_THROTTLE_MS,
|
||||
} from "./lessonSyncShared";
|
||||
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
|
||||
|
||||
describe("reportSyncProgress", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2026-06-28T12:00:00.000Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("reports preparing and done immediately", () => {
|
||||
const onProgress = jest.fn();
|
||||
const preparing: GoogleCalendarSyncProgress = {
|
||||
phase: "preparing",
|
||||
current: 0,
|
||||
total: 10,
|
||||
message: "Preparing…",
|
||||
};
|
||||
const done: GoogleCalendarSyncProgress = {
|
||||
phase: "done",
|
||||
current: 10,
|
||||
total: 10,
|
||||
message: "Done",
|
||||
};
|
||||
|
||||
reportSyncProgress(onProgress, preparing);
|
||||
reportSyncProgress(onProgress, done);
|
||||
|
||||
expect(onProgress).toHaveBeenCalledTimes(2);
|
||||
expect(onProgress).toHaveBeenNthCalledWith(1, preparing);
|
||||
expect(onProgress).toHaveBeenNthCalledWith(2, done);
|
||||
});
|
||||
|
||||
it("throttles upserting progress to at most once per second", () => {
|
||||
const onProgress = jest.fn();
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
reportSyncProgress(onProgress, {
|
||||
phase: "upserting",
|
||||
current: i,
|
||||
total: 5,
|
||||
message: `Syncing events (${i}/5)…`,
|
||||
});
|
||||
}
|
||||
|
||||
expect(onProgress).toHaveBeenCalledTimes(1);
|
||||
expect(onProgress).toHaveBeenCalledWith({
|
||||
phase: "upserting",
|
||||
current: 1,
|
||||
total: 5,
|
||||
message: "Syncing events (1/5)…",
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(SYNC_PROGRESS_THROTTLE_MS);
|
||||
|
||||
expect(onProgress).toHaveBeenCalledTimes(2);
|
||||
expect(onProgress).toHaveBeenLastCalledWith({
|
||||
phase: "upserting",
|
||||
current: 5,
|
||||
total: 5,
|
||||
message: "Syncing events (5/5)…",
|
||||
});
|
||||
});
|
||||
|
||||
it("flushes pending progress before reporting done", () => {
|
||||
const onProgress = jest.fn();
|
||||
|
||||
reportSyncProgress(onProgress, {
|
||||
phase: "upserting",
|
||||
current: 1,
|
||||
total: 5,
|
||||
message: "Syncing events (1/5)…",
|
||||
});
|
||||
|
||||
reportSyncProgress(onProgress, {
|
||||
phase: "upserting",
|
||||
current: 4,
|
||||
total: 5,
|
||||
message: "Syncing events (4/5)…",
|
||||
});
|
||||
|
||||
reportSyncProgress(onProgress, {
|
||||
phase: "done",
|
||||
current: 5,
|
||||
total: 5,
|
||||
message: "Sync complete",
|
||||
});
|
||||
|
||||
expect(onProgress).toHaveBeenCalledTimes(3);
|
||||
expect(onProgress).toHaveBeenNthCalledWith(1, {
|
||||
phase: "upserting",
|
||||
current: 1,
|
||||
total: 5,
|
||||
message: "Syncing events (1/5)…",
|
||||
});
|
||||
expect(onProgress).toHaveBeenNthCalledWith(2, {
|
||||
phase: "upserting",
|
||||
current: 4,
|
||||
total: 5,
|
||||
message: "Syncing events (4/5)…",
|
||||
});
|
||||
expect(onProgress).toHaveBeenNthCalledWith(3, {
|
||||
phase: "done",
|
||||
current: 5,
|
||||
total: 5,
|
||||
message: "Sync complete",
|
||||
});
|
||||
});
|
||||
|
||||
it("resetSyncProgressThrottle clears queued updates", () => {
|
||||
const onProgress = jest.fn();
|
||||
|
||||
reportSyncProgress(onProgress, {
|
||||
phase: "upserting",
|
||||
current: 1,
|
||||
total: 3,
|
||||
message: "Syncing events (1/3)…",
|
||||
});
|
||||
|
||||
resetSyncProgressThrottle(onProgress);
|
||||
jest.advanceTimersByTime(SYNC_PROGRESS_THROTTLE_MS);
|
||||
|
||||
expect(onProgress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,8 @@ import type {
|
||||
} 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 }>;
|
||||
|
||||
@@ -23,11 +25,87 @@ export type MappedLessonEvent = {
|
||||
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,
|
||||
) {
|
||||
onProgress?.(progress);
|
||||
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);
|
||||
}
|
||||
|
||||
export function lessonDateForEvent(startDateTime: string, seqtaKey: string): string {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
trailingWeekRange,
|
||||
} from "@/seqta/utils/googleCalendar/fetchTimetable";
|
||||
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
|
||||
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
||||
import { syncLessonsToGoogleCalendar } from "@/seqta/utils/googleCalendar/syncEngine";
|
||||
import type {
|
||||
GoogleCalendarSyncOptions,
|
||||
@@ -36,7 +37,7 @@ export async function runGoogleCalendarSync(
|
||||
const mode = params.mode ?? "full";
|
||||
const weeksAhead = await getSyncWeeksAhead();
|
||||
|
||||
params.onProgress?.({
|
||||
reportSyncProgress(params.onProgress, {
|
||||
phase: "preparing",
|
||||
current: 0,
|
||||
total: 1,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
trailingWeekRange,
|
||||
} from "@/seqta/utils/googleCalendar/fetchTimetable";
|
||||
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
|
||||
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
||||
import { syncLessonsToOutlookCalendar } from "@/seqta/utils/outlookCalendar/syncEngine";
|
||||
import type {
|
||||
GoogleCalendarSyncOptions,
|
||||
@@ -36,7 +37,7 @@ export async function runOutlookCalendarSync(
|
||||
const mode = params.mode ?? "full";
|
||||
const weeksAhead = await getSyncWeeksAhead();
|
||||
|
||||
params.onProgress?.({
|
||||
reportSyncProgress(params.onProgress, {
|
||||
phase: "preparing",
|
||||
current: 0,
|
||||
total: 1,
|
||||
|
||||
Reference in New Issue
Block a user