1import {
2 AVAILABLE_CONNECTION_STATE,
3 connectionProjectionPhase,
4} from "@t3tools/client-runtime/connection";
5import {
6 createEnvironmentShellAtoms,
7 createEnvironmentSnapshotAtom,
8 createShellEnvironmentAtoms,
9 type EnvironmentShellState,
10} from "@t3tools/client-runtime/state/shell";
11import {
12 type EnvironmentCatalogState,
13 enabledEnvironmentIds,
14} from "@t3tools/client-runtime/state/connections";
15import type { EnvironmentId } from "@t3tools/contracts";
16import * as Option from "effect/Option";
17import { AsyncResult, Atom } from "effect/reactivity";
18
19import { environmentCatalog } from "../connection/catalog";
20import { connectionAtomRuntime } from "../connection/runtime";
21import { isHostedStaticApp } from "../hostedPairing";
22
23export const shellEnvironment = createShellEnvironmentAtoms(connectionAtomRuntime);
24export const environmentShell = createEnvironmentShellAtoms(connectionAtomRuntime);
25export const environmentSnapshotAtom = createEnvironmentSnapshotAtom(environmentShell.stateAtom);
26
27export const allEnvironmentShellsBootstrappedAtom = Atom.make((get) => {
28 const catalog = AsyncResult.value(get(environmentCatalog.catalogAtom));
29 if (Option.isNone(catalog)) {
30 return false;
31 }
32 for (const environmentId of enabledEnvironmentIds(catalog.value)) {
33 if (Option.isSome(get(environmentShell.stateValueAtom(environmentId)).snapshot)) {
34 continue;
35 }
36 const connection = Option.getOrElse(
37 AsyncResult.value(get(environmentCatalog.stateAtom(environmentId))),
38 () => AVAILABLE_CONNECTION_STATE,
39 );
40 if (connectionProjectionPhase(connection) !== "disconnected") {
41 return false;
42 }
43 // A retrying environment is only transiently disconnected; give it its
44 // first retries before letting the landing settle without its snapshot.
45 if (connection.phase === "backoff" && connection.desired && connection.attempt <= 2) {
46 return false;
47 }
48 }
49 return true;
50}).pipe(Atom.withLabel("web-all-environment-shells-bootstrapped"));
51
52/**
53 * The persisted catalog can emit before platform discovery registers the primary environment.
54 * Neither that gap nor an empty catalog proves a project is absent.
55 */
56function catalogIsComplete(
57 catalog: EnvironmentCatalogState,
58 requiresPrimaryEnvironment: boolean,
59): boolean {
60 if (!catalog.isReady || catalog.entries.size === 0) return false;
61 return (
62 !requiresPrimaryEnvironment ||
63 Array.from(catalog.entries.values()).some(
64 (entry) => entry.target._tag === "PrimaryConnectionTarget",
65 )
66 );
67}
68
69/** Cached or missing snapshots cannot establish that a saved project no longer exists. */
70export function createAllEnvironmentProjectSnapshotsReadyAtom(input: {
71 readonly catalogValueAtom: Atom.Atom<EnvironmentCatalogState>;
72 readonly shellStateValueAtom: (environmentId: EnvironmentId) => Atom.Atom<EnvironmentShellState>;
73 readonly requiresPrimaryEnvironment: boolean;
74}) {
75 return Atom.make((get) => {
76 const catalog = get(input.catalogValueAtom);
77 if (!catalogIsComplete(catalog, input.requiresPrimaryEnvironment)) return false;
78 for (const environmentId of enabledEnvironmentIds(catalog)) {
79 const shell = get(input.shellStateValueAtom(environmentId));
80 if (shell.status !== "live" || Option.isNone(shell.snapshot)) return false;
81 }
82 return true;
83 }).pipe(Atom.withLabel("web-all-environment-project-snapshots-ready"));
84}
85
86export const allEnvironmentProjectSnapshotsReadyAtom =
87 createAllEnvironmentProjectSnapshotsReadyAtom({
88 catalogValueAtom: environmentCatalog.catalogValueAtom,
89 shellStateValueAtom: environmentShell.stateValueAtom,
90 requiresPrimaryEnvironment: !isHostedStaticApp(),
91 });
92
93/**
94 * Every enabled machine has sent its live projects or has settled offline. Prompt links wait for
95 * it before they decide a repository is missing: one offline machine must not hold a link
96 * forever. A machine that has not started connecting yet is "available", not offline.
97 */
98export const allReachableShellsLiveAtom = Atom.make((get) => {
99 const catalog = get(environmentCatalog.catalogValueAtom);
100 if (!catalogIsComplete(catalog, !isHostedStaticApp())) return false;
101 for (const environmentId of enabledEnvironmentIds(catalog)) {
102 const shell = get(environmentShell.stateValueAtom(environmentId));
103 if (shell.status === "live" && Option.isSome(shell.snapshot)) continue;
104 const state = AsyncResult.value(get(environmentCatalog.stateAtom(environmentId)));
105 if (Option.isNone(state)) return false;
106 const connection = state.value;
107 const settledOffline =
108 connection.phase === "offline" ||
109 connection.phase === "blocked" ||
110 (connection.phase === "backoff" && connection.attempt > 2);
111 if (!settledOffline) return false;
112 }
113 return true;
114}).pipe(Atom.withLabel("web-all-reachable-shells-live"));