import { createAuthController, type AuthController } from "./auth/controller"; import { createLogtoAuthClient } from "./auth/client"; import { isAuthRequestMessage, type AuthResponseMessage } from "../shared/auth-messages"; import { createBatchSubmitClient } from "../shared/batch-submit-client"; import { createBackendMetricsClient } from "../shared/backend-metrics-client"; import { DEFAULT_BACKEND_METRICS_BASE_URL } from "../shared/backend-metrics-config"; import { isBackendMetricsSearchRequestMessage } from "../shared/backend-metrics-messages"; interface ChromeDownloadsLike { download( options: { filename: string; saveAs?: boolean; url: string; }, callback?: () => void ): Promise | void; } interface ChromeRuntimeLike { onMessage?: { addListener( listener: ( message: unknown, sender: unknown, sendResponse: (response: unknown) => void ) => boolean | void ): void; }; } interface ChromeLike { downloads?: ChromeDownloadsLike; runtime?: ChromeRuntimeLike; } type DownloadMarketCsvMessage = { csv: string; filename: string; type: "download-market-csv"; }; type BatchSubmitMessage = { payload: unknown; type: "batch:submit"; }; export function registerBackgroundMessageHandler( chromeLike: ChromeLike = readChromeLike(), dependencies: { authController?: AuthController; searchBackendMetrics?: (starIds: string[]) => Promise; submitBatch?: (payload: unknown) => Promise; } = {} ): void { let authController = dependencies.authController; let searchBackendMetrics = dependencies.searchBackendMetrics; let submitBatch = dependencies.submitBatch; chromeLike.runtime?.onMessage?.addListener((message, _sender, sendResponse) => { if (isDownloadMarketCsvMessage(message)) { void triggerCsvDownload(chromeLike, message) .then(() => { sendResponse({ ok: true }); }) .catch((error) => { sendResponse({ error: error instanceof Error ? error.message : String(error), ok: false }); }); return true; } if (isBatchSubmitMessage(message)) { authController ??= createAuthController({ authClient: createLogtoAuthClient() }); submitBatch ??= createBatchSubmitClient({ baseUrl: "http://127.0.0.1:4319", getAccessToken: () => authController!.getAccessToken(), sendMessage: () => Promise.reject(new Error("background batch submit does not use sendMessage")) }).submitBatch; void submitBatch(message.payload) .then((value) => { sendResponse({ ok: true, type: "batch:ack", value }); }) .catch((error) => { sendResponse({ error: error instanceof Error ? error.message : String(error), ok: false, type: "batch:error" }); }); return true; } if (isBackendMetricsSearchRequestMessage(message)) { authController ??= createAuthController({ authClient: createLogtoAuthClient() }); searchBackendMetrics ??= createBackendMetricsClient({ baseUrl: DEFAULT_BACKEND_METRICS_BASE_URL, getAccessToken: () => authController!.getAccessToken() }).searchByStarIds; void searchBackendMetrics(message.value.starIds) .then((rows) => { sendResponse({ ok: true, type: "backend-metrics:result", value: { rows } }); }) .catch((error) => { sendResponse({ error: error instanceof Error ? error.message : String(error), ok: false, type: "backend-metrics:error" }); }); return true; } if (!isAuthRequestMessage(message)) { return; } authController ??= createAuthController({ authClient: createLogtoAuthClient() }); void handleAuthMessage(authController, message) .then((response) => { sendResponse(response); }) .catch((error) => { sendResponse({ error: error instanceof Error ? error.message : String(error), ok: false, type: "auth:error" } satisfies AuthResponseMessage); }); return true; }); } async function handleAuthMessage( authController: AuthController, message: Parameters[0] & { type: string } ): Promise { if (message.type === "auth:get-state") { return { ok: true, type: "auth:state", value: await authController.getAuthState() }; } if (message.type === "auth:get-access-token") { return { ok: true, type: "auth:token", value: { accessToken: await authController.getAccessToken() } }; } if (message.type === "auth:sign-in") { await authController.signIn(); return { ok: true, type: "auth:ack" }; } await authController.signOut(); return { ok: true, type: "auth:ack" }; } function readChromeLike(): ChromeLike { return ( globalThis as typeof globalThis & { chrome?: ChromeLike; } ).chrome ?? {}; } async function triggerCsvDownload( chromeLike: ChromeLike, message: DownloadMarketCsvMessage ): Promise { if (!chromeLike.downloads?.download) { throw new Error("chrome.downloads.download is unavailable"); } const csvUrl = `data:text/csv;charset=utf-8,${encodeURIComponent(`\uFEFF${message.csv}`)}`; await Promise.resolve( chromeLike.downloads.download({ filename: message.filename, saveAs: false, url: csvUrl }) ); } function isDownloadMarketCsvMessage( message: unknown ): message is DownloadMarketCsvMessage { if (!message || typeof message !== "object") { return false; } const candidate = message as Partial; return ( candidate.type === "download-market-csv" && typeof candidate.csv === "string" && typeof candidate.filename === "string" ); } function isBatchSubmitMessage(message: unknown): message is BatchSubmitMessage { if (!message || typeof message !== "object") { return false; } const candidate = message as Partial; return candidate.type === "batch:submit" && "payload" in candidate; } registerBackgroundMessageHandler();