Merge nucleic/rustic-maple-marten into dev
This commit is contained in:
@@ -264,7 +264,8 @@ final class HostConnection {
|
||||
// a gossiped address carries only the IP.
|
||||
tailnetPort: hasTailnet ? 43_753 : nil,
|
||||
relayRoomID: member.addresses?.relayRoomID,
|
||||
relayMembershipToken: nil, relayURL: nil)
|
||||
relayMembershipToken: nil, relayURL: nil,
|
||||
addressesUpdatedAt: member.addresses?.updatedAt)
|
||||
}
|
||||
|
||||
/// Whether the dialable endpoints of a gossiped record differ from the one on file — the
|
||||
@@ -588,16 +589,25 @@ final class HostConnection {
|
||||
guard fingerprint != self.hostID else { continue } // that's the Mac we're on
|
||||
let incoming = Self.pairedHost(from: member)
|
||||
let existing = IdentityStore.pairedHost(id: fingerprint)
|
||||
IdentityStore.mergePairedHost(incoming)
|
||||
// Re-dial not only when a Mac is brand-new, but also when a known Mac's dialable
|
||||
// address changed — a Mac's LAN port is OS-assigned (`.any`), so it lands on a
|
||||
// fresh one every relaunch and re-gossips it. `mergePairedHost` updates the
|
||||
// registry in place, but a connection already retrying the dead old address won't
|
||||
// pick that up on its own (its retry loop reuses the address it was last handed),
|
||||
// so without forcing a fresh reconnect here the phone never reconnects to it.
|
||||
if existing == nil || Self.dialableAddressChanged(from: existing!, to: incoming) {
|
||||
//
|
||||
// Compare against the POST-merge record, not the raw gossip: the merge deliberately
|
||||
// preserves fields the gossip omits (a relay room learned from the host's own
|
||||
// `relayMembership` push, stale-vs-fresh address arbitration), so a member gossiped
|
||||
// by a Mac that lacks those fields is NOT a change. Comparing pre-merge made every
|
||||
// such push read as "changed" forever — a mesh-wide reconnect storm that tore down
|
||||
// in-flight handshakes on each gossip wave and pinned connections on "Connecting…".
|
||||
let merged = IdentityStore.pairedHost(id: fingerprint)
|
||||
if existing == nil
|
||||
|| (merged != nil && Self.dialableAddressChanged(from: existing!, to: merged!)) {
|
||||
changed = true
|
||||
}
|
||||
IdentityStore.mergePairedHost(incoming)
|
||||
}
|
||||
for tombstone in push.tombstones {
|
||||
// fingerprint = first 16 hex of the hostID (sha256 prefix), the registry key.
|
||||
@@ -722,6 +732,13 @@ final class HostConnection {
|
||||
if case .failed = connectivity {} else {
|
||||
connectivity = .failed("Couldn't connect to your Mac — check that it's reachable, then scan again.")
|
||||
}
|
||||
} else if case .failed = connectivity {
|
||||
// keep the surfaced error
|
||||
} else {
|
||||
// Reconnect-mode chain exhausted through channel closes (the LAN/relay connect
|
||||
// watchdogs surface that way) — mirror `asyncAttemptFailed` so the row reads
|
||||
// "host offline" through the retry backoff instead of a stale "Connecting…".
|
||||
connectivity = .hostOffline
|
||||
}
|
||||
connectPlan = nil
|
||||
callbacks.didUpdate()
|
||||
|
||||
@@ -28,6 +28,11 @@ struct PairedHost: Codable, Equatable {
|
||||
var relayRoomID: String?
|
||||
var relayMembershipToken: String?
|
||||
var relayURL: String?
|
||||
/// When the gossiped addresses this record's dial hints came from were stamped at their
|
||||
/// origin (`PeerAddresses.updatedAt`) — the freshness clock `mergePairedHost` arbitrates
|
||||
/// with, so a Mac gossiping a stale snapshot can't clobber a fresher one. Nil for records
|
||||
/// predating the field and for hints seeded straight from a pairing QR.
|
||||
var addressesUpdatedAt: Date? = nil
|
||||
|
||||
var transportHint: SyncTransportHint { transport.flatMap(SyncTransportHint.init(rawValue:)) ?? .lan }
|
||||
}
|
||||
@@ -119,9 +124,17 @@ enum IdentityStore {
|
||||
static func mergePairedHost(_ host: PairedHost) {
|
||||
var hosts = pairedHosts()
|
||||
if let index = hosts.firstIndex(where: { $0.fingerprint == host.fingerprint }) {
|
||||
var merged = host
|
||||
let existing = hosts[index]
|
||||
// Freshness gate on the dial hints: two Macs can gossip disagreeing snapshots of a
|
||||
// third's addresses (one still holds its pre-relaunch LAN port). Adopting the incoming
|
||||
// hints blindly made the record — and every dial loop keyed off it — flip-flop between
|
||||
// the stale and fresh snapshots on alternating pushes. The origin's `updatedAt` rides
|
||||
// in as `addressesUpdatedAt`; an undated record loses to any dated one.
|
||||
let incomingAt = host.addressesUpdatedAt ?? .distantPast
|
||||
let existingAt = existing.addressesUpdatedAt ?? .distantPast
|
||||
var merged = incomingAt >= existingAt ? host : existing
|
||||
merged.deviceID = existing.deviceID // keep our established id for this host
|
||||
merged.hostName = host.hostName // labels carry no clock; newest gossip wins
|
||||
merged.relayMembershipToken = host.relayMembershipToken ?? existing.relayMembershipToken
|
||||
merged.relayRoomID = host.relayRoomID ?? existing.relayRoomID
|
||||
merged.relayURL = host.relayURL ?? existing.relayURL
|
||||
|
||||
Reference in New Issue
Block a user