refactor(mobile-web): put the transport connection state on the wire

The shell collapsed six transport states into four, and every page route
expanded them back. Both mappings are gone; the page reads the state the
transport reported.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-07 15:08:45 -04:00
parent effd8ec5de
commit f09f618f3b
15 changed files with 26 additions and 81 deletions
+2 -3
View File
@@ -22,7 +22,6 @@ import { MobileWebHybridShellPresentation } from '../src/mobile-web/MobileWebHyb
import { mobileWebShellLoadFailureWarning } from '../src/mobile-web/mobile-web-shell-load-failure-warning'
import { useMobileWebNavigationIntentHandoff } from '../src/mobile-web/use-mobile-web-navigation-intent-handoff'
import { useMobileWebColdResumeRoute } from '../src/mobile-web/use-mobile-web-cold-resume-route'
import { mobileWebBridgeConnectionState } from '../src/mobile-web/mobile-web-bridge-connection-state'
import { MobileWebOneShotResponseDrop } from '../src/mobile-web/mobile-web-one-shot-response-drop'
import { useMobileWebE2eHostSelection } from '../src/mobile-web/mobile-web-e2e-host-selection'
import { useMobileWebAppForegroundAuthority } from '../src/mobile-web/use-mobile-web-app-foreground-authority'
@@ -266,7 +265,7 @@ export default function HybridScreen() {
}, [postInit])
useEffect(() => {
brokerRef.current?.updateConnectionState(mobileWebBridgeConnectionState(state))
brokerRef.current?.updateConnectionState(state)
const current = session
if (!current || pageDocument.initializedSessionRef.current !== current.sessionId) {
return
@@ -276,7 +275,7 @@ export default function HybridScreen() {
type: 'connection',
shellSessionId: current.sessionId,
buildId: current.buildId,
state: mobileWebBridgeConnectionState(state),
state,
reconnectAttempts: reconnects,
lastConnectedAt: lastConnected
})
+1 -7
View File
@@ -14,18 +14,12 @@ export default function HostMobileWebAccountsRoute() {
: undefined,
[shell.client, shell.hostDisplayName]
)
const connectionState =
shell.connection === 'offline'
? 'disconnected'
: shell.connection === 'recovering'
? 'reconnecting'
: shell.connection
return (
<AccountsScreen
hostId={HOSTED_PAGE_HOST_ID}
operations={operations}
connectionState={connectionState}
connectionState={shell.connection}
nativeHostBinding={false}
/>
)
@@ -9,17 +9,11 @@ export default function HostMobileWebFileExplorerRoute() {
() => (shell.client ? webHostFileExplorerOperations(shell.client) : undefined),
[shell.client]
)
const connectionState =
shell.connection === 'offline'
? 'disconnected'
: shell.connection === 'recovering'
? 'reconnecting'
: shell.connection
return (
<MobileFileExplorerScreen
operations={operations}
connectionState={connectionState}
connectionState={shell.connection}
nativeHostBinding={false}
/>
)
@@ -9,17 +9,11 @@ export default function HostMobileWebFilePreviewRoute() {
() => (shell.client ? webHostFilePreviewOperations(shell.client) : undefined),
[shell.client]
)
const connectionState =
shell.connection === 'offline'
? 'disconnected'
: shell.connection === 'recovering'
? 'reconnecting'
: shell.connection
return (
<MobileFilePreviewRoute
operations={operations}
connectionState={connectionState}
connectionState={shell.connection}
nativeHostBinding={false}
/>
)
@@ -16,19 +16,13 @@ export default function HostMobileWebDiffReviewRoute() {
() => (shell.client && workspaceId ? webHostDiffReviewClient(shell.client, workspaceId) : null),
[shell.client, workspaceId]
)
const connectionState =
shell.connection === 'offline'
? 'disconnected'
: shell.connection === 'recovering'
? 'reconnecting'
: shell.connection
return (
<MobileDiffReviewRoute
routeName={workspaceName}
binding={{
client,
connectionState,
connectionState: shell.connection,
reconnect: async () => {
await shell.client?.navigationReconnect()
},
@@ -83,12 +83,6 @@ export default function HostMobileWebSessionRoute() {
() => (shell.client ? webHostSessionChatPendingDeliveryOperations(shell.client) : undefined),
[shell.client]
)
const connectionState =
shell.connection === 'offline'
? 'disconnected'
: shell.connection === 'recovering'
? 'reconnecting'
: shell.connection
return (
<SessionScreen
@@ -104,7 +98,7 @@ export default function HostMobileWebSessionRoute() {
sessionNativeChatOperations={sessionNativeChatOperations}
sessionChatDraftOperations={sessionChatDraftOperations}
sessionChatPendingDeliveryOperations={sessionChatPendingDeliveryOperations}
connectionState={connectionState}
connectionState={shell.connection}
nativeHostBinding={false}
reconnect={() =>
shell.client
@@ -17,19 +17,13 @@ export default function HostMobileWebSourceControlRoute() {
shell.client && workspaceId ? webHostSourceControlClient(shell.client, workspaceId) : null,
[shell.client, workspaceId]
)
const connectionState =
shell.connection === 'offline'
? 'disconnected'
: shell.connection === 'recovering'
? 'reconnecting'
: shell.connection
return (
<MobileSourceControlRoute
routeName={workspaceName}
binding={{
client,
connectionState,
connectionState: shell.connection,
reconnect: async () => {
await shell.client?.navigationReconnect()
},
+1 -7
View File
@@ -41,12 +41,6 @@ export default function HostMobileWebTasksRoute() {
workspaceCreation: webHostWorkspaceCreationOperations(shell.client)
}
}, [shell.client])
const connectionState =
shell.connection === 'offline'
? 'disconnected'
: shell.connection === 'recovering'
? 'reconnecting'
: shell.connection
return (
<MobileTasksScreen
@@ -65,7 +59,7 @@ export default function HostMobileWebTasksRoute() {
providerWriteOperations={operations?.providerWrite}
readOperations={operations?.read}
workspaceCreationOperations={operations?.workspaceCreation}
connectionState={connectionState}
connectionState={shell.connection}
connectionMetrics={{
reconnectAttempts: shell.reconnectAttempts,
lastConnectedAt: shell.lastConnectedAt
+1 -7
View File
@@ -45,12 +45,6 @@ export default function HostMobileWebRoute() {
),
[pathname, router, shell.client]
)
const connectionState =
shell.connection === 'offline'
? 'disconnected'
: shell.connection === 'recovering'
? 'reconnecting'
: shell.connection
const hostStatusGates = useWebHostStatusGates({
client: shell.client,
connection: shell.connection
@@ -60,7 +54,7 @@ export default function HostMobileWebRoute() {
<HostScreen
hostId={HOSTED_PAGE_HOST_ID}
nativeHostBinding={false}
connectionState={connectionState}
connectionState={shell.connection}
connectionMetrics={{
reconnectAttempts: shell.reconnectAttempts,
lastConnectedAt: shell.lastConnectedAt
@@ -1,16 +0,0 @@
import type { ConnectionState } from '../transport/types'
export function mobileWebBridgeConnectionState(
state: ConnectionState
): 'connecting' | 'connected' | 'offline' | 'recovering' {
if (state === 'connected') {
return 'connected'
}
if (state === 'reconnecting') {
return 'recovering'
}
if (state === 'connecting' || state === 'handshaking') {
return 'connecting'
}
return 'offline'
}
@@ -1,6 +1,7 @@
import { requireMobileWebConnectedClient } from './mobile-web-connected-client'
import type {
MobileWebBridgePageMessage,
MobileWebConnectionState,
MobileWebResumeRoute
} from '../../../src/shared/mobile-web/bridge-contract'
import type { RpcClient } from '../transport/rpc-client'
@@ -115,7 +116,7 @@ export class MobileWebCapabilityBroker {
void this.messages.error(requestId, 'cancelled', false)
}
}
updateConnectionState(state: 'connecting' | 'connected' | 'offline' | 'recovering'): void {
updateConnectionState(state: MobileWebConnectionState): void {
if (state !== 'connected') {
this.authorities.terminalArtifact.clear()
void this.speechAuthority.cancel('disconnected')
@@ -4,7 +4,6 @@ import {
type MobileWebResumeRoute
} from '../../../src/shared/mobile-web/bridge-contract'
import type { ConnectionState } from '../transport/types'
import { mobileWebBridgeConnectionState } from './mobile-web-bridge-connection-state'
import { MOBILE_WEB_PRODUCTION_GRANTS } from './mobile-web-production-grants'
type MobileWebShellInitArgs = {
@@ -28,7 +27,7 @@ export function mobileWebShellInitMessage(
type: 'init',
shellSessionId: args.shellSessionId,
buildId: args.buildId,
connection: mobileWebBridgeConnectionState(args.state),
connection: args.state,
hostDisplayName: args.hostDisplayName,
reconnectAttempts: args.reconnectAttempts,
lastConnectedAt: args.lastConnectedAt,
+2 -1
View File
@@ -15,6 +15,7 @@ import {
parseMobileWebBridgeInitialMessage,
parseMobileWebBridgeShellMessage,
type MobileWebBridgeMessageContext,
type MobileWebConnectionState,
type MobileWebNavigationRoute,
type MobileWebResumeRoute
} from '../../shared/mobile-web/bridge-contract'
@@ -29,7 +30,7 @@ import { subscribeToMobileWebShellMessages } from './native-shell-message-inbox'
export type MobileWebNativeShellState = {
client: MobileWebBridgeClient | null
context: MobileWebBridgeMessageContext | null
connection: 'connecting' | 'connected' | 'offline' | 'recovering'
connection: MobileWebConnectionState
hostDisplayName: string | null
reconnectAttempts: number
lastConnectedAt: number | null
@@ -516,7 +516,7 @@ describe('mobile web bridge shell contract', () => {
type: 'connection',
shellSessionId: SHELL_SESSION_ID,
buildId: BUILD_ID,
state: 'recovering'
state: 'reconnecting'
}
expect(
+10 -1
View File
@@ -165,7 +165,16 @@ const ShellEnvelopeSchema = z.object({
buildId: BuildIdSchema
})
const ConnectionStateSchema = z.enum(['connecting', 'connected', 'offline', 'recovering'])
// The shell publishes the transport's own state; the page decides what to show for each.
const ConnectionStateSchema = z.enum([
'connecting',
'handshaking',
'connected',
'disconnected',
'reconnecting',
'auth-failed'
])
export type MobileWebConnectionState = z.infer<typeof ConnectionStateSchema>
const ConnectionMetricsShape = {
reconnectAttempts: z.number().int().nonnegative().max(1_000_000).optional(),
lastConnectedAt: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).nullable().optional()