Skip to content
Source
Sign inSign up

shell.ts

151 lines6.0 KB

1import {
2 AVAILABLE_CONNECTION_STATE,
3 connectionProjectionPhase,
4} from "@t3tools/client-runtime/connection";
5import {
6 createEnvironmentShellAtoms,
7 createEnvironmentSnapshotAtom,
8 createShellEnvironmentAtoms,
9 type EnvironmentShellState,
10 type EnvironmentShellStatus,
11} from "@t3tools/client-runtime/state/shell";
12import {
13 type EnvironmentCatalogState,
14 enabledEnvironmentIds,
15} from "@t3tools/client-runtime/state/connections";
16import type { EnvironmentId } from "@t3tools/contracts";
17import { useAtomValue } from "@effect/atom-react";
18import * as Option from "effect/Option";
19import { AsyncResult, Atom } from "effect/reactivity";
20​
21import { environmentCatalog } from "../connection/catalog";
22import { connectionAtomRuntime } from "../connection/runtime";
23import { isHostedStaticApp } from "../hostedPairing";
24import { isLocalEnvironmentDisabled } from "../localEnvironment";
25​
26export const shellEnvironment = createShellEnvironmentAtoms(connectionAtomRuntime);
27export const environmentShell = createEnvironmentShellAtoms(connectionAtomRuntime);
28export const environmentSnapshotAtom = createEnvironmentSnapshotAtom(environmentShell.stateAtom);
29​
30const NO_SHELL_STATUS_ATOM = Atom.make<EnvironmentShellStatus | null>(null).pipe(
31 Atom.withLabel("web-environment-shell-status:none"),
32);
33const NO_SHELL_SNAPSHOT_ATOM = Atom.make(false).pipe(
34 Atom.withLabel("web-environment-shell-has-snapshot:none"),
35);
36​
37export function useEnvironmentShellStatus(
38 environmentId: EnvironmentId | null,
39): EnvironmentShellStatus | null {
40 return useAtomValue(
41 environmentId === null ? NO_SHELL_STATUS_ATOM : environmentShell.statusAtom(environmentId),
42 );
44​
45export function useEnvironmentShellHasSnapshot(environmentId: EnvironmentId | null): boolean {
46 return useAtomValue(
47 environmentId === null
48 ? NO_SHELL_SNAPSHOT_ATOM
49 : environmentShell.hasSnapshotAtom(environmentId),
50 );
52​
53let previousLiveEnvironmentIds: ReadonlySet<EnvironmentId> = new Set();
54/** Environments whose threads reflect the server now, not a cache or a resync in flight. */
55export const liveShellEnvironmentIdsAtom = Atom.make((get) => {
56 const next = new Set<EnvironmentId>();
57 for (const environmentId of enabledEnvironmentIds(get(environmentCatalog.catalogValueAtom))) {
58 if (get(environmentShell.statusAtom(environmentId)) === "live") next.add(environmentId);
59 }
60 if (
61 next.size === previousLiveEnvironmentIds.size &&
62 [...next].every((environmentId) => previousLiveEnvironmentIds.has(environmentId))
63 ) {
64 return previousLiveEnvironmentIds;
65 }
66 previousLiveEnvironmentIds = next;
67 return next;
68}).pipe(Atom.withLabel("web-live-shell-environment-ids"));
69​
70export const allEnvironmentShellsBootstrappedAtom = Atom.make((get) => {
71 const catalog = AsyncResult.value(get(environmentCatalog.catalogAtom));
72 if (Option.isNone(catalog)) {
73 return false;
74 }
75 for (const environmentId of enabledEnvironmentIds(catalog.value)) {
76 if (Option.isSome(get(environmentShell.stateValueAtom(environmentId)).snapshot)) {
77 continue;
78 }
79 const connection = AsyncResult.getOrElse(
80 get(environmentCatalog.stateAtom(environmentId)),
81 () => AVAILABLE_CONNECTION_STATE,
82 );
83 if (connectionProjectionPhase(connection) !== "disconnected") {
84 return false;
85 }
86 if (connection.phase === "backoff" && connection.desired && connection.attempt <= 2) {
87 return false;
88 }
89 }
90 return true;
91}).pipe(Atom.withLabel("web-all-environment-shells-bootstrapped"));
92​
93// Catalog can precede primary discovery; an empty or incomplete catalog cannot prove absence.
94function catalogIsComplete(
95 catalog: EnvironmentCatalogState,
96 requiresPrimaryEnvironment: boolean,
97): boolean {
98 if (!catalog.isReady || catalog.entries.size === 0) return false;
99 return (
100 !requiresPrimaryEnvironment ||
101 Array.from(catalog.entries.values()).some(
102 (entry) => entry.target._tag === "PrimaryConnectionTarget",
103 )
104 );
106​
107export function createAllEnvironmentProjectSnapshotsReadyAtom(input: {
108 readonly catalogValueAtom: Atom.Atom<EnvironmentCatalogState>;
109 readonly shellStateValueAtom: (environmentId: EnvironmentId) => Atom.Atom<EnvironmentShellState>;
110 readonly requiresPrimaryEnvironment: boolean;
111}) {
112 return Atom.make((get) => {
113 const catalog = get(input.catalogValueAtom);
114 if (!catalogIsComplete(catalog, input.requiresPrimaryEnvironment)) return false;
115 for (const environmentId of enabledEnvironmentIds(catalog)) {
116 const shell = get(input.shellStateValueAtom(environmentId));
117 if (shell.status !== "live" || Option.isNone(shell.snapshot)) return false;
118 }
119 return true;
120 }).pipe(Atom.withLabel("web-all-environment-project-snapshots-ready"));
122​
123export const allEnvironmentProjectSnapshotsReadyAtom =
124 createAllEnvironmentProjectSnapshotsReadyAtom({
125 catalogValueAtom: environmentCatalog.catalogValueAtom,
126 shellStateValueAtom: environmentShell.stateValueAtom,
127 requiresPrimaryEnvironment: expectsPrimaryEnvironment(),
128 });
129​
130/** Hosted clients and desktops with the local environment disabled never discover a primary. */
131export function expectsPrimaryEnvironment(): boolean {
132 return !isHostedStaticApp() && !isLocalEnvironmentDisabled();
134​
135export const allReachableShellsLiveAtom = Atom.make((get) => {
136 const catalog = get(environmentCatalog.catalogValueAtom);
137 if (!catalogIsComplete(catalog, expectsPrimaryEnvironment())) return false;
138 for (const environmentId of enabledEnvironmentIds(catalog)) {
139 const shell = get(environmentShell.stateValueAtom(environmentId));
140 if (shell.status === "live" && Option.isSome(shell.snapshot)) continue;
141 const state = AsyncResult.value(get(environmentCatalog.stateAtom(environmentId)));
142 if (Option.isNone(state)) return false;
143 const connection = state.value;
144 const settledOffline =
145 connection.phase === "offline" ||
146 connection.phase === "blocked" ||
147 (connection.phase === "backoff" && connection.attempt > 2);
148 if (!settledOffline) return false;
149 }
150 return true;
151}).pipe(Atom.withLabel("web-all-reachable-shells-live"));