mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat: background resuming for calendar syncing
This commit is contained in:
@@ -3,7 +3,6 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
|||||||
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
|
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
|
||||||
import { ensureFontLoaded } from "@/seqta/ui/fonts/Manager";
|
import { ensureFontLoaded } from "@/seqta/ui/fonts/Manager";
|
||||||
import { getFontPreset } from "@/seqta/ui/fonts/presets";
|
import { getFontPreset } from "@/seqta/ui/fonts/presets";
|
||||||
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
|
|
||||||
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
|
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
|
||||||
import hostStyles from "./calendarSyncHost.css?inline";
|
import hostStyles from "./calendarSyncHost.css?inline";
|
||||||
|
|
||||||
@@ -152,7 +151,6 @@ export async function mountGoogleCalendarButton(): Promise<void> {
|
|||||||
if (!toolbar) return;
|
if (!toolbar) return;
|
||||||
|
|
||||||
ensureHostStyles();
|
ensureHostStyles();
|
||||||
registerCalendarContentHandlers();
|
|
||||||
|
|
||||||
const controls = document.createElement("div");
|
const controls = document.createElement("div");
|
||||||
controls.className = CONTROLS_CLASS;
|
controls.className = CONTROLS_CLASS;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
|
|||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
import { verboseLog } from "@/utils/verboseLog";
|
import { verboseLog } from "@/utils/verboseLog";
|
||||||
import { mountGoogleCalendarButton, unmountGoogleCalendarButton } from "./calendarSyncUi";
|
import { mountGoogleCalendarButton, unmountGoogleCalendarButton } from "./calendarSyncUi";
|
||||||
|
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
|
||||||
|
|
||||||
const timetablePlugin: Plugin<{}, {}> = {
|
const timetablePlugin: Plugin<{}, {}> = {
|
||||||
id: "timetable",
|
id: "timetable",
|
||||||
@@ -15,6 +16,8 @@ const timetablePlugin: Plugin<{}, {}> = {
|
|||||||
disableToggle: true,
|
disableToggle: true,
|
||||||
|
|
||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
|
registerCalendarContentHandlers();
|
||||||
|
|
||||||
const { unregister } = api.seqta.onMount(".timetablepage", handleTimetable);
|
const { unregister } = api.seqta.onMount(".timetablepage", handleTimetable);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
} from "@/seqta/utils/calendarSync/providerStorage";
|
} from "@/seqta/utils/calendarSync/providerStorage";
|
||||||
|
|
||||||
export const BSPLUS_CALENDAR_SYNC_SETTINGS_KEY = "bsplus_calendar_sync_settings";
|
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 CALENDAR_WEEKLY_ALARM = "bsplus_calendar_weekly";
|
||||||
export const WEEKLY_SYNC_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
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> {
|
export async function markWeeklySyncPending(): Promise<void> {
|
||||||
await writeSharedCalendarSyncSettings({ pendingWeeklySync: true });
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,15 @@ import {
|
|||||||
} from "@/seqta/utils/calendarSync/syncEngine";
|
} from "@/seqta/utils/calendarSync/syncEngine";
|
||||||
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
||||||
import type { OutlookCalendarStatus } from "@/seqta/utils/calendarSync/providerStorage";
|
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 {
|
import type {
|
||||||
GoogleCalendarStatus,
|
GoogleCalendarStatus,
|
||||||
GoogleCalendarSyncProgress,
|
GoogleCalendarSyncProgress,
|
||||||
@@ -98,6 +106,16 @@ export async function runCalendarSync(
|
|||||||
const mode = params.mode ?? "full";
|
const mode = params.mode ?? "full";
|
||||||
const weeksAhead = await getSyncWeeksAhead();
|
const weeksAhead = await getSyncWeeksAhead();
|
||||||
|
|
||||||
|
await browser.storage.local.set({
|
||||||
|
[BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY]: {
|
||||||
|
provider: config.provider,
|
||||||
|
mode,
|
||||||
|
origin: location.origin,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
reportSyncProgress(params.onProgress, {
|
reportSyncProgress(params.onProgress, {
|
||||||
phase: "preparing",
|
phase: "preparing",
|
||||||
current: 0,
|
current: 0,
|
||||||
@@ -110,12 +128,34 @@ export async function runCalendarSync(
|
|||||||
? await fetchTimetableLessons(trailingWeekRange(weeksAhead))
|
? await fetchTimetableLessons(trailingWeekRange(weeksAhead))
|
||||||
: await fetchTimetableForSync(weeksAhead);
|
: await fetchTimetableForSync(weeksAhead);
|
||||||
|
|
||||||
return syncLessonsToCalendar(
|
return await syncLessonsToCalendar(
|
||||||
config.lessonSyncProvider,
|
config.lessonSyncProvider,
|
||||||
{ origin: location.origin, lessons, mode, weeksAhead },
|
{ origin: location.origin, lessons, mode, weeksAhead },
|
||||||
() => getCalendarAccessToken(config.provider),
|
() => getCalendarAccessToken(config.provider),
|
||||||
{ onProgress: params.onProgress },
|
{ 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) =>
|
export const runGoogleCalendarSync = (params?: RunCalendarSyncParams) =>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
normalizeThemeIdForSync,
|
normalizeThemeIdForSync,
|
||||||
resolveThemeIdForPostSyncDownload,
|
resolveThemeIdForPostSyncDownload,
|
||||||
} from "./cloudSettingsSync";
|
} from "./cloudSettingsSync";
|
||||||
|
import { BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY } from "@/seqta/utils/calendarSync/settings";
|
||||||
|
|
||||||
describe("migrateLegacyToPluginSettings", () => {
|
describe("migrateLegacyToPluginSettings", () => {
|
||||||
it("maps animatedbk without overwriting existing plugin fields", () => {
|
it("maps animatedbk without overwriting existing plugin fields", () => {
|
||||||
@@ -30,6 +31,7 @@ describe("isKeyIncludedInCloudUploadPayload", () => {
|
|||||||
it("excludes auth and device cache prefixes", () => {
|
it("excludes auth and device cache prefixes", () => {
|
||||||
expect(isKeyIncludedInCloudUploadPayload("bsplus_token")).toBe(false);
|
expect(isKeyIncludedInCloudUploadPayload("bsplus_token")).toBe(false);
|
||||||
expect(isKeyIncludedInCloudUploadPayload("bsplus_install_id")).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(
|
expect(isKeyIncludedInCloudUploadPayload("plugin.global-search.storage.index")).toBe(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import isEqual from "@/seqta/utils/isEqual";
|
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 */
|
/** Matches the contract in docs/CLOUD_SETTINGS_SYNC_SERVER.md */
|
||||||
export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1;
|
export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1;
|
||||||
@@ -42,6 +43,7 @@ export const KEYS_OMITTED_FROM_CLOUD_UPLOAD = [
|
|||||||
"bsplus_install_id",
|
"bsplus_install_id",
|
||||||
/** Pending feedback ids awaiting a reply notification — device-local. */
|
/** Pending feedback ids awaiting a reply notification — device-local. */
|
||||||
"bsplus_pending_feedback_ids",
|
"bsplus_pending_feedback_ids",
|
||||||
|
BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +66,7 @@ const CLIENT_ONLY_CLOUD_KEYS_EXACT = [
|
|||||||
BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY,
|
BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY,
|
||||||
"bsplus_lastCloudPoll",
|
"bsplus_lastCloudPoll",
|
||||||
BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY,
|
BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY,
|
||||||
|
BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** After restoring from cloud, keep local session so the user stays signed in. */
|
/** After restoring from cloud, keep local session so the user stays signed in. */
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import {
|
|||||||
shouldRunWeeklySync,
|
shouldRunWeeklySync,
|
||||||
} from "@/seqta/utils/calendarSync/settings";
|
} from "@/seqta/utils/calendarSync/settings";
|
||||||
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
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 {
|
import {
|
||||||
readGoogleCalendarState,
|
readGoogleCalendarState,
|
||||||
readOutlookCalendarState,
|
readOutlookCalendarState,
|
||||||
@@ -12,6 +16,7 @@ import {
|
|||||||
import type { GoogleCalendarSyncResult } from "@/seqta/utils/googleCalendar/types";
|
import type { GoogleCalendarSyncResult } from "@/seqta/utils/googleCalendar/types";
|
||||||
|
|
||||||
let listenerRegistered = false;
|
let listenerRegistered = false;
|
||||||
|
let resumeChecked = false;
|
||||||
|
|
||||||
const WEEKLY_PROVIDERS = [
|
const WEEKLY_PROVIDERS = [
|
||||||
{ label: "Google Calendar", read: readGoogleCalendarState, run: runGoogleCalendarSync },
|
{ label: "Google Calendar", read: readGoogleCalendarState, run: runGoogleCalendarSync },
|
||||||
@@ -44,6 +49,10 @@ async function runWeeklySyncForConnectedProviders(): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerCalendarContentHandlers(): void {
|
export function registerCalendarContentHandlers(): void {
|
||||||
|
if (!resumeChecked) {
|
||||||
|
resumeChecked = true;
|
||||||
|
void resumeInterruptedCalendarSync();
|
||||||
|
}
|
||||||
if (listenerRegistered) return;
|
if (listenerRegistered) return;
|
||||||
listenerRegistered = true;
|
listenerRegistered = true;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user