| 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { |
| 3 | AuthRelayReadScope, |
| 4 | EnvironmentId, |
| 5 | type EnvironmentCloudLinkStateResult, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import * as Cause from "effect/Cause"; |
| 8 | import * as Context from "effect/Context"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Layer from "effect/Layer"; |
| 11 | import * as Option from "effect/Option"; |
| 12 | import { AsyncResult, Atom } from "effect/reactivity"; |
| 13 | import { HttpClient } from "effect/http"; |
| 14 | import { useCallback, useMemo } from "react"; |
| 15 | |
| 16 | import { usePrimaryEnvironment } from "../state/environments"; |
| 17 | import { runtime } from "../lib/runtime"; |
| 18 | import { appAtomRegistry } from "../rpc/atomRegistry"; |
| 19 | import { readEnvironmentScope, useEnvironmentScope } from "../state/session"; |
| 20 | import { readPrimaryCloudLinkState, type CloudLinkTarget } from "./linkEnvironment"; |
| 21 | import { hasCloudPublicConfig } from "./publicConfig"; |
| 22 | |
| 23 | const primaryCloudLinkAtomRuntime = Atom.runtime( |
| 24 | Layer.effect( |
| 25 | HttpClient.HttpClient, |
| 26 | runtime.contextEffect.pipe( |
| 27 | Effect.map((context) => Context.get(context, HttpClient.HttpClient)), |
| 28 | ), |
| 29 | ), |
| 30 | ); |
| 31 | |
| 32 | const primaryCloudLinkStateAtom = Atom.family((key: string) => { |
| 33 | const target = JSON.parse(key) as CloudLinkTarget; |
| 34 | return primaryCloudLinkAtomRuntime |
| 35 | .atom(readPrimaryCloudLinkState({ target })) |
| 36 | .pipe( |
| 37 | Atom.swr({ staleTime: 5_000, revalidateOnMount: true }), |
| 38 | Atom.setIdleTTL(5 * 60_000), |
| 39 | Atom.withLabel(`primary-cloud-link:${target.environmentId}`), |
| 40 | ); |
| 41 | }); |
| 42 | |
| 43 | const EMPTY_PRIMARY_CLOUD_LINK_STATE_ATOM = Atom.make( |
| 44 | AsyncResult.success<EnvironmentCloudLinkStateResult | null>(null), |
| 45 | ).pipe(Atom.keepAlive, Atom.withLabel("primary-cloud-link:null")); |
| 46 | |
| 47 | function targetKey(target: CloudLinkTarget): string { |
| 48 | return JSON.stringify(target); |
| 49 | } |
| 50 | |
| 51 | function refreshPrimaryCloudLinkState(target: CloudLinkTarget | null): void { |
| 52 | if ( |
| 53 | target && |
| 54 | readEnvironmentScope(EnvironmentId.make(target.environmentId), AuthRelayReadScope) |
| 55 | ) { |
| 56 | appAtomRegistry.refresh(primaryCloudLinkStateAtom(targetKey(target))); |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | export function readCachedPrimaryCloudLinkState(target: CloudLinkTarget) { |
| 61 | if (!readEnvironmentScope(EnvironmentId.make(target.environmentId), AuthRelayReadScope)) |
| 62 | return null; |
| 63 | const result = appAtomRegistry.get(primaryCloudLinkStateAtom(targetKey(target))); |
| 64 | return result._tag === "Success" ? result.value : null; |
| 65 | } |
| 66 | |
| 67 | export function usePrimaryCloudLinkState() { |
| 68 | const primary = usePrimaryEnvironment(); |
| 69 | const canReadRelay = useEnvironmentScope(primary?.environmentId ?? null, AuthRelayReadScope); |
| 70 | const target = useMemo( |
| 71 | () => |
| 72 | primary?.entry.target._tag === "PrimaryConnectionTarget" |
| 73 | ? { |
| 74 | environmentId: primary.environmentId, |
| 75 | label: primary.label, |
| 76 | httpBaseUrl: primary.entry.target.httpBaseUrl, |
| 77 | wsBaseUrl: primary.entry.target.wsBaseUrl, |
| 78 | } |
| 79 | : null, |
| 80 | [primary], |
| 81 | ); |
| 82 | // Builds without T3 Connect have no link to read; skip the request. |
| 83 | const atom = |
| 84 | target && hasCloudPublicConfig() && canReadRelay |
| 85 | ? primaryCloudLinkStateAtom(targetKey(target)) |
| 86 | : EMPTY_PRIMARY_CLOUD_LINK_STATE_ATOM; |
| 87 | const result = useAtomValue(atom); |
| 88 | const refresh = useCallback(() => { |
| 89 | refreshPrimaryCloudLinkState(target); |
| 90 | }, [target]); |
| 91 | let error: string | null = null; |
| 92 | if (result._tag === "Failure") { |
| 93 | const cause = Cause.squash(result.cause); |
| 94 | error = cause instanceof Error ? cause.message : "Could not read T3 Connect link state."; |
| 95 | } |
| 96 | |
| 97 | return { |
| 98 | data: result._tag === "Failure" ? null : Option.getOrNull(AsyncResult.value(result)), |
| 99 | error, |
| 100 | isPending: result.waiting, |
| 101 | refresh, |
| 102 | target, |
| 103 | }; |
| 104 | } |