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.
This commit is contained in:
Neil
2026-10-31 19:29:29 -07:00
committed by GitHub
parent daa11df406
commit b903e5df04
5 changed files with 57 additions and 39 deletions
+3 -11
View File
@@ -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).
@@ -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
@@ -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<typeof BridgeConnectionSnapshotSchema>
/** 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
}
}
@@ -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<typeof BridgeConnectionSnapshotSchema>
export const BridgeGrantsSchema = z.object({
rpc: z.object({
maxPendingRequests: z.number().int().positive(),
@@ -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({