feat: support outlook for calendar plus minor fixes

This commit is contained in:
2026-06-27 15:07:10 +09:30
parent 2163f06de9
commit d34333ca38
31 changed files with 1791 additions and 231 deletions
@@ -1,28 +1,60 @@
import browser from "webextension-polyfill";
import { shouldRunWeeklySync } from "@/seqta/utils/googleCalendar/syncSettings";
import {
markWeeklySyncComplete,
shouldRunWeeklySync,
} from "@/seqta/utils/calendarSync/settings";
import {
formatSyncResultMessage,
runGoogleCalendarSync,
} from "@/seqta/utils/googleCalendar/syncRunner";
import {
formatOutlookSyncResultMessage,
runOutlookCalendarSync,
} from "@/seqta/utils/outlookCalendar/syncRunner";
import { readGoogleCalendarState } from "@/seqta/utils/googleCalendar/storage";
import { readOutlookCalendarState } from "@/seqta/utils/outlookCalendar/storage";
import type { GoogleCalendarSyncResult } from "@/seqta/utils/googleCalendar/types";
let listenerRegistered = false;
export function registerGoogleCalendarContentHandlers(): void {
async function runWeeklySyncForConnectedProviders(): Promise<GoogleCalendarSyncResult[]> {
const [google, outlook] = await Promise.all([
readGoogleCalendarState(),
readOutlookCalendarState(),
]);
const results: GoogleCalendarSyncResult[] = [];
if (google.refreshToken || google.accessToken) {
results.push(await runGoogleCalendarSync({ mode: "incremental", silent: true }));
}
if (outlook.refreshToken || outlook.accessToken) {
results.push(await runOutlookCalendarSync({ mode: "incremental", silent: true }));
}
if (results.some((r) => r.success)) {
await markWeeklySyncComplete();
}
return results;
}
export function registerCalendarContentHandlers(): void {
if (listenerRegistered) return;
listenerRegistered = true;
browser.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request?.type !== "googleCalendarRunWeeklySync") return false;
void runGoogleCalendarSync({ mode: "incremental", silent: true })
.then((result: GoogleCalendarSyncResult) => sendResponse(result))
.catch((err: unknown) => {
sendResponse({
success: false,
error: err instanceof Error ? err.message : "Weekly sync failed",
if (request?.type === "calendarRunWeeklySync" || request?.type === "googleCalendarRunWeeklySync") {
void runWeeklySyncForConnectedProviders()
.then((results) => sendResponse({ success: true, results }))
.catch((err: unknown) => {
sendResponse({
success: false,
error: err instanceof Error ? err.message : "Weekly sync failed",
});
});
});
return true;
return true;
}
return false;
});
}
@@ -31,17 +63,38 @@ export async function maybeRunDueWeeklySync(
): Promise<void> {
if (!(await shouldRunWeeklySync())) return;
const result = await runGoogleCalendarSync({ mode: "incremental", silent: true });
const [google, outlook] = await Promise.all([
readGoogleCalendarState(),
readOutlookCalendarState(),
]);
const results = await runWeeklySyncForConnectedProviders();
if (!onComplete) return;
if (!result.success) {
onComplete(result.error ?? "Weekly calendar sync failed.", true);
const errors = results.filter((r) => !r.success);
if (errors.length > 0) {
onComplete(errors[0]?.error ?? "Weekly calendar sync failed.", true);
return;
}
const changed =
(result.created ?? 0) + (result.updated ?? 0) + (result.deleted ?? 0) > 0;
if (changed) {
onComplete(formatSyncResultMessage(result));
const messages: string[] = [];
let index = 0;
if (google.refreshToken || google.accessToken) {
const result = results[index++];
const changed =
(result.created ?? 0) + (result.updated ?? 0) + (result.deleted ?? 0) > 0;
if (changed) messages.push(formatSyncResultMessage(result));
}
if (outlook.refreshToken || outlook.accessToken) {
const result = results[index++];
const changed =
(result.created ?? 0) + (result.updated ?? 0) + (result.deleted ?? 0) > 0;
if (changed) messages.push(formatOutlookSyncResultMessage(result));
}
if (messages.length > 0) {
onComplete(messages.join(" "));
}
}
/** @deprecated use registerCalendarContentHandlers */
export const registerGoogleCalendarContentHandlers = registerCalendarContentHandlers;
@@ -15,7 +15,7 @@ jest.mock("@/seqta/utils/googleCalendar/storage", () => ({
writeGoogleCalendarState: jest.fn(async (patch: unknown) => patch),
}));
jest.mock("@/seqta/utils/googleCalendar/syncSettings", () => ({
jest.mock("@/seqta/utils/calendarSync/settings", () => ({
getSyncWeeksAhead: jest.fn(async () => 12),
}));
+1 -1
View File
@@ -10,7 +10,7 @@ import {
isDateInRange,
syncWindowRange,
} from "@/seqta/utils/googleCalendar/syncDateRange";
import { getSyncWeeksAhead } from "@/seqta/utils/googleCalendar/syncSettings";
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
import {
eventMapKey,
readGoogleCalendarState,
+1 -8
View File
@@ -4,10 +4,7 @@ import {
fetchTimetableLessons,
trailingWeekRange,
} from "@/seqta/utils/googleCalendar/fetchTimetable";
import {
getSyncWeeksAhead,
markWeeklySyncComplete,
} from "@/seqta/utils/googleCalendar/syncSettings";
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
import { syncLessonsToGoogleCalendar } from "@/seqta/utils/googleCalendar/syncEngine";
import type {
GoogleCalendarSyncOptions,
@@ -63,10 +60,6 @@ export async function runGoogleCalendarSync(
options,
);
if (result.success && mode === "incremental") {
await markWeeklySyncComplete();
}
return result;
}
+10 -57
View File
@@ -1,57 +1,10 @@
import {
GOOGLE_CALENDAR_SYNC_WEEKS_DEFAULT,
GOOGLE_CALENDAR_SYNC_WEEKS_MAX,
GOOGLE_CALENDAR_SYNC_WEEKS_MIN,
} from "@/config/googleCalendar";
import { readGoogleCalendarState, writeGoogleCalendarState } from "./storage";
export const GOOGLE_CALENDAR_WEEKLY_ALARM = "bsplus_google_calendar_weekly";
export const WEEKLY_SYNC_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
export function clampSyncWeeks(weeks: number): number {
if (!Number.isFinite(weeks)) return GOOGLE_CALENDAR_SYNC_WEEKS_DEFAULT;
return Math.min(
GOOGLE_CALENDAR_SYNC_WEEKS_MAX,
Math.max(GOOGLE_CALENDAR_SYNC_WEEKS_MIN, Math.round(weeks)),
);
}
export async function getSyncWeeksAhead(): Promise<number> {
const state = await readGoogleCalendarState();
return clampSyncWeeks(state.syncWeeksAhead ?? GOOGLE_CALENDAR_SYNC_WEEKS_DEFAULT);
}
export async function setSyncWeeksAhead(weeks: number): Promise<number> {
const syncWeeksAhead = clampSyncWeeks(weeks);
await writeGoogleCalendarState({ syncWeeksAhead });
return syncWeeksAhead;
}
export async function getAutoSyncWeekly(): Promise<boolean> {
const state = await readGoogleCalendarState();
return state.autoSyncWeekly !== false;
}
export async function setAutoSyncWeekly(enabled: boolean): Promise<void> {
await writeGoogleCalendarState({ autoSyncWeekly: enabled });
}
export async function shouldRunWeeklySync(): Promise<boolean> {
const state = await readGoogleCalendarState();
if (!state.refreshToken && !state.accessToken) return false;
if (state.autoSyncWeekly === false) return false;
if (state.pendingWeeklySync) return true;
const last = state.lastWeeklySyncAt ?? state.lastSyncAt ?? 0;
return Date.now() - last >= WEEKLY_SYNC_INTERVAL_MS;
}
export async function markWeeklySyncComplete(): Promise<void> {
await writeGoogleCalendarState({
lastWeeklySyncAt: Date.now(),
pendingWeeklySync: false,
});
}
export async function markWeeklySyncPending(): Promise<void> {
await writeGoogleCalendarState({ pendingWeeklySync: true });
}
export {
CALENDAR_WEEKLY_ALARM as GOOGLE_CALENDAR_WEEKLY_ALARM,
WEEKLY_SYNC_INTERVAL_MS,
clampSyncWeeks,
getAutoSyncWeekly,
getSyncWeeksAhead,
markWeeklySyncComplete,
markWeeklySyncPending,
shouldRunWeeklySync,
} from "@/seqta/utils/calendarSync/settings";