Files
Zoltan Papp 6116ea921c Show exit nodes right after connect without visiting another tab (#232)
* Show exit nodes right after connect without visiting another tab

The exit node selector stayed on "No exit nodes available" after every
connect until the user opened the Peers or Resources tab and came back.

The extension reports the tunnel connected from onConnected, which the core
fires once management and signal are up - before the engine is reachable and
a few hundred milliseconds before the first network map arrives. The app reads
the routes on that .connected transition, the core answers "not connected",
and the extension swallowed that error into an empty route list. The app takes
any answer from the core as the truth about the network map, so it cached zero
routes. Nothing re-read them afterwards: the status poll only fetches status,
and route reads happen on .connected and on a view's onAppear, which is why
switching tabs "fixed" it. The extension log shows the same pair on every
connect, a few milliseconds apart:

  onConnected: state=connected
  Error retrieving or encoding route selection details: not connected

Two changes. getSelectRoutes now answers a core error with no payload, which
the adapter already treats as a failed read that leaves the cache alone - an
error is not zero routes. And the extension now tells the app when the
network map moves: the core's onPeersListChanged fires at the end of every
network map update, after the route manager has applied the routes, and on
every route/peer state change, so the previously empty callback posts a
Darwin notification (the same mechanism the app uses to announce MDM policy
changes to the extension, in the other direction). Android's HomeFragment
relies on the same event for the same reason.

RoutesViewModel observes that notification and re-reads the routes, coalesced
to one read per 300 ms window: the core announces a change per peer and per
route, so a connect with dozens of peers produces dozens of notifications
within seconds, and each read is an IPC round-trip that serialises the whole
route list. One pending work item absorbs every notification until it fires;
a change announced after the fire schedules the next, so nothing is lost.
Optimistic selection writes and clearRoutes() retire in-flight and pending
reads, so a read dispatched before a tap can no longer answer after it with
the pre-tap selection; the mutation's own reconcile read is the one allowed
to answer.

* Drop network map notifications overtaken by a clear or a selection write

scheduleNetworkMapRefresh hops to the main queue before it looks at the
pending refresh, and that hop is a window. A notification still in transit
when clearRoutes() or beginSelectionMutation() ran on main found the pending
slot they had just emptied and scheduled a fresh read after the very
invalidation that should have dropped it. After clearRoutes() that read is not
harmless: the optimistic disconnect and the profile switch clear the routes
while the extension is still winding down and still announcing peer changes,
so the read can succeed and repopulate a list nothing clears again - the
selector enabled over nodes the core can no longer apply.

Capture a lock-guarded epoch when the notification arrives, on whatever
thread the Darwin notify center delivers on, advance it in clearRoutes() and
beginSelectionMutation(), and re-check it once the request reaches main. A
notification that was valid when it arrived and is still valid on main
schedules exactly as before.
2026-09-23 15:09:08 +02:00
..
2024-02-28 19:22:16 +01:00