SIGN IN SIGN UP

Keep a send on the transport that actually took its bytes

A signed-in startup threw `TypeError: Cannot read properties of undefined
(reading 'byteLength')` three times, from `getTransportError` by way of
`handleSentEncryptedRequestHTTP`.

`MTTransport.send` returns nothing; HTTP is the one transport that answers in
the response to it instead. So whether an answer comes back from `send` is a
property of the transport - and it was decided twice, from two reads of
`this.transport` taken at different times: `sendRequest` attached the HTTP
follow-up from a synchronous read, and `sendEncryptedRequest` read it again
after `await getEncryptedOutput` to pick both the transport and the guard.

The two disagree on every signed-in startup. `Modes.multipleTransports` starts
the client on `https` and `transportController` moves it to `websocket` as soon
as the socket answers a ping - 280ms later in a preview:

    [0.265] [ACC-1-NET-5-C-0] change transport HTTP undefined
    [0.545] [ACC-1-API]       changing transport from https to websocket
    [0.546] [ACC-1-NET-5-C-0] change transport TcpObfuscated HTTP

A message still being encrypted then goes out over the socket, whose `send`
returns `undefined`, and takes the branch that skips the `!result?.byteLength`
guard - while the HTTP follow-up from the earlier read is already waiting on it.

The transport is now read once, after the encryption, and the follow-up is
attached from there. That fixes the other direction too: a socket at compose
time and HTTP at send time used to drop the response body outright.

The crash predates `4940a8315`. Before it `onTransportData` went straight into
`parseResponse`, where `new TLDeserialization(undefined)` threw the same
TypeError one frame away with no try/catch either; that commit only moved the
frame that reports it. `?pfs=1` has no part in this - it picks a key, not a
transport.

Along the way:

* `isHTTPTransport`: the predicate was written out seven times, and the crash
  was two of them disagreeing.
* `settleNoResponseMessages` settles by message and not by id - `cleanupSent`
  takes an http_wait out of `sentMessages` the moment it goes out, so the long
  poll deferred was never settled and `sendingLongPoll` wedged: one long poll
  that went unanswered and the networker never sent another. It is settled on
  the socket path and on a failed encryption too, where nothing else would.
* `onTransportData` says so instead of throwing when it is handed no packet,
  and sends again what nothing answered rather than dropping it in silence.

`src/tests/api/transportSwitchRace.test.ts` drives a networker with no network
behind it and swaps its transport mid-encryption; `src/tests/networkerHarness.ts`
is the factory it shares with `pfsNetworker.test.ts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
E
Eduard Kuzmenko committed
ebb9e034b6b3ad8b27dbe5448d2f50e16470a4b8
Parent: 472e3e7