| 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { PROVIDER_DISPLAY_NAMES } from "@t3tools/contracts"; |
| 3 | import { Atom } from "effect/reactivity"; |
| 4 | import { useMemo } from "react"; |
| 5 | |
| 6 | import { APP_VERSION } from "~/branding"; |
| 7 | import { isDesktopLocalConnectionTarget } from "~/connection/desktopLocal"; |
| 8 | import { isElectron } from "~/env"; |
| 9 | import { useEnvironments } from "~/state/environments"; |
| 10 | import { useNpmReleasePublished } from "~/state/npmRelease"; |
| 11 | import { serverEnvironment } from "~/state/server"; |
| 12 | import { environmentSession } from "~/state/session"; |
| 13 | import { getProviderUpdateRunOutcome } from "../ProviderUpdateLaunchNotification.logic"; |
| 14 | import { useProviderUpdates } from "../ProviderUpdatesAction"; |
| 15 | import { canUpdateServer, useServerUpdate } from "../ServerUpdateAction"; |
| 16 | import { |
| 17 | collectRestartingMachines, |
| 18 | collectServerUpdateTargets, |
| 19 | collectUpdateEverythingResult, |
| 20 | describeUpdateEverything, |
| 21 | runUpdateEverything, |
| 22 | type SkippedMachine, |
| 23 | type UpdateRunMachine, |
| 24 | } from "./updateEverything.logic"; |
| 25 | import { |
| 26 | clearSavedUpdateEverythingResult, |
| 27 | saveUpdateEverythingResult, |
| 28 | setUpdateEverythingRun, |
| 29 | settleUpdateRunMachine, |
| 30 | showUpdateEverythingResult, |
| 31 | } from "./updateEverythingRun"; |
| 32 | |
| 33 | /** Where this app's own download or install reports a failure. */ |
| 34 | export type ReportLocalUpdateError = (title: string, description: string) => void; |
| 35 | |
| 36 | /** |
| 37 | * The remote half of the app menu updater: outdated providers on every |
| 38 | * connected machine, and servers behind the version this client runs or is |
| 39 | * about to install (`localUpdateVersion`). Servers that install from npm appear |
| 40 | * only once npm confirms that version. `run` performs the whole pass, including |
| 41 | * this app's own download and install when given. It updates quietly, tracks |
| 42 | * each machine in the run state, and reports one summary toast at the end. |
| 43 | * When this app installs, the summary waits for the relaunch. |
| 44 | */ |
| 45 | export function useUpdateEverything(localUpdateVersion: string | null) { |
| 46 | const targetVersion = localUpdateVersion ?? APP_VERSION; |
| 47 | const { environments } = useEnvironments(); |
| 48 | const machinesAtom = useMemo( |
| 49 | () => |
| 50 | Atom.make((get) => |
| 51 | environments.map((environment) => ({ |
| 52 | environmentId: environment.environmentId, |
| 53 | label: environment.label, |
| 54 | serverConfig: environment.serverConfig, |
| 55 | connected: environment.connection.phase === "connected", |
| 56 | hostedByThisApp: |
| 57 | isElectron && |
| 58 | (environment.entry.target._tag === "PrimaryConnectionTarget" || |
| 59 | isDesktopLocalConnectionTarget(environment.entry.target)), |
| 60 | canMaintain: canUpdateServer( |
| 61 | get(environmentSession.sessionStateAtom(environment.environmentId)), |
| 62 | ), |
| 63 | updateState: get(serverEnvironment.updateStateAtom(environment.environmentId)), |
| 64 | })), |
| 65 | ), |
| 66 | [environments], |
| 67 | ); |
| 68 | const machines = useAtomValue(machinesAtom); |
| 69 | const { targets: behindServers, skipped: unreachable } = collectServerUpdateTargets( |
| 70 | machines, |
| 71 | targetVersion, |
| 72 | ); |
| 73 | // A desktop-managed server installs from its own app's update feed, so only |
| 74 | // servers that download t3 from npm wait for npm to publish the version. |
| 75 | const needsNpm = behindServers.some((server) => server.selfUpdate !== "desktop-managed"); |
| 76 | const published = useNpmReleasePublished(needsNpm ? targetVersion : null); |
| 77 | const servers = published |
| 78 | ? behindServers |
| 79 | : behindServers.filter((server) => server.selfUpdate === "desktop-managed"); |
| 80 | const skipped: ReadonlyArray<SkippedMachine> = [ |
| 81 | ...unreachable, |
| 82 | ...behindServers |
| 83 | .filter((server) => !servers.includes(server)) |
| 84 | .map((server) => ({ |
| 85 | environmentId: server.environmentId, |
| 86 | label: server.serverLabel, |
| 87 | reason: `t3@${targetVersion} is not on npm yet`, |
| 88 | })), |
| 89 | ]; |
| 90 | const restarts = collectRestartingMachines(machines, servers, localUpdateVersion !== null); |
| 91 | const providers = useProviderUpdates(); |
| 92 | const updateServer = useServerUpdate(); |
| 93 | |
| 94 | const { summary, lines } = describeUpdateEverything({ |
| 95 | localVersion: localUpdateVersion, |
| 96 | servers, |
| 97 | providerMachines: providers.machines.map((machine) => ({ |
| 98 | environmentId: machine.environmentId, |
| 99 | label: machine.label, |
| 100 | providers: machine.candidates.map( |
| 101 | (candidate) => PROVIDER_DISPLAY_NAMES[candidate.driver] ?? candidate.driver, |
| 102 | ), |
| 103 | })), |
| 104 | }); |
| 105 | |
| 106 | return { |
| 107 | servers, |
| 108 | hasUpdates: servers.length > 0 || providers.machines.length > 0, |
| 109 | /** Short label for the tooltip and the confirmation title. */ |
| 110 | summary, |
| 111 | /** One line per machine, for the confirmation. */ |
| 112 | lines, |
| 113 | /** The servers this run restarts, which stops their running threads. */ |
| 114 | restarts, |
| 115 | /** Behind machines the run leaves alone, with the reason. */ |
| 116 | skipped, |
| 117 | /** Runs the pass. `installLocal` resolves true when this app is relaunching. |
| 118 | Both report failures to the pass, which lists them in its summary. */ |
| 119 | run: async ({ |
| 120 | downloadLocal, |
| 121 | installLocal, |
| 122 | }: { |
| 123 | downloadLocal?: (reportError: ReportLocalUpdateError) => Promise<boolean>; |
| 124 | installLocal?: (reportError: ReportLocalUpdateError) => Promise<boolean>; |
| 125 | }) => { |
| 126 | const localFailures: Array<string> = []; |
| 127 | const reportLocalError: ReportLocalUpdateError = (title, description) => { |
| 128 | localFailures.push(`${title}: ${description}`); |
| 129 | }; |
| 130 | const machinesById = new Map<UpdateRunMachine["environmentId"], UpdateRunMachine>(); |
| 131 | for (const machine of providers.machines) { |
| 132 | machinesById.set(machine.environmentId, { |
| 133 | environmentId: machine.environmentId, |
| 134 | label: machine.label, |
| 135 | updatesProviders: true, |
| 136 | updatesServer: false, |
| 137 | result: null, |
| 138 | }); |
| 139 | } |
| 140 | for (const server of servers) { |
| 141 | const existing = machinesById.get(server.environmentId); |
| 142 | machinesById.set(server.environmentId, { |
| 143 | environmentId: server.environmentId, |
| 144 | label: server.serverLabel, |
| 145 | updatesProviders: existing?.updatesProviders ?? false, |
| 146 | updatesServer: true, |
| 147 | result: null, |
| 148 | }); |
| 149 | } |
| 150 | const localVersion = installLocal ? localUpdateVersion : null; |
| 151 | setUpdateEverythingRun(() => ({ |
| 152 | localVersion, |
| 153 | phase: "providers", |
| 154 | machines: Array.from(machinesById.values()), |
| 155 | })); |
| 156 | const failedProviderMachines = new Set<UpdateRunMachine["environmentId"]>(); |
| 157 | try { |
| 158 | const outcome = await runUpdateEverything({ |
| 159 | ...(downloadLocal ? { downloadLocal: () => downloadLocal(reportLocalError) } : {}), |
| 160 | updateProviders: async () => { |
| 161 | const runs = await providers.updateAll({ quiet: true }); |
| 162 | const outcomes = runs.map((run) => { |
| 163 | const runOutcome = getProviderUpdateRunOutcome(run); |
| 164 | // An interrupted update did not finish either, so its row is not "done". |
| 165 | if (runOutcome.status !== "updated") failedProviderMachines.add(run.environmentId); |
| 166 | return runOutcome; |
| 167 | }); |
| 168 | for (const machine of machinesById.values()) { |
| 169 | if (!machine.updatesServer) { |
| 170 | settleUpdateRunMachine( |
| 171 | machine.environmentId, |
| 172 | failedProviderMachines.has(machine.environmentId), |
| 173 | ); |
| 174 | } |
| 175 | } |
| 176 | setUpdateEverythingRun((run) => run && { ...run, phase: "servers" }); |
| 177 | return outcomes; |
| 178 | }, |
| 179 | updateServers: () => |
| 180 | Promise.all( |
| 181 | servers.map(async (server) => { |
| 182 | const serverOutcome = await updateServer(server, { quiet: true }); |
| 183 | settleUpdateRunMachine( |
| 184 | server.environmentId, |
| 185 | serverOutcome.status === "failed" || |
| 186 | failedProviderMachines.has(server.environmentId), |
| 187 | ); |
| 188 | return { label: server.serverLabel, outcome: serverOutcome }; |
| 189 | }), |
| 190 | ), |
| 191 | }); |
| 192 | const result = () => |
| 193 | collectUpdateEverythingResult({ |
| 194 | localVersion: null, |
| 195 | localFailures, |
| 196 | servers: outcome.servers, |
| 197 | providers: outcome.providers, |
| 198 | }); |
| 199 | if (outcome.localReady && installLocal) { |
| 200 | setUpdateEverythingRun((run) => run && { ...run, phase: "installing" }); |
| 201 | saveUpdateEverythingResult({ ...result(), localVersion }); |
| 202 | if (await installLocal(reportLocalError)) return; |
| 203 | clearSavedUpdateEverythingResult(); |
| 204 | } |
| 205 | showUpdateEverythingResult(result()); |
| 206 | } finally { |
| 207 | setUpdateEverythingRun(() => null); |
| 208 | } |
| 209 | }, |
| 210 | }; |
| 211 | } |