Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 67 additions & 33 deletions apps/ios/ADE/Services/SyncService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,24 @@ final class SyncService: ObservableObject {
.first { !$0.isEmpty }
}

private func profileHasTailnetRoute(_ profile: HostConnectionProfile) -> Bool {
if profile.tailscaleAddress != nil { return true }
if let last = profile.lastSuccessfulAddress, syncIsTailscaleRoute(last) { return true }
return profile.savedAddressCandidates.contains(where: syncIsTailscaleRoute)
}

private func shouldPreferProfile(_ candidate: HostConnectionProfile, over existing: HostConnectionProfile) -> Bool {
if candidate.updatedAt != existing.updatedAt {
return candidate.updatedAt > existing.updatedAt
}
let candidateTailnet = profileHasTailnetRoute(candidate)
let existingTailnet = profileHasTailnetRoute(existing)
if candidateTailnet != existingTailnet {
return candidateTailnet
}
return candidate.lastSuccessfulAddress != nil && existing.lastSuccessfulAddress == nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private func loadSavedProfilesRaw() -> [String: HostConnectionProfile] {
guard let data = UserDefaults.standard.data(forKey: profilesKey) else {
return [:]
Expand All @@ -1434,6 +1452,9 @@ final class SyncService: ObservableObject {
var migrated: [String: HostConnectionProfile] = [:]
for profile in legacyArray {
guard let key = profileStorageKey(profile) else { continue }
if let existing = migrated[key], !shouldPreferProfile(profile, over: existing) {
continue
}
migrated[key] = profile
}
syncConnectLog.warning("Migrated \(legacyArray.count, privacy: .public) legacy array-format host profiles to dict format (\(migrated.count, privacy: .public) keyed)")
Expand All @@ -1453,15 +1474,59 @@ final class SyncService: ObservableObject {
}

private func saveSavedProfiles(_ profiles: [String: HostConnectionProfile]) {
if profiles.isEmpty {
let normalized = deduplicatedProfiles(profiles)
if normalized.isEmpty {
UserDefaults.standard.removeObject(forKey: profilesKey)
return
}
if let data = try? encoder.encode(profiles) {
if let data = try? encoder.encode(normalized) {
UserDefaults.standard.set(data, forKey: profilesKey)
}
}

private func profileIdentityKeys(_ profile: HostConnectionProfile) -> [String] {
var keys: [String] = []
if let id = profile.hostIdentity?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty {
keys.append("identity:\(id)")
}
if let device = profile.lastHostDeviceId?.trimmingCharacters(in: .whitespacesAndNewlines), !device.isEmpty {
keys.append("device:\(device)")
}
if let name = profile.hostName?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty {
keys.append("name:\(name.lowercased()):\(profile.port)")
}
if let last = profile.lastSuccessfulAddress?.trimmingCharacters(in: .whitespacesAndNewlines), !last.isEmpty {
keys.append("addr:\(last):\(profile.port)")
}
return keys
}

private func deduplicatedProfiles(_ profiles: [String: HostConnectionProfile]) -> [String: HostConnectionProfile] {
var canonicalProfiles: [HostConnectionProfile] = []
var keyToIndex: [String: Int] = [:]
for profile in profiles.values {
let identityKeys = profileIdentityKeys(profile)
let matchedIndex = identityKeys.lazy.compactMap { keyToIndex[$0] }.first
if let index = matchedIndex {
if shouldPreferProfile(profile, over: canonicalProfiles[index]) {
canonicalProfiles[index] = profile
}
for key in identityKeys { keyToIndex[key] = index }
} else {
let index = canonicalProfiles.count
canonicalProfiles.append(profile)
for key in identityKeys { keyToIndex[key] = index }
}
}
var byKey: [String: HostConnectionProfile] = [:]
byKey.reserveCapacity(canonicalProfiles.count)
for profile in canonicalProfiles {
guard let key = profileStorageKey(profile) else { continue }
byKey[key] = profile
}
return byKey
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private func migrateTokenIfNeeded(for profile: HostConnectionProfile) {
guard let key = profileStorageKey(profile),
keychain.loadToken(hostKey: key) == nil,
Expand Down Expand Up @@ -1639,37 +1704,6 @@ final class SyncService: ObservableObject {
lastError = nil
}

func reconnectToSavedHost(_ host: DiscoveredSyncHost, preferTailnet: Bool = false) async {
guard let profile = profile(forSavedHost: host), let token = tokenForProfile(profile) else {
lastError = "That saved host is missing pairing credentials. Pair it again from Settings."
connectionState = .error
return
}
keychain.saveToken(token)
saveProfile(profile)
await reconnectIfPossible(userInitiated: true, preferTailnet: preferTailnet || host.tailscaleAddress != nil)
}

private func profile(forSavedHost host: DiscoveredSyncHost) -> HostConnectionProfile? {
let normalizedHostId = host.hostIdentity?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let hostAddresses = Set((host.addresses + (host.tailscaleAddress.map { [$0] } ?? [])).map(syncNormalizedRouteHost))
return loadSavedProfiles().values.first { profile in
if let normalizedHostId,
let profileIdentity = profile.hostIdentity?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
profileIdentity == normalizedHostId {
return true
}
let profileAddresses = Set(
(profile.savedAddressCandidates
+ profile.discoveredLanAddresses
+ (profile.lastSuccessfulAddress.map { [$0] } ?? [])
+ (profile.tailscaleAddress.map { [$0] } ?? []))
.map(syncNormalizedRouteHost)
)
return !profileAddresses.isDisjoint(with: hostAddresses)
}
}

private func shouldPreferTailnetForUserReconnect(_ profile: HostConnectionProfile) -> Bool {
guard let snapshot = lastNetworkPathSnapshot else { return false }
return syncShouldRoamToTailnet(
Expand Down
Loading