1import type {
2 EnvironmentPresentation,
3 PreparedConnection,
4} from "@t3tools/client-runtime/connection";
5import { connectionCatalogDisplayUrl } from "@t3tools/client-runtime/connection";
6import type { EnvironmentId, ServerConfig } from "@t3tools/contracts";
7import * as Option from "effect/Option";
8import { Atom } from "effect/reactivity";
9
10import type { SavedRemoteConnection } from "../lib/connection";
11import type { EnvironmentRuntimeState } from "./remote-runtime-types";
12
13export function createRemoteEnvironmentProjectionAtoms(input: {
14 readonly presentationAtom: (
15 environmentId: EnvironmentId,
16 ) => Atom.Atom<EnvironmentPresentation | null>;
17 readonly preparedConnectionAtom: (
18 environmentId: EnvironmentId,
19 ) => Atom.Atom<Option.Option<PreparedConnection>>;
20 readonly serverConfigAtom: (environmentId: EnvironmentId) => Atom.Atom<ServerConfig | null>;
21}) {
22 const savedConnectionAtom = Atom.family((environmentId: EnvironmentId) => {
23 let previousEntry: EnvironmentPresentation["entry"] | null = null;
24 let previousPrepared: PreparedConnection | null = null;
25 let previous: SavedRemoteConnection | null = null;
26
27 return Atom.make((get) => {
28 const presentation = get(input.presentationAtom(environmentId));
29 if (presentation === null) {
30 previousEntry = null;
31 previousPrepared = null;
32 previous = null;
33 return null;
34 }
35
36 const prepared = Option.getOrNull(get(input.preparedConnectionAtom(environmentId)));
37 if (
38 previous !== null &&
39 presentation.entry === previousEntry &&
40 prepared === previousPrepared
41 ) {
42 return previous;
43 }
44
45 const displayUrl = connectionCatalogDisplayUrl(presentation.entry) ?? "";
46 const httpBaseUrl = prepared?.httpBaseUrl ?? displayUrl;
47 const socketUrl = prepared?.socketUrl ?? "";
48 const wsBaseUrl =
49 socketUrl === ""
50 ? displayUrl.startsWith("https://")
51 ? displayUrl.replace(/^https:/, "wss:")
52 : displayUrl.replace(/^http:/, "ws:")
53 : new URL(socketUrl).origin;
54 const authorization = prepared?.httpAuthorization ?? null;
55 // Credentials follow the route in use, which may not be the preferred one.
56 const relayManaged = prepared
57 ? prepared.target._tag === "RelayConnectionTarget"
58 : presentation.entry.target._tag === "RelayConnectionTarget";
59
60 previousEntry = presentation.entry;
61 previousPrepared = prepared;
62 previous = {
63 environmentId,
64 environmentLabel: presentation.entry.target.label,
65 pairingUrl: displayUrl,
66 displayUrl,
67 httpBaseUrl,
68 wsBaseUrl,
69 bearerToken: authorization?._tag === "Bearer" ? authorization.token : null,
70 ...(relayManaged
71 ? {
72 authenticationMethod: "dpop" as const,
73 relayManaged: true as const,
74 ...(authorization?._tag === "Dpop"
75 ? { dpopAccessToken: authorization.accessToken }
76 : {}),
77 }
78 : { authenticationMethod: "bearer" as const }),
79 };
80 return previous;
81 }).pipe(Atom.withLabel(`mobile:saved-connection:${environmentId}`));
82 });
83
84 const runtimeStateAtom = Atom.family((environmentId: EnvironmentId) => {
85 let previousConnection: EnvironmentPresentation["connection"] | null = null;
86 let previousServerConfig: ServerConfig | null = null;
87 let previous: EnvironmentRuntimeState | null = null;
88
89 return Atom.make((get) => {
90 const presentation = get(input.presentationAtom(environmentId));
91 if (presentation === null) {
92 previousConnection = null;
93 previousServerConfig = null;
94 previous = null;
95 return null;
96 }
97
98 const connection = presentation.connection;
99 const serverConfig = get(input.serverConfigAtom(environmentId));
100 if (
101 previous !== null &&
102 connection.phase === previousConnection?.phase &&
103 connection.error === previousConnection?.error &&
104 connection.traceId === previousConnection?.traceId &&
105 serverConfig === previousServerConfig
106 ) {
107 return previous;
108 }
109
110 previousConnection = connection;
111 previousServerConfig = serverConfig;
112 previous = {
113 connectionState: connection.phase,
114 connectionError: connection.error,
115 connectionErrorTraceId: connection.traceId,
116 serverConfig,
117 };
118 return previous;
119 }).pipe(Atom.withLabel(`mobile:environment-runtime-state:${environmentId}`));
120 });
121
122 return { savedConnectionAtom, runtimeStateAtom };
123}