From b903e5df044ee2fd7dc638d8884ebaa092c1f324 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:29:29 -0700 Subject: [PATCH] refactor(mobile): move the bridge connection snapshot into its own module (#36818) The connection snapshot schema, the shell's snapshot builder and the page's not-ready guard move to bridge-connection-snapshot.ts and BridgeConnectionCache.require(). No behavior change: the envelope re-exports the schema, so every frame parses and serializes as before. Prepares the next change, which adds connection path fields to the snapshot, without pushing the bridge modules over the size cap. --- mobile/src/mobile-web-shell/bridge-host.ts | 14 ++----- .../bridge/bridge-client-connection-cache.ts | 11 +++++- .../bridge/bridge-connection-snapshot.ts | 37 +++++++++++++++++++ .../bridge/bridge-envelope.ts | 26 +++---------- .../bridge/bridge-rpc-client.ts | 8 +--- 5 files changed, 57 insertions(+), 39 deletions(-) create mode 100644 mobile/src/mobile-web-shell/bridge/bridge-connection-snapshot.ts diff --git a/mobile/src/mobile-web-shell/bridge-host.ts b/mobile/src/mobile-web-shell/bridge-host.ts index 786e9f50633..37ff0817907 100644 --- a/mobile/src/mobile-web-shell/bridge-host.ts +++ b/mobile/src/mobile-web-shell/bridge-host.ts @@ -9,8 +9,7 @@ import { BRIDGE_PROTOCOL_VERSION, BRIDGE_RPC_ENVELOPE_GRANT, readBridgeClientMessage, - type BridgeClientMessage, - type BridgeConnectionSnapshot + type BridgeClientMessage } from './bridge/bridge-envelope' import { BridgePageRouteGrantsSchema } from './bridge/bridge-page-route-grants' import { createBridgeInitFrame } from './bridge/bridge-init-frame' @@ -21,6 +20,7 @@ import { createBridgeNotifyForwarder } from './bridge-host-notify' import { createBridgeHostRoute } from './bridge-host-route' import { pageStorageForReader } from './page-storage-keys' import { BRIDGE_TERMINAL_KEY_FRAME } from './bridge/bridge-terminal-key' +import { readBridgeConnectionSnapshot } from './bridge/bridge-connection-snapshot' import type { BridgeHost, BridgeHostOptions } from './bridge-host-contract' export type { BridgeHost, BridgeHostDiagnostic, BridgeHostOptions } from './bridge-host-contract' @@ -87,15 +87,7 @@ export function createBridgeHost(options: BridgeHostOptions): BridgeHost { }) // Use the event’s state; listeners can run before the mirrored getter updates. - function snapshot(state?: ConnectionState): BridgeConnectionSnapshot { - return { - state: state ?? client.getState(), - reconnectAttempt: client.getReconnectAttempt(), - lastConnectedAt: client.getLastConnectedAt(), - lastInboundAt: client.getLastInboundAt?.() ?? null, - generation: client.getGeneration?.() ?? null - } - } + const snapshot = (state?: ConnectionState) => readBridgeConnectionSnapshot(client, state) /** * Posts `init`, and answers nothing (ruling 34). diff --git a/mobile/src/mobile-web-shell/bridge/bridge-client-connection-cache.ts b/mobile/src/mobile-web-shell/bridge/bridge-client-connection-cache.ts index ae1801af5e2..f4336a57a14 100644 --- a/mobile/src/mobile-web-shell/bridge/bridge-client-connection-cache.ts +++ b/mobile/src/mobile-web-shell/bridge/bridge-client-connection-cache.ts @@ -1,5 +1,6 @@ import type { ConnectionState } from '../../transport/types' -import type { BridgeConnectionSnapshot } from './bridge-envelope' +import { BridgeClientNotReadyError } from './bridge-client-errors' +import type { BridgeConnectionSnapshot } from './bridge-connection-snapshot' export type BridgeSnapshotOutcome = 'applied' | 'stale' | 'unprimed' @@ -12,6 +13,14 @@ export class BridgeConnectionCache { return this.held } + // Getters run only after init primed the cache; reading earlier is a page bug, not a disconnect. + require(): BridgeConnectionSnapshot { + if (this.held === null) { + throw new BridgeClientNotReadyError() + } + return this.held + } + // Repriming identical state is not a transition and must notify nobody. prime(snapshot: BridgeConnectionSnapshot): void { const changed = this.held?.state !== snapshot.state diff --git a/mobile/src/mobile-web-shell/bridge/bridge-connection-snapshot.ts b/mobile/src/mobile-web-shell/bridge/bridge-connection-snapshot.ts new file mode 100644 index 00000000000..389d86ac379 --- /dev/null +++ b/mobile/src/mobile-web-shell/bridge/bridge-connection-snapshot.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' +import type { RpcClient } from '../../transport/rpc-client' +import type { ConnectionState } from '../../transport/types' + +export const BRIDGE_CONNECTION_STATES = [ + 'connecting', + 'handshaking', + 'connected', + 'disconnected', + 'reconnecting', + 'auth-failed' +] as const + +// Send whole connection snapshots so lost frames cannot leave partial state; generation detects snapshots that need repair. +export const BridgeConnectionSnapshotSchema = z.object({ + state: z.enum(BRIDGE_CONNECTION_STATES), + reconnectAttempt: z.number().int().nonnegative(), + lastConnectedAt: z.number().nullable(), + lastInboundAt: z.number().nullable(), + generation: z.number().int().nonnegative().nullable() +}) + +export type BridgeConnectionSnapshot = z.infer + +/** The shell's whole view of its client; `state` is the event's, which a getter can outrun. */ +export function readBridgeConnectionSnapshot( + client: RpcClient, + state?: ConnectionState +): BridgeConnectionSnapshot { + return { + state: state ?? client.getState(), + reconnectAttempt: client.getReconnectAttempt(), + lastConnectedAt: client.getLastConnectedAt(), + lastInboundAt: client.getLastInboundAt?.() ?? null, + generation: client.getGeneration?.() ?? null + } +} diff --git a/mobile/src/mobile-web-shell/bridge/bridge-envelope.ts b/mobile/src/mobile-web-shell/bridge/bridge-envelope.ts index 6e96fac47fc..edd0b00a87c 100644 --- a/mobile/src/mobile-web-shell/bridge/bridge-envelope.ts +++ b/mobile/src/mobile-web-shell/bridge/bridge-envelope.ts @@ -5,6 +5,7 @@ import { NODE_PLATFORM_NAMES } from '../../transport/mobile-runtime-host-platfor import { RpcEnvelopeFieldsSchema } from '../../transport/rpc-envelope-fields' import { isRpcResponse } from '../../transport/rpc-response-shape' import type { RpcResponse } from '../../transport/types' +import { BridgeConnectionSnapshotSchema } from './bridge-connection-snapshot' import { BridgeErrorCaptureSchema } from './bridge-error-capture' import { BridgeInitRouteSchema, type BridgeInitRoute } from './bridge-init-route' import { BridgePageRouteGrantsSchema } from './bridge-page-route-grants' @@ -21,6 +22,11 @@ import { } from './bridge-terminal-key' import { BRIDGE_ID_PATTERN, idSchema, methodSchema, versionSchema } from './bridge-frame-fields' +export { + BRIDGE_CONNECTION_STATES, + BridgeConnectionSnapshotSchema, + type BridgeConnectionSnapshot +} from './bridge-connection-snapshot' export { BRIDGE_EXTERNAL_LINK_GRANT, BRIDGE_FAULT_GRANT, @@ -46,28 +52,8 @@ import { type BridgeRead } from './bridge-caps' -export const BRIDGE_CONNECTION_STATES = [ - 'connecting', - 'handshaking', - 'connected', - 'disconnected', - 'reconnecting', - 'auth-failed' -] as const - export const BRIDGE_BINARY_FORMATS = ['jpeg', 'png'] as const -// Send whole connection snapshots so lost frames cannot leave partial state; generation detects snapshots that need repair. -export const BridgeConnectionSnapshotSchema = z.object({ - state: z.enum(BRIDGE_CONNECTION_STATES), - reconnectAttempt: z.number().int().nonnegative(), - lastConnectedAt: z.number().nullable(), - lastInboundAt: z.number().nullable(), - generation: z.number().int().nonnegative().nullable() -}) - -export type BridgeConnectionSnapshot = z.infer - export const BridgeGrantsSchema = z.object({ rpc: z.object({ maxPendingRequests: z.number().int().positive(), diff --git a/mobile/src/mobile-web-shell/bridge/bridge-rpc-client.ts b/mobile/src/mobile-web-shell/bridge/bridge-rpc-client.ts index fa8a339d005..0e7bd677a29 100644 --- a/mobile/src/mobile-web-shell/bridge/bridge-rpc-client.ts +++ b/mobile/src/mobile-web-shell/bridge/bridge-rpc-client.ts @@ -149,13 +149,7 @@ export function createBridgeRpcClient(options: BridgeRpcClientOptions): BridgeRp } } - function snapshot(): BridgeConnectionSnapshot { - const held = cache.read() - if (held === null) { - throw new BridgeClientNotReadyError() - } - return held - } + const snapshot = () => cache.require() // Repeated init for the same session updates route/insets/cache while preserving exchanges and the original storage snapshot. A different session id invalidates prior work. const shellSession = createBridgeClientShellSession({