Connection lifecycle
Every device carries its connection lifecycle as data on device.connection.
You never subscribe to connection events — there is no onDisconnect anywhere in
the SDK. You read device.connection and branch on status. This page is the
map of that state machine.
The state machine
Section titled “The state machine”ready is the working state; reconnecting and updating are degraded, not gone.The ConnectionState union
Section titled “The ConnectionState union”Each status carries exactly the data that state needs:
type ConnectionState = | { status: 'disconnected'; cause: 'never-connected' | 'user' | 'link-lost' | 'bluetooth-off' | 'timeout' | 'failed'; error: ConsoleSdkError | null } | { status: 'connecting'; via: 'user' | 'auto-reconnect'; startedAt: number; step: 'link' | 'negotiating' | 'identifying' | 'clock-sync' } | { status: 'ready'; via: 'user' | 'auto-reconnect'; since: number; link: 'healthy' | 'unresponsive' } | { status: 'updating'; since: number; progress: number } | { status: 'reconnecting'; since: number; attempt: number; nextAttemptAt: number; lastError: ConsoleSdkError | null };Read it with an exhaustive switch:
switch (device.connection.status) { case 'disconnected': /* cause tells you why; never-connected is the fresh state */ break; case 'connecting': /* step reports setup progress */ break; case 'ready': /* link === 'unresponsive' badges a degraded-but-alive link */ break; case 'updating': /* progress 0..1 during an OTA transfer */ break; case 'reconnecting': /* attempt / nextAttemptAt for a countdown */ break;}Degraded is not dead
Section titled “Degraded is not dead”Two states are degraded, not gone — and treating them as “disconnected” is a classic bug this model prevents.
reconnectingmeans the link dropped while the device was still wanted (auto-reconnect policy, or a drop mid-workout). It is bounded retries with a visibleattemptandnextAttemptAt, not a terminal state. A workout keeps its device through a blip; a tile shows “reconnecting”, not an empty slot.readywithlink: 'unresponsive'is a degraded health-check reading — the 2-second health check has not heard back, but commands remain callable and may recover the link. Badge it; do not tear the session down.
The device.isConnected convenience is true for ready, reconnecting, and
updating — precisely because a session survives all three.
via provenance drives reconnect toasts
Section titled “via provenance drives reconnect toasts”connecting and ready carry via: 'user' | 'auto-reconnect'. This is how the
reconnect toast is derived from state rather than stored as an event: a ready
connection whose via is 'auto-reconnect' and that you have not acknowledged is,
by definition, a recent reconnection. useAutoReconnect() reads exactly that — see
Fleet & auto-reconnect. There is no
“reconnected” event to catch and no listener to register.
Terminal cause, and the updating hold
Section titled “Terminal cause, and the updating hold”disconnected is the one cause-carrying terminal state. Its cause distinguishes
a fresh device ('never-connected'), a deliberate disconnect() ('user', which
also suppresses auto-reconnect for the session), an exhausted reconnect
('link-lost'), a radio-off drop ('bluetooth-off'), and setup failures
('timeout' / 'failed'); error carries the typed failure when one applies.
updating is fleet-visible so every screen renders a device consistently during
an OTA transfer. Workout arming rejects with command/busy while a device is
updating.
Graceful detach on unmount
Section titled “Graceful detach on unmount”When the provider unmounts (or its config changes), the runtime runs an ordered teardown: connected devices are gracefully detached, any session is abandoned, and the transport disposes — in that order. Connection lifetime is provider lifetime. You never write cleanup for this; unmounting the provider is the disposal path (see Architecture).