1import type { EnvironmentId, ServerSelfUpdateInput } from "@t3tools/contracts";
2import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import type * as HttpClient from "effect/http/HttpClient";
6import type { Atom } from "effect/reactivity";
7import type * as Socket from "effect/socket/Socket";
8
9import { updateOutdatedHost } from "../connection/outdatedHostUpdate.ts";
10import type * as ConnectionResolver from "../connection/resolver.ts";
11import type * as EnvironmentRegistry from "../connection/registry.ts";
12import type * as RelayEnvironmentDiscovery from "../relay/discovery.ts";
13import { createAtomCommandScheduler, createRuntimeCommand } from "./runtime.ts";
14import {
15 serverUpdateFailureMessage,
16 serverUpdateStateAtom,
17 type ServerUpdateStage,
18} from "./server.ts";
19
20export interface OutdatedServerUpdateTarget {
21 readonly environmentId: EnvironmentId;
22 readonly input: ServerSelfUpdateInput;
23 /** From the host descriptor when known; an outdated host never delivers a server config. */
24 readonly fromVersion?: string;
25}
26
27/**
28 * Updates a host too old for this client to connect to. Progress lands in the
29 * same per-environment update state as a normal server update.
30 */
31export function createOutdatedServerUpdateCommand<E>(
32 runtime: Atom.AtomRuntime<
33 | EnvironmentRegistry.EnvironmentRegistry
34 | ConnectionResolver.ConnectionResolver
35 | RelayEnvironmentDiscovery.RelayEnvironmentDiscovery
36 | Socket.WebSocketConstructor
37 | HttpClient.HttpClient,
38 E
39 >,
40) {
41 return createRuntimeCommand(runtime, {
42 label: "environment-data:server:update-outdated-server",
43 scheduler: createAtomCommandScheduler(),
44 concurrency: {
45 mode: "singleFlight",
46 key: ({ environmentId }: OutdatedServerUpdateTarget) => environmentId,
47 },
48 execute: (target: OutdatedServerUpdateTarget, atomRegistry) => {
49 const stateAtom = serverUpdateStateAtom(target.environmentId);
50 const targetVersion = target.input.targetVersion;
51 const fromVersion = target.fromVersion ?? targetVersion;
52 let currentStage: ServerUpdateStage = "downloading";
53 const setStage = (stage: ServerUpdateStage) =>
54 Effect.sync(() => {
55 currentStage = stage;
56 atomRegistry.set(stateAtom, { status: "running", stage, fromVersion, targetVersion });
57 });
58 return setStage(currentStage).pipe(
59 Effect.andThen(updateOutdatedHost(target.environmentId, target.input, setStage)),
60 Effect.onExit((exit) =>
61 Effect.sync(() => {
62 if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) {
63 atomRegistry.set(stateAtom, { status: "idle" });
64 return;
65 }
66 atomRegistry.set(stateAtom, {
67 status: "failed",
68 stage: currentStage,
69 fromVersion,
70 targetVersion,
71 message: serverUpdateFailureMessage(Cause.squash(exit.cause)),
72 });
73 }),
74 ),
75 );
76 },
77 });
78}