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({