"A socket that is dead looks exactly like a socket that is quiet."
When Request and Reply Are Not Enough
Some clients need frames flowing both ways for as long as a screen is open: the terminal app's phone client attached to a live session, the coding engine's remote gateway, the bridges into Pippa's backend. URLSessionWebSocketTask covers the protocol. send(.string(…)) writes a text frame and send(.data(…)) a binary one, and against a Python websockets server the two arrived exactly that way. That difference matters to the peer: the family's Python bridges read JSON from text frames while the terminal gateway wants binary, so the shared connection sends text unless told otherwise.
Three Things That Are Easy to Get Wrong
The shared connection type was composed from three apps that had each solved reconnection, at three levels of completeness. The survey named what separated them.
- Generation. Every teardown mints a new token, and every asynchronous callback compares against it before touching state. Without it, a receive callback from a socket you cancelled two reconnects ago can deliver a stale frame, or report a failure that tears down the socket that replaced it.
- Wanted is not connected, and neither is active. "The user asked to be connected", "the app is in the foreground" and "a socket is open" are three booleans. Backgrounding drops the socket without forgetting the user wants it, and a failure retries only while the first two hold.
- Dead looks quiet. For this lesson a test server was frozen with
SIGSTOPmid-session. Five seconds later the client task still reported.running. A ping with a three-second deadline reported silence. One of the three donor apps had no heartbeat at all, so a dropped tailnet link left it "running" until something tried to write.
Retry the Connection, Never the Command
When the socket drops, the connection reconnects on its own: one second, then doubling to a thirty-second cap, forever by default, because a Mac at home that is merely asleep should be waited for. What it never does is resend a frame that was in flight. A command sent to a terminal might have run before the drop, and replaying it could run it twice, so a lost request is the app's to re-issue or to drop. Two smaller rules come from the same type. The destination is committed only by an explicit connect(to:), so editing the host field in Settings cannot redirect a live connection's next reconnect. And subscribe to the connection's events before connecting: phases are state and are replayed to a new subscriber, frames are not, so a late subscriber can miss the first ones.