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.
|
// a gossiped address carries only the IP.
|
||||||
tailnetPort: hasTailnet ? 43_753 : nil,
|
tailnetPort: hasTailnet ? 43_753 : nil,
|
||||||
relayRoomID: member.addresses?.relayRoomID,
|
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
|
/// 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
|
guard fingerprint != self.hostID else { continue } // that's the Mac we're on
|
||||||
let incoming = Self.pairedHost(from: member)
|
let incoming = Self.pairedHost(from: member)
|
||||||
let existing = IdentityStore.pairedHost(id: fingerprint)
|
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
|
// 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
|
// 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
|
// 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
|
// 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),
|
// 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.
|
// 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
|
changed = true
|
||||||
}
|
}
|
||||||
IdentityStore.mergePairedHost(incoming)
|
|
||||||
}
|
}
|
||||||
for tombstone in push.tombstones {
|
for tombstone in push.tombstones {
|
||||||
// fingerprint = first 16 hex of the hostID (sha256 prefix), the registry key.
|
// fingerprint = first 16 hex of the hostID (sha256 prefix), the registry key.
|
||||||
@@ -722,6 +732,13 @@ final class HostConnection {
|
|||||||
if case .failed = connectivity {} else {
|
if case .failed = connectivity {} else {
|
||||||
connectivity = .failed("Couldn't connect to your Mac — check that it's reachable, then scan again.")
|
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
|
connectPlan = nil
|
||||||
callbacks.didUpdate()
|
callbacks.didUpdate()
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ struct PairedHost: Codable, Equatable {
|
|||||||
var relayRoomID: String?
|
var relayRoomID: String?
|
||||||
var relayMembershipToken: String?
|
var relayMembershipToken: String?
|
||||||
var relayURL: 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 }
|
var transportHint: SyncTransportHint { transport.flatMap(SyncTransportHint.init(rawValue:)) ?? .lan }
|
||||||
}
|
}
|
||||||
@@ -119,9 +124,17 @@ enum IdentityStore {
|
|||||||
static func mergePairedHost(_ host: PairedHost) {
|
static func mergePairedHost(_ host: PairedHost) {
|
||||||
var hosts = pairedHosts()
|
var hosts = pairedHosts()
|
||||||
if let index = hosts.firstIndex(where: { $0.fingerprint == host.fingerprint }) {
|
if let index = hosts.firstIndex(where: { $0.fingerprint == host.fingerprint }) {
|
||||||
var merged = host
|
|
||||||
let existing = hosts[index]
|
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.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.relayMembershipToken = host.relayMembershipToken ?? existing.relayMembershipToken
|
||||||
merged.relayRoomID = host.relayRoomID ?? existing.relayRoomID
|
merged.relayRoomID = host.relayRoomID ?? existing.relayRoomID
|
||||||
merged.relayURL = host.relayURL ?? existing.relayURL
|
merged.relayURL = host.relayURL ?? existing.relayURL
|
||||||
|
|||||||
Reference in New Issue
Block a user