feat: background resuming for calendar syncing

This commit is contained in:
2026-08-20 08:22:41 +09:30
parent eaf89d3f12
commit b830f53069
7 changed files with 107 additions and 19 deletions
@@ -3,7 +3,6 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
import { ensureFontLoaded } from "@/seqta/ui/fonts/Manager";
import { getFontPreset } from "@/seqta/ui/fonts/presets";
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
import hostStyles from "./calendarSyncHost.css?inline";
@@ -152,7 +151,6 @@ export async function mountGoogleCalendarButton(): Promise<void> {
if (!toolbar) return;
ensureHostStyles();
registerCalendarContentHandlers();
const controls = document.createElement("div");
controls.className = CONTROLS_CLASS;
+3
View File
@@ -5,6 +5,7 @@ import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { verboseLog } from "@/utils/verboseLog";
import { mountGoogleCalendarButton, unmountGoogleCalendarButton } from "./calendarSyncUi";
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
const timetablePlugin: Plugin<{}, {}> = {
id: "timetable",
@@ -15,6 +16,8 @@ const timetablePlugin: Plugin<{}, {}> = {
disableToggle: true,
run: async (api) => {
registerCalendarContentHandlers();
const { unregister } = api.seqta.onMount(".timetablepage", handleTimetable);
return () => {
+33
View File
@@ -10,6 +10,8 @@ import {
} from "@/seqta/utils/calendarSync/providerStorage";
export const BSPLUS_CALENDAR_SYNC_SETTINGS_KEY = "bsplus_calendar_sync_settings";
/** Device-local interrupted sync marker — never cloud-synced. */
export const BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY = "bsplus_calendar_sync_in_progress";
export const CALENDAR_WEEKLY_ALARM = "bsplus_calendar_weekly";
export const WEEKLY_SYNC_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -98,3 +100,34 @@ export async function markWeeklySyncComplete(): Promise<void> {
export async function markWeeklySyncPending(): Promise<void> {
await writeSharedCalendarSyncSettings({ pendingWeeklySync: true });
}
type PendingCalendarSync = {
provider: "google" | "outlook";
mode: "full" | "incremental";
origin: string;
startedAt: number;
};
/** Returns a pending sync worth resuming, or clears and returns null. */
export async function readResumableCalendarSync(): Promise<PendingCalendarSync | null> {
const got = await browser.storage.local.get(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY);
const raw = got[BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY];
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
const pending = raw as PendingCalendarSync;
const validProvider = pending.provider === "google" || pending.provider === "outlook";
const validMode = pending.mode === "full" || pending.mode === "incremental";
const fresh = Date.now() - pending.startedAt < 24 * 60 * 60 * 1000;
if (
!validProvider ||
!validMode ||
pending.origin !== location.origin ||
!fresh
) {
await browser.storage.local.remove(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY);
return null;
}
return pending;
}
+56 -16
View File
@@ -9,7 +9,15 @@ import {
} from "@/seqta/utils/calendarSync/syncEngine";
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
import type { OutlookCalendarStatus } from "@/seqta/utils/calendarSync/providerStorage";
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
import {
readGoogleCalendarState,
readOutlookCalendarState,
} from "@/seqta/utils/calendarSync/providerStorage";
import {
BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY,
getSyncWeeksAhead,
readResumableCalendarSync,
} from "@/seqta/utils/calendarSync/settings";
import type {
GoogleCalendarStatus,
GoogleCalendarSyncProgress,
@@ -98,24 +106,56 @@ export async function runCalendarSync(
const mode = params.mode ?? "full";
const weeksAhead = await getSyncWeeksAhead();
reportSyncProgress(params.onProgress, {
phase: "preparing",
current: 0,
total: 1,
message: mode === "incremental" ? "Fetching new week…" : "Fetching timetable…",
await browser.storage.local.set({
[BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY]: {
provider: config.provider,
mode,
origin: location.origin,
startedAt: Date.now(),
},
});
const lessons =
mode === "incremental"
? await fetchTimetableLessons(trailingWeekRange(weeksAhead))
: await fetchTimetableForSync(weeksAhead);
try {
reportSyncProgress(params.onProgress, {
phase: "preparing",
current: 0,
total: 1,
message: mode === "incremental" ? "Fetching new week…" : "Fetching timetable…",
});
return syncLessonsToCalendar(
config.lessonSyncProvider,
{ origin: location.origin, lessons, mode, weeksAhead },
() => getCalendarAccessToken(config.provider),
{ onProgress: params.onProgress },
);
const lessons =
mode === "incremental"
? await fetchTimetableLessons(trailingWeekRange(weeksAhead))
: await fetchTimetableForSync(weeksAhead);
return await syncLessonsToCalendar(
config.lessonSyncProvider,
{ origin: location.origin, lessons, mode, weeksAhead },
() => getCalendarAccessToken(config.provider),
{ onProgress: params.onProgress },
);
} finally {
await browser.storage.local.remove(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY);
}
}
export async function resumeInterruptedCalendarSync(
params: RunCalendarSyncParams = {},
): Promise<GoogleCalendarSyncResult | null> {
const pending = await readResumableCalendarSync();
if (!pending) return null;
const state =
pending.provider === "google"
? await readGoogleCalendarState()
: await readOutlookCalendarState();
if (!state.refreshToken && !state.accessToken) {
await browser.storage.local.remove(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY);
return null;
}
const config = pending.provider === "google" ? GOOGLE_SYNC_RUNNER : OUTLOOK_SYNC_RUNNER;
return runCalendarSync(config, { ...params, mode: pending.mode });
}
export const runGoogleCalendarSync = (params?: RunCalendarSyncParams) =>
@@ -4,6 +4,7 @@ import {
normalizeThemeIdForSync,
resolveThemeIdForPostSyncDownload,
} from "./cloudSettingsSync";
import { BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY } from "@/seqta/utils/calendarSync/settings";
describe("migrateLegacyToPluginSettings", () => {
it("maps animatedbk without overwriting existing plugin fields", () => {
@@ -30,6 +31,7 @@ describe("isKeyIncludedInCloudUploadPayload", () => {
it("excludes auth and device cache prefixes", () => {
expect(isKeyIncludedInCloudUploadPayload("bsplus_token")).toBe(false);
expect(isKeyIncludedInCloudUploadPayload("bsplus_install_id")).toBe(false);
expect(isKeyIncludedInCloudUploadPayload(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY)).toBe(false);
expect(isKeyIncludedInCloudUploadPayload("plugin.global-search.storage.index")).toBe(
false,
);
+3
View File
@@ -1,5 +1,6 @@
import browser from "webextension-polyfill";
import isEqual from "@/seqta/utils/isEqual";
import { BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY } from "@/seqta/utils/calendarSync/settings";
/** Matches the contract in docs/CLOUD_SETTINGS_SYNC_SERVER.md */
export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1;
@@ -42,6 +43,7 @@ export const KEYS_OMITTED_FROM_CLOUD_UPLOAD = [
"bsplus_install_id",
/** Pending feedback ids awaiting a reply notification — device-local. */
"bsplus_pending_feedback_ids",
BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY,
] as const;
/**
@@ -64,6 +66,7 @@ const CLIENT_ONLY_CLOUD_KEYS_EXACT = [
BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY,
"bsplus_lastCloudPoll",
BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY,
BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY,
] as const;
/** After restoring from cloud, keep local session so the user stays signed in. */
@@ -4,7 +4,11 @@ import {
shouldRunWeeklySync,
} from "@/seqta/utils/calendarSync/settings";
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
import { runGoogleCalendarSync, runOutlookCalendarSync } from "@/seqta/utils/calendarSync/syncRunner";
import {
resumeInterruptedCalendarSync,
runGoogleCalendarSync,
runOutlookCalendarSync,
} from "@/seqta/utils/calendarSync/syncRunner";
import {
readGoogleCalendarState,
readOutlookCalendarState,
@@ -12,6 +16,7 @@ import {
import type { GoogleCalendarSyncResult } from "@/seqta/utils/googleCalendar/types";
let listenerRegistered = false;
let resumeChecked = false;
const WEEKLY_PROVIDERS = [
{ label: "Google Calendar", read: readGoogleCalendarState, run: runGoogleCalendarSync },
@@ -44,6 +49,10 @@ async function runWeeklySyncForConnectedProviders(): Promise<
}
export function registerCalendarContentHandlers(): void {
if (!resumeChecked) {
resumeChecked = true;
void resumeInterruptedCalendarSync();
}
if (listenerRegistered) return;
listenerRegistered = true;