import { defaultInstanceIdForDriver, PROVIDER_DISPLAY_NAMES, type ProviderDriverKind, type ProviderInstanceId, type ServerProvider, } from "@t3tools/contracts"; export type ProviderUpdateCandidate = ServerProvider & { readonly versionAdvisory: NonNullable & { readonly status: "behind_latest"; readonly latestVersion: string; }; }; export type ProviderUpdateToastType = "warning" | "loading" | "error" | "success"; export type ProviderUpdateToastPhase = "initial" | "running" | "failed" | "unchanged" | "succeeded"; export interface ProviderUpdateToastView { readonly phase: ProviderUpdateToastPhase; readonly type: ProviderUpdateToastType; readonly title: string; readonly description: string; readonly dismissAfterVisibleMs?: number; } export type ProviderUpdateSidebarPillTone = "loading" | "warning" | "error" | "success"; export interface ProviderUpdateSidebarPillView { readonly key: string; readonly tone: ProviderUpdateSidebarPillTone; readonly title: string; readonly description: string; readonly dismissible?: boolean; readonly dismissAfterVisibleMs?: number; } interface ProviderUpdateSidebarPillOptions { readonly visibleAfterIso?: string; readonly dismissedKeys?: ReadonlySet; } const PROVIDER_UPDATE_SUCCESS_VISIBLE_MS = 3_000; function formatVersion(value: string): string { return value.startsWith("v") ? value : `v${value}`; } function chooseRepresentativeProvider( current: ServerProvider | undefined, candidate: ServerProvider, ): ServerProvider { if (!current) { return candidate; } const defaultInstanceId = defaultInstanceIdForDriver(candidate.driver); if (candidate.instanceId === defaultInstanceId) { return candidate; } if (current.instanceId === defaultInstanceId) { return current; } return candidate.checkedAt.localeCompare(current.checkedAt) >= 0 ? candidate : current; } function dedupeProvidersByDriver(providers: ReadonlyArray): T[] { const latestProviderByDriver = new Map(); for (const provider of providers) { latestProviderByDriver.set( provider.driver, chooseRepresentativeProvider(latestProviderByDriver.get(provider.driver), provider) as T, ); } return [...latestProviderByDriver.values()]; } function dedupeProvidersByInstanceId(providers: ReadonlyArray): T[] { const latestProviderByInstanceId = new Map(); for (const provider of providers) { const current = latestProviderByInstanceId.get(provider.instanceId); if (!current || provider.checkedAt.localeCompare(current.checkedAt) >= 0) { latestProviderByInstanceId.set(provider.instanceId, provider); } } return [...latestProviderByInstanceId.values()]; } function getProviderUpdatedTitle(provider: Pick): string { const providerName = PROVIDER_DISPLAY_NAMES[provider.driver] ?? provider.driver; return provider.version ? `${providerName} updated: ${formatVersion(provider.version)}` : `${providerName} updated`; } function getProviderUpdatedDescription(providerCount: number): string { return providerCount === 1 ? "New sessions will use the updated provider." : "New sessions will use the updated providers."; } function getProviderFailedUpdateTitle( provider: Pick, ): string { const providerName = PROVIDER_DISPLAY_NAMES[provider.driver] ?? provider.driver; const attemptedVersion = provider.versionAdvisory?.latestVersion; return attemptedVersion ? `${providerName} ${formatVersion(attemptedVersion)} update failed` : `${providerName} update failed`; } export function isProviderUpdateCandidate( provider: ServerProvider, ): provider is ProviderUpdateCandidate { return ( provider.enabled && provider.versionAdvisory?.status === "behind_latest" && provider.versionAdvisory.latestVersion !== null ); } export function isProviderUpdateActive(provider: Pick): boolean { return provider.updateState?.status === "queued" || provider.updateState?.status === "running"; } export function collectProviderUpdateCandidates( providers: ReadonlyArray, ): ProviderUpdateCandidate[] { return dedupeProvidersByDriver(providers.filter(isProviderUpdateCandidate)); } export function hasOneClickUpdateProviderCandidate( candidate: ProviderUpdateCandidate, providers: ReadonlyArray, ): boolean { if ( candidate.versionAdvisory.canUpdate !== true || candidate.versionAdvisory.updateCommand === null ) { return false; } const driverProviders = providers.filter((provider) => provider.driver === candidate.driver); if (driverProviders.length === 0) { return false; } const updateCommands = new Set(); for (const provider of driverProviders) { if (!isProviderUpdateCandidate(provider)) { continue; } const advisory = provider.versionAdvisory; if (!advisory || advisory.canUpdate !== true || advisory.updateCommand === null) { return false; } updateCommands.add(advisory.updateCommand); } return updateCommands.size === 1; } export function canOneClickUpdateProviderCandidate( candidate: ProviderUpdateCandidate, providers: ReadonlyArray, ): boolean { return ( !isProviderUpdateActive(candidate) && hasOneClickUpdateProviderCandidate(candidate, providers) ); } export function providerUpdateNotificationKey( providers: ReadonlyArray, ): string | null { const parts = dedupeProvidersByDriver(providers) .map((provider) => { const advisory = provider.versionAdvisory; return [provider.driver, advisory.latestVersion].join(":"); }) .toSorted(); return parts.length > 0 ? parts.join("|") : null; } export function providerUpdateCandidateKey(provider: ProviderUpdateCandidate): string { return providerUpdateNotificationKey([provider])!; } export function formatProviderList(providers: ReadonlyArray>) { const names = providers.map( (provider) => PROVIDER_DISPLAY_NAMES[provider.driver] ?? provider.driver, ); if (names.length <= 2) { return names.join(" and "); } return `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`; } export function getProviderUpdateInitialToastView(input: { readonly updateProviders: ReadonlyArray; readonly oneClickProviders: ReadonlyArray; }): ProviderUpdateToastView { return { phase: "initial", type: "warning", title: getProviderUpdateInitialToastTitle(input.updateProviders), description: input.oneClickProviders.length > 0 ? "Install the update now or review provider settings." : `${formatProviderList(input.updateProviders)} can be updated from provider settings.`, }; } export function getProviderUpdateRunningToastView(providerCount: number): ProviderUpdateToastView { return { phase: "running", type: "loading", title: providerCount === 1 ? "Updating provider" : "Updating providers", description: "Running provider update command.", }; } export function getProviderUpdateRejectedToastView( providerCount: number, message: string, ): ProviderUpdateToastView { return { phase: "failed", type: "error", title: providerCount === 1 ? "Provider update failed" : "Provider updates failed", description: message, }; } export function getProviderUpdateProgressToastView(input: { readonly providers: ReadonlyArray; readonly providerCount: number; }): ProviderUpdateToastView { const providers = dedupeProvidersByDriver(input.providers); const failedProviders = providers.filter((provider) => provider.updateState?.status === "failed"); if (failedProviders.length > 0) { return { phase: "failed", type: "error", title: failedProviders.length === 1 ? "Provider update failed" : "Provider updates failed", description: getFailedProviderUpdateDescription(failedProviders), }; } const unchangedProviders = providers.filter( (provider) => provider.updateState?.status === "unchanged", ); if (unchangedProviders.length > 0) { return { phase: "unchanged", type: "warning", title: unchangedProviders.length === 1 ? "Provider still needs an update" : "Providers still need updates", description: `${formatProviderList(unchangedProviders)} ${ unchangedProviders.length === 1 ? "still appears" : "still appear" } outdated. Check provider settings for details.`, }; } if (providers.some(isProviderUpdateActive)) { return getProviderUpdateRunningToastView(input.providerCount); } const hasCompleteProviderSnapshots = providers.length >= input.providerCount; const allProvidersUpdated = hasCompleteProviderSnapshots && providers.every( (provider) => provider.updateState?.status === "succeeded" || !isProviderUpdateCandidate(provider), ); if (allProvidersUpdated) { return { phase: "succeeded", type: "success", title: input.providerCount === 1 ? "Provider updated" : "Provider updates finished", description: getProviderUpdatedDescription(input.providerCount), dismissAfterVisibleMs: PROVIDER_UPDATE_SUCCESS_VISIBLE_MS, }; } return getProviderUpdateRunningToastView(input.providerCount); } export function getSingleProviderUpdateProgressToastView( provider: ServerProvider, ): ProviderUpdateToastView { const view = getProviderUpdateProgressToastView({ providers: [provider], providerCount: 1, }); const providerName = PROVIDER_DISPLAY_NAMES[provider.driver] ?? provider.driver; switch (view.phase) { case "running": return { ...view, title: `Updating ${providerName}`, }; case "failed": return { ...view, title: getProviderFailedUpdateTitle(provider), }; case "unchanged": return { ...view, title: `${providerName} still needs an update`, }; case "succeeded": return { ...view, title: getProviderUpdatedTitle(provider), }; default: return view; } } export function collectUpdatedProviderSnapshots(input: { readonly results: ReadonlyArray< PromiseSettledResult<{ readonly providers: ReadonlyArray }> >; readonly providerInstanceIds: ReadonlySet; }): ServerProvider[] { const matchedProviders: ServerProvider[] = []; for (const result of input.results) { if (result.status !== "fulfilled") { continue; } for (const provider of result.value.providers) { if (input.providerInstanceIds.has(provider.instanceId)) { matchedProviders.push(provider); } } } return dedupeProvidersByInstanceId(matchedProviders); } export function firstRejectedProviderUpdateMessage( results: ReadonlyArray>, ): string | null { const rejected = results.find((result) => result.status === "rejected"); if (!rejected) { return null; } return rejected.reason instanceof Error ? rejected.reason.message : "Provider update failed."; } function getUpdateFinishedAt(provider: ServerProvider): string | null { return provider.updateState?.finishedAt ?? null; } function isRecentTerminalProvider( provider: ServerProvider, visibleAfterIso: string | undefined, ): boolean { const status = provider.updateState?.status; if (status !== "failed" && status !== "unchanged" && status !== "succeeded") { return false; } if (visibleAfterIso === undefined) { return true; } const finishedAt = getUpdateFinishedAt(provider); return finishedAt !== null && finishedAt >= visibleAfterIso; } function latestFinishedAtForProviders(providers: ReadonlyArray): string | null { return providers.reduce((latest, provider) => { const finishedAt = getUpdateFinishedAt(provider); if (finishedAt === null) { return latest; } return latest === null || finishedAt > latest ? finishedAt : latest; }, null); } export function getProviderUpdateSidebarPillView( providers: ReadonlyArray, options?: ProviderUpdateSidebarPillOptions, ): ProviderUpdateSidebarPillView | null { const dedupedProviders = dedupeProvidersByDriver(providers); const activeProviders = dedupedProviders.filter(isProviderUpdateActive); if (activeProviders.length > 0) { const activeProvider = activeProviders[0]!; const activeProviderName = PROVIDER_DISPLAY_NAMES[activeProvider.driver] ?? activeProvider.driver; return { key: `loading:${activeProviders .map((provider) => `${provider.driver}:${provider.updateState?.status ?? "idle"}`) .toSorted() .join("|")}`, tone: "loading", title: activeProviders.length === 1 ? `Updating ${activeProviderName}` : `Updating ${activeProviders.length} providers`, description: activeProviders.length === 1 ? `${formatProviderList(activeProviders)} update in progress.` : `${formatProviderList(activeProviders)} updates are in progress.`, }; } const recentTerminalProviders = dedupedProviders.filter((provider) => isRecentTerminalProvider(provider, options?.visibleAfterIso), ); const terminalCandidates: ProviderUpdateSidebarPillView[] = []; const failedProviders = recentTerminalProviders.filter( (provider) => provider.updateState?.status === "failed", ); if (failedProviders.length > 0) { const failedProvider = failedProviders[0]!; terminalCandidates.push({ key: `failed:${failedProviders .map( (provider) => `${provider.driver}:${provider.updateState?.finishedAt ?? "pending"}:${provider.updateState?.message ?? ""}`, ) .toSorted() .join("|")}`, tone: "error", title: failedProviders.length === 1 ? getProviderFailedUpdateTitle(failedProvider) : `${failedProviders.length} provider updates failed`, description: getFailedProviderUpdateDescription(failedProviders), dismissible: true, }); } const unchangedProviders = recentTerminalProviders.filter( (provider) => provider.updateState?.status === "unchanged", ); if (unchangedProviders.length > 0) { const unchangedProvider = unchangedProviders[0]!; const unchangedProviderName = PROVIDER_DISPLAY_NAMES[unchangedProvider.driver] ?? unchangedProvider.driver; terminalCandidates.push({ key: `unchanged:${unchangedProviders .map( (provider) => `${provider.driver}:${provider.updateState?.finishedAt ?? "pending"}:${provider.updateState?.message ?? ""}`, ) .toSorted() .join("|")}`, tone: "warning", title: unchangedProviders.length === 1 ? `${unchangedProviderName} still needs an update` : `${unchangedProviders.length} providers still need updates`, description: `${formatProviderList(unchangedProviders)} ${ unchangedProviders.length === 1 ? "still appears" : "still appear" } outdated. Review provider settings for details.`, dismissible: true, }); } const succeededProviders = recentTerminalProviders.filter( (provider) => provider.updateState?.status === "succeeded", ); if (succeededProviders.length > 0) { const succeededProvider = succeededProviders[0]!; terminalCandidates.push({ key: `succeeded:${succeededProviders .map( (provider) => `${provider.driver}:${provider.updateState?.finishedAt ?? "pending"}:${provider.updateState?.message ?? ""}`, ) .toSorted() .join("|")}`, tone: "success", title: succeededProviders.length === 1 ? getProviderUpdatedTitle(succeededProvider) : `${succeededProviders.length} providers updated`, description: getProviderUpdatedDescription(succeededProviders.length), dismissAfterVisibleMs: PROVIDER_UPDATE_SUCCESS_VISIBLE_MS, }); } return ( terminalCandidates .toSorted((left, right) => { const leftProviders = left.tone === "error" ? failedProviders : left.tone === "warning" ? unchangedProviders : succeededProviders; const rightProviders = right.tone === "error" ? failedProviders : right.tone === "warning" ? unchangedProviders : succeededProviders; const leftFinishedAt = latestFinishedAtForProviders(leftProviders) ?? ""; const rightFinishedAt = latestFinishedAtForProviders(rightProviders) ?? ""; return rightFinishedAt.localeCompare(leftFinishedAt); }) .find((candidate) => !options?.dismissedKeys?.has(candidate.key)) ?? null ); } function getProviderUpdateInitialToastTitle( providers: ReadonlyArray, ): string { if (providers.length === 1) { const provider = providers[0]!; const providerName = PROVIDER_DISPLAY_NAMES[provider.driver] ?? provider.driver; return `Update Available: ${providerName} ${formatVersion(provider.versionAdvisory.latestVersion)}`; } return `Updates Available: ${providers.length} providers`; } function getFailedProviderUpdateDescription(providers: ReadonlyArray): string { if (providers.length === 1) { const provider = providers[0]!; if (provider.updateState?.message) { return provider.updateState.message; } } return `${formatProviderList(providers)} failed to update. Check provider settings for details.`; }