import { safeLocalStorageGet, safeLocalStorageRemove, safeLocalStorageSet } from "@/lib/backend/safeStorage"; const PENDING_COMPONENT_UPDATES_STORAGE_KEY = "dbx:updates:pending-components-after-restart"; export const COMPONENT_UPDATE_CATEGORIES = ["drivers", "jdbc", "mcp", "plugins"] as const; export type ComponentUpdateCategory = (typeof COMPONENT_UPDATE_CATEGORIES)[number]; export type PendingComponentUpdatePlan = { kind: "auto" } | { kind: "manual"; categories: ComponentUpdateCategory[] }; export interface PendingComponentUpdates { fromVersion: string; targetVersion: string; plan: PendingComponentUpdatePlan; } export type UpdateAllAction = "none" | "download-app" | "install-app" | "update-components"; export function updateBlockerLabels(blockers: Array<{ label: string; connections?: string[] }>): string[] { return [...new Set(blockers.flatMap((blocker) => (blocker.connections?.length ? blocker.connections : [blocker.label])))]; } function normalizeVersion(version: string): string { return version.trim().replace(/^[vV]/, ""); } function isComponentUpdateCategory(value: unknown): value is ComponentUpdateCategory { return typeof value === "string" && COMPONENT_UPDATE_CATEGORIES.includes(value as ComponentUpdateCategory); } function normalizeManualCategories(value: unknown): ComponentUpdateCategory[] | null { if (!Array.isArray(value) || !value.every(isComponentUpdateCategory)) return null; return [...new Set(value)]; } function normalizePendingComponentUpdatePlan(plan: PendingComponentUpdatePlan): PendingComponentUpdatePlan | null { if (plan.kind === "auto") return plan; const categories = normalizeManualCategories(plan.categories); return categories ? { kind: "manual", categories } : null; } function parsePendingComponentUpdatePlan(value: unknown): PendingComponentUpdatePlan | null { if (value === undefined) return { kind: "auto" }; if (!value || typeof value !== "object") return null; const plan = value as { kind?: unknown; categories?: unknown }; if (plan.kind === "auto") return { kind: "auto" }; if (plan.kind !== "manual") return null; const categories = normalizeManualCategories(plan.categories); return categories ? { kind: "manual", categories } : null; } function parsePendingComponentUpdates(raw: string | null): PendingComponentUpdates | null { if (!raw) return null; try { const parsed = JSON.parse(raw) as Partial> & { plan?: unknown }; const fromVersion = typeof parsed.fromVersion === "string" ? parsed.fromVersion.trim() : ""; const targetVersion = typeof parsed.targetVersion === "string" ? parsed.targetVersion.trim() : ""; const plan = parsePendingComponentUpdatePlan(parsed.plan); return fromVersion && plan ? { fromVersion, targetVersion, plan } : null; } catch { return null; } } export function markPendingComponentUpdatesAfterAppUpdate(fromVersion: string, targetVersion: string, plan: PendingComponentUpdatePlan = { kind: "auto" }): boolean { const normalizedFromVersion = fromVersion.trim(); if (!normalizedFromVersion) return false; const normalizedPlan = normalizePendingComponentUpdatePlan(plan); if (!normalizedPlan) return false; const existing = parsePendingComponentUpdates(safeLocalStorageGet(PENDING_COMPONENT_UPDATES_STORAGE_KEY)); if (normalizedPlan.kind === "auto" && existing?.plan.kind === "manual" && normalizeVersion(existing.fromVersion) === normalizeVersion(normalizedFromVersion)) return true; return safeLocalStorageSet( PENDING_COMPONENT_UPDATES_STORAGE_KEY, JSON.stringify({ fromVersion: normalizedFromVersion, targetVersion: targetVersion.trim(), plan: normalizedPlan, } satisfies PendingComponentUpdates), ); } export function clearPendingComponentUpdatesAfterAppUpdate() { safeLocalStorageRemove(PENDING_COMPONENT_UPDATES_STORAGE_KEY); } export function hasPendingComponentUpdatesAfterAppRestart(): boolean { return parsePendingComponentUpdates(safeLocalStorageGet(PENDING_COMPONENT_UPDATES_STORAGE_KEY)) !== null; } export async function runPendingComponentUpdatesBeforePluginReconnect(options: { hasPendingComponentUpdates: () => boolean; prepareStartup: () => Promise; consumePendingComponentUpdates: () => Promise; reconnectRestoredPluginTabs: () => Promise }): Promise { if (!options.hasPendingComponentUpdates()) { void options.reconnectRestoredPluginTabs(); await options.prepareStartup(); return; } try { await options.prepareStartup(); await options.consumePendingComponentUpdates(); } finally { void options.reconnectRestoredPluginTabs(); } } export function takePendingComponentUpdatesAfterAppRestart(currentVersion: string): PendingComponentUpdates | null { const pending = parsePendingComponentUpdates(safeLocalStorageGet(PENDING_COMPONENT_UPDATES_STORAGE_KEY)); if (!pending) { clearPendingComponentUpdatesAfterAppUpdate(); return null; } const normalizedCurrentVersion = normalizeVersion(currentVersion); if (!normalizedCurrentVersion || normalizedCurrentVersion === normalizeVersion(pending.fromVersion)) return null; clearPendingComponentUpdatesAfterAppUpdate(); return pending; } export function resolveUpdateAllAction(options: { hasAppUpdate: boolean; appUpdateCanInstall: boolean; appUpdatePrepared: boolean; hasComponentUpdates: boolean }): UpdateAllAction { if (!options.hasAppUpdate || !options.appUpdateCanInstall) return options.hasComponentUpdates ? "update-components" : "none"; return options.appUpdatePrepared ? "install-app" : "download-app"; } export async function continuePreparedAppUpdate(options: { hasComponentUpdates: boolean; restartOnly: boolean; rememberComponentUpdates: () => Promise; installComponents: () => Promise; installDownloadedUpdate: () => Promise; restartApp: () => Promise }): Promise { if (options.hasComponentUpdates && !(await options.rememberComponentUpdates())) { await options.installComponents(); return; } if (options.restartOnly) await options.restartApp(); else await options.installDownloadedUpdate(); } export function shouldCloseUpdateCenterAfterComponentUpdate(options: { failedCount: number; skippedDriverCount: number; hasAppUpdate: boolean; remainingComponentUpdateCount: number }): boolean { return options.failedCount === 0 && options.skippedDriverCount === 0 && !options.hasAppUpdate && options.remainingComponentUpdateCount === 0; } export function runPendingComponentUpdatePlan(pending: PendingComponentUpdates, updates: { installCategories: (categories: ComponentUpdateCategory[]) => Promise; autoUpdateEnabledComponents: () => Promise }): Promise { return pending.plan.kind === "manual" ? updates.installCategories(pending.plan.categories) : updates.autoUpdateEnabledComponents(); }