import { useAtomValue } from "@effect/atom-react"; import type { PreparedConnection } from "@t3tools/client-runtime/connection"; import type { EnvironmentId } from "@t3tools/contracts"; import type { ServerConfig } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Option from "effect/Option"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { useCallback, useMemo } from "react"; import { Alert } from "react-native"; import { useEnvironmentServerConfig } from "../state/entities"; import { useConnectionController } from "../features/connection/useConnectionController"; import { environmentPresentations, useEnvironmentPresentation } from "./presentation"; import { projectEnvironmentPresentation, type EnvironmentPresentation, } from "../state/environments"; import { useWorkspaceState } from "../state/workspace"; import type { SavedRemoteConnection } from "../lib/connection"; import { appAtomRegistry } from "./atom-registry"; import type { ConnectedEnvironmentSummary, EnvironmentRuntimeState } from "./remote-runtime-types"; import { environmentSession, usePreparedConnection } from "./session"; import { environmentCatalog } from "../connection/catalog"; const connectionPairingUrlAtom = Atom.make("").pipe( Atom.keepAlive, Atom.withLabel("mobile:connection-pairing-url"), ); const pendingConnectionErrorAtom = Atom.make(null).pipe( Atom.keepAlive, Atom.withLabel("mobile:pending-connection-error"), ); export function setPendingConnectionError(message: string | null): void { appAtomRegistry.set(pendingConnectionErrorAtom, message); } function toSavedConnection( environment: EnvironmentPresentation, prepared: Option.Option, ): SavedRemoteConnection { const displayUrl = environment.displayUrl ?? ""; const active = Option.getOrNull(prepared); const httpBaseUrl = active?.httpBaseUrl ?? displayUrl; const socketUrl = active?.socketUrl ?? ""; const wsBaseUrl = socketUrl === "" ? displayUrl.startsWith("https://") ? displayUrl.replace(/^https:/, "wss:") : displayUrl.replace(/^http:/, "ws:") : new URL(socketUrl).origin; const authorization = active?.httpAuthorization ?? null; return { environmentId: environment.environmentId, environmentLabel: environment.label, pairingUrl: displayUrl, displayUrl, httpBaseUrl, wsBaseUrl, bearerToken: authorization?._tag === "Bearer" ? authorization.token : null, ...(environment.relayManaged ? { authenticationMethod: "dpop" as const, relayManaged: true as const, ...(authorization?._tag === "Dpop" ? { dpopAccessToken: authorization.accessToken } : {}), } : { authenticationMethod: "bearer" as const }), }; } const savedConnectionsByIdAtom = Atom.make((get) => { const presentationById = get(environmentPresentations.presentationsAtom); return Object.fromEntries( [...presentationById.entries()].map(([environmentId, presentation]) => [ environmentId, toSavedConnection( projectEnvironmentPresentation(environmentId, presentation), get(environmentSession.preparedConnectionValueAtom(environmentId)), ), ]), ) as Record; }).pipe(Atom.withLabel("mobile:saved-connections-by-id")); function toRuntimeState( environment: EnvironmentPresentation, serverConfig: ServerConfig | null, ): EnvironmentRuntimeState { return { connectionState: environment.connection.phase, connectionError: environment.connection.error, connectionErrorTraceId: environment.connection.traceId, serverConfig, }; } export function useSavedRemoteConnections() { const catalog = useAtomValue(environmentCatalog.catalogValueAtom); const savedConnectionsById = useAtomValue(savedConnectionsByIdAtom); return { isLoadingSavedConnection: !catalog.isReady, savedConnectionsById, }; } export function useSavedRemoteConnection( environmentId: EnvironmentId | null, ): SavedRemoteConnection | null { const { presentation } = useEnvironmentPresentation(environmentId); const prepared = usePreparedConnection(environmentId); if (environmentId === null || presentation === null) { return null; } return toSavedConnection(projectEnvironmentPresentation(environmentId, presentation), prepared); } export function useRemoteEnvironmentRuntime( environmentId: EnvironmentId | null, ): EnvironmentRuntimeState | null { const { presentation } = useEnvironmentPresentation(environmentId); const serverConfig = useEnvironmentServerConfig(environmentId); if (environmentId === null || presentation === null) { return null; } return toRuntimeState(projectEnvironmentPresentation(environmentId, presentation), serverConfig); } export function useRemoteConnectionStatus() { const workspace = useWorkspaceState(); const pendingConnectionError = useAtomValue(pendingConnectionErrorAtom); const connectedEnvironments = useMemo>( () => workspace.environments.map((environment) => ({ environmentId: environment.environmentId, environmentLabel: environment.environmentLabel, displayUrl: environment.displayUrl, isRelayManaged: environment.isRelayManaged, connectionState: environment.connectionState, connectionError: environment.connectionError, connectionErrorTraceId: environment.connectionErrorTraceId, })), [workspace.environments], ); return { connectedEnvironments, connectionState: workspace.state.connectionState, connectionError: pendingConnectionError ?? workspace.state.connectionError, }; } export function useRemoteConnections() { const controller = useConnectionController(); const connectionPairingUrl = useAtomValue(connectionPairingUrlAtom); const pendingConnectionError = useAtomValue(pendingConnectionErrorAtom); const { connectedEnvironments, connectionError, connectionState } = useRemoteConnectionStatus(); const onChangeConnectionPairingUrl = useCallback((pairingUrl: string) => { appAtomRegistry.set(connectionPairingUrlAtom, pairingUrl); }, []); const onConnectPress = useCallback( async (pairingUrl?: string) => { const nextPairingUrl = pairingUrl ?? connectionPairingUrl; setPendingConnectionError(null); const result = await controller.connectPairingUrl(nextPairingUrl); if (AsyncResult.isFailure(result)) { const error = Cause.squash(result.cause); const message = error instanceof Error ? error.message : "Failed to pair with the environment."; setPendingConnectionError(message); } else { appAtomRegistry.set(connectionPairingUrlAtom, ""); } return result; }, [connectionPairingUrl, controller], ); const onReconnectEnvironment = useCallback( (environmentId: EnvironmentId) => controller.retryEnvironment(environmentId), [controller], ); const onUpdateEnvironment = useCallback( ( environmentId: EnvironmentId, updates: { readonly label: string; readonly displayUrl: string }, ) => controller.updateEnvironment(environmentId, updates), [controller], ); const onRemoveEnvironmentPress = useCallback( (environmentId: EnvironmentId) => { const environment = connectedEnvironments.find( (candidate) => candidate.environmentId === environmentId, ); if (!environment) { return; } Alert.alert( "Remove environment?", `Disconnect and forget ${environment.environmentLabel} on this device.`, [ { text: "Cancel", style: "cancel" }, { text: "Remove", style: "destructive", onPress: () => { void controller.removeEnvironment(environmentId); }, }, ], ); }, [connectedEnvironments, controller], ); return { connectionPairingUrl, connectionState, connectionError, pairingConnectionError: pendingConnectionError, connectedEnvironments, connectedEnvironmentCount: connectedEnvironments.length, onChangeConnectionPairingUrl, onConnectPress, onReconnectEnvironment, onUpdateEnvironment, onRemoveEnvironmentPress, }; }