@@ -7,18 +7,22 @@ import {
77 AuthRelayWriteScope ,
88 type EnvironmentCloudLinkStateResult ,
99} from "@t3tools/contracts" ;
10+ import { ManagedRelay } from "@t3tools/client-runtime/relay" ;
1011import type { RelayManagedEndpointProviderKind } from "@t3tools/contracts/relay" ;
1112import {
1213 isAtomCommandInterrupted ,
1314 settlePromise ,
1415 squashAtomCommandFailure ,
1516} from "@t3tools/client-runtime/state/runtime" ;
17+ import * as Schema from "effect/Schema" ;
1618import { useState } from "react" ;
1719
1820import { toastManager } from "../components/ui/toast" ;
1921import { relayEnvironmentDiscovery } from "../state/relay" ;
2022import { readEnvironmentScope } from "../state/session" ;
2123import { useAtomCommand } from "../state/use-atom-command" ;
24+ import { CloudEnvironmentLinkError } from "./linkEnvironment" ;
25+ import { refreshManagedTunnelUsage } from "./managedRelayState" ;
2226import {
2327 linkPrimaryEnvironment as linkPrimaryEnvironmentAtom ,
2428 unlinkPrimaryEnvironment as unlinkPrimaryEnvironmentAtom ,
@@ -46,6 +50,18 @@ function isManagedTunnelActive(
4650 ) ;
4751}
4852
53+ const isManagedRelayRequestFailedError = Schema . is ( ManagedRelay . ManagedRelayRequestFailedError ) ;
54+
55+ /** The account's tunnel cap when the relay refused a new tunnel for being at it. */
56+ function tunnelLimitFromLinkFailure ( cause : unknown ) : number | null {
57+ if ( ! ( cause instanceof CloudEnvironmentLinkError ) ) return null ;
58+ const relayCause = cause . cause ;
59+ if ( ! isManagedRelayRequestFailedError ( relayCause ) ) return null ;
60+ return relayCause . relayError ?. _tag === "RelayEnvironmentLinkLimitExceededError"
61+ ? relayCause . relayError . maxTunnels
62+ : null ;
63+ }
64+
4965export function useCloudLinkController ( ) {
5066 const { getToken, isSignedIn } = useAuth ( ) ;
5167 const refreshRelayEnvironments = useAtomCommand ( relayEnvironmentDiscovery . refresh , {
@@ -68,6 +84,7 @@ export function useCloudLinkController() {
6884 ? relayEnvironments . get ( primaryEnvironmentId ) ?. environment . endpoint . providerKind
6985 : undefined ;
7086 const [ operationError , setOperationError ] = useState < string | null > ( null ) ;
87+ const [ tunnelLimit , setTunnelLimit ] = useState < number | null > ( null ) ;
7188
7289 const reportUpdateFailure = ( cause : unknown ) => {
7390 const message = cause instanceof Error ? cause . message : "Could not update T3 Connect access." ;
@@ -100,6 +117,7 @@ export function useCloudLinkController() {
100117
101118 const reconcileCloudState = async ( desired : CloudLinkDesiredState ) : Promise < boolean > => {
102119 setOperationError ( null ) ;
120+ setTunnelLimit ( null ) ;
103121 const target = primaryCloudLinkState . target ;
104122 if ( ! target ) {
105123 reportUpdateFailure ( new Error ( "Local environment is not ready yet." ) ) ;
@@ -138,6 +156,7 @@ export function useCloudLinkController() {
138156 target,
139157 clerkToken : tokenResult . _tag === "Success" ? ( tokenResult . value ?? null ) : null ,
140158 } ) ;
159+ refreshManagedTunnelUsage ( ) ;
141160 if ( unlinkResult . _tag === "Failure" ) {
142161 if ( ! isAtomCommandInterrupted ( unlinkResult ) ) {
143162 reportUpdateFailure ( squashAtomCommandFailure ( unlinkResult ) ) ;
@@ -168,9 +187,15 @@ export function useCloudLinkController() {
168187 clerkToken,
169188 mode : desired . managedTunnel ? "managed" : "publish_only" ,
170189 } ) ;
190+ // Any link attempt can change the account's tunnel count.
191+ refreshManagedTunnelUsage ( ) ;
171192 if ( linkResult . _tag === "Failure" ) {
172193 if ( ! isAtomCommandInterrupted ( linkResult ) ) {
173- reportUpdateFailure ( squashAtomCommandFailure ( linkResult ) ) ;
194+ const cause = squashAtomCommandFailure ( linkResult ) ;
195+ // The caller shows an upgrade prompt instead of an error.
196+ const limit = tunnelLimitFromLinkFailure ( cause ) ;
197+ if ( limit === null ) reportUpdateFailure ( cause ) ;
198+ else setTunnelLimit ( limit ) ;
174199 }
175200 primaryCloudLinkState . refresh ( ) ;
176201 return false ;
@@ -215,6 +240,7 @@ export function useCloudLinkController() {
215240 publishAgentActivity,
216241 holdWebhooksWhileOffline,
217242 operationError,
243+ tunnelLimit,
218244 reconcileCloudState,
219245 } ;
220246}
0 commit comments