| 1 | import type { |
| 2 | EnvironmentPresentation, |
| 3 | PreparedConnection, |
| 4 | } from "@t3tools/client-runtime/connection"; |
| 5 | import { connectionCatalogDisplayUrl } from "@t3tools/client-runtime/connection"; |
| 6 | import type { EnvironmentId, ServerConfig } from "@t3tools/contracts"; |
| 7 | import * as Option from "effect/Option"; |
| 8 | import { Atom } from "effect/reactivity"; |
| 9 | |
| 10 | import type { SavedRemoteConnection } from "../lib/connection"; |
| 11 | import type { EnvironmentRuntimeState } from "./remote-runtime-types"; |
| 12 | |
| 13 | export 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 | } |