Extract connected device source helpers

This commit is contained in:
John Burwell 2026-05-28 22:41:11 -05:00
parent 1e0447a2b1
commit 3a30b94369
2 changed files with 244 additions and 204 deletions

View File

@ -0,0 +1,220 @@
//
// SourceConnectedDevices.swift
// World Manager for Minecraft
//
// Created by OpenAI Codex on 2026-05-28.
//
import Foundation
struct ConnectedDeviceSidebarEntry: Identifiable, Hashable {
let device: ConnectedDevice
let containers: [DeviceAppContainer]
let matchedSourceID: URL?
let discoveryErrorDescription: String?
var id: String { device.id }
var minecraftContainer: DeviceAppContainer? {
containers.first(where: { $0.appID == "com.mojang.minecraftpe" })
?? containers.first(where: { $0.minecraftFolderRelativePath != nil })
}
var hasMinecraftContainer: Bool {
minecraftContainer != nil
}
}
struct CachedConnectedDeviceDiscovery {
let device: ConnectedDevice
let containers: [DeviceAppContainer]
let discoveryErrorDescription: String?
let refreshedAt: Date
}
struct ConnectedDeviceRefreshContext {
let existingSources: [MinecraftSource]
let sourceFactory: ConnectedDeviceSourceFactory
func knownSourceIDs(for device: ConnectedDevice) -> [URL] {
existingSources.compactMap { source -> URL? in
guard case .connectedDevice(let expectedDevice, _) = source.origin else {
return nil
}
return expectedDevice.udid == device.udid ? source.id : nil
}
}
func hasKnownSource(for device: ConnectedDevice) -> Bool {
!knownSourceIDs(for: device).isEmpty
}
func matchingSourceID(
for device: ConnectedDevice,
containers: [DeviceAppContainer]
) -> URL? {
for container in containers {
let sourceID = sourceFactory.makeSourceIdentifier(
device: device,
container: container
)
if existingSources.contains(where: { $0.id == sourceID }) {
return sourceID
}
}
return nil
}
}
enum ConnectedDeviceDiscoveryCachePolicy {
private static let usbCacheTTL: TimeInterval = 60.0
private static let networkCacheTTL: TimeInterval = 180.0
static func cachedDiscovery(
for device: ConnectedDevice,
cache: [String: CachedConnectedDeviceDiscovery],
isActivelyScanning: Bool,
now: Date = Date()
) -> CachedConnectedDeviceDiscovery? {
guard let cachedDiscovery = cache[device.udid] else {
return nil
}
if isActivelyScanning {
return cachedDiscovery
}
let age = now.timeIntervalSince(cachedDiscovery.refreshedAt)
guard age <= discoveryCacheTTL(for: device) else {
return nil
}
guard cachedDiscovery.device.connection == device.connection,
cachedDiscovery.device.trustState == device.trustState,
cachedDiscovery.device.name == device.name else {
return nil
}
return cachedDiscovery
}
static func cacheDiscovery(
for device: ConnectedDevice,
containers: [DeviceAppContainer],
discoveryErrorDescription: String?,
now: Date = Date()
) -> CachedConnectedDeviceDiscovery {
CachedConnectedDeviceDiscovery(
device: device,
containers: containers,
discoveryErrorDescription: discoveryErrorDescription,
refreshedAt: now
)
}
private static func discoveryCacheTTL(for device: ConnectedDevice) -> TimeInterval {
switch device.connection {
case .usb:
return usbCacheTTL
case .network:
return networkCacheTTL
}
}
}
enum ConnectedDeviceSourcePolicy {
static func availability(for device: ConnectedDevice, hasMinecraftContainer: Bool) -> SourceAvailability {
guard hasMinecraftContainer else {
return .unavailable
}
switch device.trustState {
case .trusted:
return .available
case .locked, .untrusted:
return .limited
case .unavailable:
return .disconnected
}
}
static func preferredDeviceName(
currentName: String,
fallbackDeviceName: String,
fallbackDisplayName: String
) -> String {
if let sanitizedCurrentName = sanitizedDeviceName(currentName) {
return sanitizedCurrentName
}
if let sanitizedFallbackName = sanitizedDeviceName(fallbackDeviceName) {
return sanitizedFallbackName
}
if let displayNamePrefix = fallbackDisplayName.components(separatedBy: "").first,
let sanitizedDisplayName = sanitizedDeviceName(displayNamePrefix) {
return sanitizedDisplayName
}
return "Unknown Device"
}
static func sanitizedDeviceName(_ candidate: String) -> String? {
let trimmedCandidate = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedCandidate.isEmpty else {
return nil
}
let normalizedCandidate = trimmedCandidate.lowercased()
guard normalizedCandidate != "unknown device",
normalizedCandidate != "unknown device..." else {
return nil
}
return trimmedCandidate
}
static func shouldRefreshSource(_ source: MinecraftSource) -> Bool {
guard !source.isScanning else {
return false
}
return hasRefreshDebt(source)
}
static func shouldRefreshSourceOnReconnect(_ source: MinecraftSource, now: Date = Date()) -> Bool {
guard !source.isScanning else {
return false
}
if hasRefreshDebt(source) {
return true
}
guard let lastScanDate = source.lastScanDate else {
return true
}
let reconnectGracePeriod: TimeInterval = 5 * 60
if now.timeIntervalSince(lastScanDate) < reconnectGracePeriod {
return false
}
return shouldRefreshSource(source)
}
static func hasRefreshDebt(_ source: MinecraftSource) -> Bool {
guard source.origin.kind == .connectedDevice else {
return false
}
guard !source.rawItems.isEmpty else {
return true
}
let itemCount = source.rawItems.count
return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
}
}

View File

@ -9,31 +9,6 @@ import Combine
import Foundation import Foundation
import OSLog import OSLog
struct ConnectedDeviceSidebarEntry: Identifiable, Hashable {
let device: ConnectedDevice
let containers: [DeviceAppContainer]
let matchedSourceID: URL?
let discoveryErrorDescription: String?
var id: String { device.id }
var minecraftContainer: DeviceAppContainer? {
containers.first(where: { $0.appID == "com.mojang.minecraftpe" })
?? containers.first(where: { $0.minecraftFolderRelativePath != nil })
}
var hasMinecraftContainer: Bool {
minecraftContainer != nil
}
}
private struct CachedConnectedDeviceDiscovery {
let device: ConnectedDevice
let containers: [DeviceAppContainer]
let discoveryErrorDescription: String?
let refreshedAt: Date
}
@MainActor @MainActor
final class SourceLibrary: ObservableObject { final class SourceLibrary: ObservableObject {
private static let enrichmentWorkerCount = 4 private static let enrichmentWorkerCount = 4
@ -45,8 +20,6 @@ final class SourceLibrary: ObservableObject {
private static let connectedDeviceRefreshIntervalWhileScanning: TimeInterval = 5.0 private static let connectedDeviceRefreshIntervalWhileScanning: TimeInterval = 5.0
private static let usbConnectedDeviceAutoRefreshInterval: TimeInterval = 45.0 private static let usbConnectedDeviceAutoRefreshInterval: TimeInterval = 45.0
private static let networkConnectedDeviceAutoRefreshInterval: TimeInterval = 120.0 private static let networkConnectedDeviceAutoRefreshInterval: TimeInterval = 120.0
private static let usbConnectedDeviceDiscoveryCacheTTL: TimeInterval = 60.0
private static let networkConnectedDeviceDiscoveryCacheTTL: TimeInterval = 180.0
private static let performanceLogger = Logger( private static let performanceLogger = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "WorldManagerForMinecraft", subsystem: Bundle.main.bundleIdentifier ?? "WorldManagerForMinecraft",
category: "ConnectedDevicePerformance" category: "ConnectedDevicePerformance"
@ -955,6 +928,10 @@ final class SourceLibrary: ObservableObject {
var entries: [ConnectedDeviceSidebarEntry] = [] var entries: [ConnectedDeviceSidebarEntry] = []
var matchedSourceIDs = Set<URL>() var matchedSourceIDs = Set<URL>()
let refreshContext = ConnectedDeviceRefreshContext(
existingSources: sources,
sourceFactory: connectedDeviceSourceFactory
)
let activeScanningDeviceUDIDs = Set( let activeScanningDeviceUDIDs = Set(
sources.compactMap { source -> String? in sources.compactMap { source -> String? in
guard source.isScanning, case .connectedDevice(let device, _) = source.origin else { guard source.isScanning, case .connectedDevice(let device, _) = source.origin else {
@ -968,7 +945,7 @@ final class SourceLibrary: ObservableObject {
cachedDeviceDiscoveryByUDID = cachedDeviceDiscoveryByUDID.filter { currentDeviceUDIDs.contains($0.key) } cachedDeviceDiscoveryByUDID = cachedDeviceDiscoveryByUDID.filter { currentDeviceUDIDs.contains($0.key) }
for device in devices { for device in devices {
let knownSourceIDs = knownConnectedDeviceSourceIDs(for: device) let knownSourceIDs = refreshContext.knownSourceIDs(for: device)
if !knownSourceIDs.isEmpty { if !knownSourceIDs.isEmpty {
matchedSourceIDs.formUnion(knownSourceIDs) matchedSourceIDs.formUnion(knownSourceIDs)
let cachedContainers = cachedDeviceDiscoveryByUDID[device.udid]?.containers ?? [] let cachedContainers = cachedDeviceDiscoveryByUDID[device.udid]?.containers ?? []
@ -985,7 +962,11 @@ final class SourceLibrary: ObservableObject {
let containers: [DeviceAppContainer] let containers: [DeviceAppContainer]
let discoveryErrorDescription: String? let discoveryErrorDescription: String?
if let cachedDiscovery = cachedDiscovery(for: device, isActivelyScanning: activeScanningDeviceUDIDs.contains(device.udid)) { if let cachedDiscovery = ConnectedDeviceDiscoveryCachePolicy.cachedDiscovery(
for: device,
cache: cachedDeviceDiscoveryByUDID,
isActivelyScanning: activeScanningDeviceUDIDs.contains(device.udid)
) {
containers = cachedDiscovery.containers containers = cachedDiscovery.containers
discoveryErrorDescription = cachedDiscovery.discoveryErrorDescription discoveryErrorDescription = cachedDiscovery.discoveryErrorDescription
} else { } else {
@ -1023,10 +1004,7 @@ final class SourceLibrary: ObservableObject {
} }
} }
let matchedSourceID = matchingConnectedDeviceSourceID( let matchedSourceID = refreshContext.matchingSourceID(for: device, containers: containers)
device: device,
containers: containers
)
if let matchedSourceID { if let matchedSourceID {
matchedSourceIDs.insert(matchedSourceID) matchedSourceIDs.insert(matchedSourceID)
@ -1039,7 +1017,7 @@ final class SourceLibrary: ObservableObject {
let shouldDisplayEntry = let shouldDisplayEntry =
matchedSourceID == nil matchedSourceID == nil
&& !hasKnownConnectedDeviceSource(for: device) && !refreshContext.hasKnownSource(for: device)
&& (!containers.isEmpty || device.trustState != .trusted) && (!containers.isEmpty || device.trustState != .trusted)
if shouldDisplayEntry { if shouldDisplayEntry {
@ -1075,88 +1053,27 @@ final class SourceLibrary: ObservableObject {
lastMatchedConnectedSourceIDs = matchedSourceIDs lastMatchedConnectedSourceIDs = matchedSourceIDs
} }
private func cachedDiscovery(for device: ConnectedDevice, isActivelyScanning: Bool) -> CachedConnectedDeviceDiscovery? {
guard let cachedDiscovery = cachedDeviceDiscoveryByUDID[device.udid] else {
return nil
}
if isActivelyScanning {
return cachedDiscovery
}
let age = Date().timeIntervalSince(cachedDiscovery.refreshedAt)
guard age <= discoveryCacheTTL(for: device) else {
return nil
}
guard cachedDiscovery.device.connection == device.connection,
cachedDiscovery.device.trustState == device.trustState,
cachedDiscovery.device.name == device.name else {
return nil
}
return cachedDiscovery
}
private func discoveryCacheTTL(for device: ConnectedDevice) -> TimeInterval {
switch device.connection {
case .usb:
return Self.usbConnectedDeviceDiscoveryCacheTTL
case .network:
return Self.networkConnectedDeviceDiscoveryCacheTTL
}
}
private func cacheDeviceDiscovery( private func cacheDeviceDiscovery(
device: ConnectedDevice, device: ConnectedDevice,
containers: [DeviceAppContainer], containers: [DeviceAppContainer],
discoveryErrorDescription: String? discoveryErrorDescription: String?
) { ) {
cachedDeviceDiscoveryByUDID[device.udid] = CachedConnectedDeviceDiscovery( cachedDeviceDiscoveryByUDID[device.udid] = ConnectedDeviceDiscoveryCachePolicy.cacheDiscovery(
device: device, for: device,
containers: containers, containers: containers,
discoveryErrorDescription: discoveryErrorDescription, discoveryErrorDescription: discoveryErrorDescription
refreshedAt: Date()
) )
} }
private func matchingConnectedDeviceSourceID(
device: ConnectedDevice,
containers: [DeviceAppContainer]
) -> URL? {
for container in containers {
let sourceID = connectedDeviceSourceFactory.makeSourceIdentifier(
device: device,
container: container
)
if sources.contains(where: { $0.id == sourceID }) {
return sourceID
}
}
return nil
}
private func knownConnectedDeviceSourceIDs(for device: ConnectedDevice) -> [URL] {
sources.compactMap { source -> URL? in
guard case .connectedDevice(let expectedDevice, _) = source.origin else {
return nil
}
return expectedDevice.udid == device.udid ? source.id : nil
}
}
private func hasKnownConnectedDeviceSource(for device: ConnectedDevice) -> Bool {
!knownConnectedDeviceSourceIDs(for: device).isEmpty
}
private func refreshMatchedConnectedDeviceSource( private func refreshMatchedConnectedDeviceSource(
sourceID: URL, sourceID: URL,
device: ConnectedDevice, device: ConnectedDevice,
containers: [DeviceAppContainer] containers: [DeviceAppContainer]
) { ) {
let nextAvailability = availability(for: device, hasMinecraftContainer: true) let nextAvailability = ConnectedDeviceSourcePolicy.availability(
for: device,
hasMinecraftContainer: true
)
updateSource(sourceID) { source in updateSource(sourceID) { source in
guard case .connectedDevice(let previousDevice, let previousContainer) = source.origin else { guard case .connectedDevice(let previousDevice, let previousContainer) = source.origin else {
return return
@ -1167,7 +1084,7 @@ final class SourceLibrary: ObservableObject {
}) ?? previousContainer }) ?? previousContainer
var resolvedDevice = device var resolvedDevice = device
resolvedDevice.name = preferredConnectedDeviceName( resolvedDevice.name = ConnectedDeviceSourcePolicy.preferredDeviceName(
currentName: device.name, currentName: device.name,
fallbackDeviceName: previousDevice.name, fallbackDeviceName: previousDevice.name,
fallbackDisplayName: source.displayName fallbackDisplayName: source.displayName
@ -1187,7 +1104,7 @@ final class SourceLibrary: ObservableObject {
} }
if transition.becameAvailable { if transition.becameAvailable {
if shouldRefreshConnectedDeviceOnReconnect(source, device: device) { if ConnectedDeviceSourcePolicy.shouldRefreshSourceOnReconnect(source) {
queueAutomaticSync( queueAutomaticSync(
for: sourceID, for: sourceID,
reason: source.hasCachedContent reason: source.hasCachedContent
@ -1198,7 +1115,7 @@ final class SourceLibrary: ObservableObject {
return return
} }
if shouldRefreshConnectedDeviceSource(source, device: device) { if ConnectedDeviceSourcePolicy.shouldRefreshSource(source) {
queueAutomaticSync(for: sourceID, reason: "Refreshing device library...") queueAutomaticSync(for: sourceID, reason: "Refreshing device library...")
} }
} }
@ -1215,57 +1132,6 @@ final class SourceLibrary: ObservableObject {
} }
} }
private func availability(for device: ConnectedDevice, hasMinecraftContainer: Bool) -> SourceAvailability {
guard hasMinecraftContainer else {
return .unavailable
}
switch device.trustState {
case .trusted:
return .available
case .locked, .untrusted:
return .limited
case .unavailable:
return .disconnected
}
}
private func preferredConnectedDeviceName(
currentName: String,
fallbackDeviceName: String,
fallbackDisplayName: String
) -> String {
if let sanitizedCurrentName = sanitizedConnectedDeviceName(currentName) {
return sanitizedCurrentName
}
if let sanitizedFallbackName = sanitizedConnectedDeviceName(fallbackDeviceName) {
return sanitizedFallbackName
}
if let displayNamePrefix = fallbackDisplayName.components(separatedBy: "").first,
let sanitizedDisplayName = sanitizedConnectedDeviceName(displayNamePrefix) {
return sanitizedDisplayName
}
return "Unknown Device"
}
private func sanitizedConnectedDeviceName(_ candidate: String) -> String? {
let trimmedCandidate = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedCandidate.isEmpty else {
return nil
}
let normalizedCandidate = trimmedCandidate.lowercased()
guard normalizedCandidate != "unknown device",
normalizedCandidate != "unknown device..." else {
return nil
}
return trimmedCandidate
}
private func restorePersistedSources() async { private func restorePersistedSources() async {
defer { defer {
isRestoringPersistedSources = false isRestoringPersistedSources = false
@ -1289,14 +1155,14 @@ final class SourceLibrary: ObservableObject {
) )
if case .connectedDevice(let device, let container) = source.origin { if case .connectedDevice(let device, let container) = source.origin {
var repairedDevice = device var repairedDevice = device
repairedDevice.name = preferredConnectedDeviceName( repairedDevice.name = ConnectedDeviceSourcePolicy.preferredDeviceName(
currentName: device.name, currentName: device.name,
fallbackDeviceName: "", fallbackDeviceName: "",
fallbackDisplayName: record.displayName fallbackDisplayName: record.displayName
) )
source.origin = .connectedDevice(device: repairedDevice, container: container) source.origin = .connectedDevice(device: repairedDevice, container: container)
let persistedDeviceName = record.displayName.components(separatedBy: "").first ?? record.displayName let persistedDeviceName = record.displayName.components(separatedBy: "").first ?? record.displayName
if sanitizedConnectedDeviceName(persistedDeviceName) == nil { if ConnectedDeviceSourcePolicy.sanitizedDeviceName(persistedDeviceName) == nil {
source.displayName = connectedDeviceSourceFactory.displayName( source.displayName = connectedDeviceSourceFactory.displayName(
for: repairedDevice, for: repairedDevice,
container: container container: container
@ -1654,52 +1520,6 @@ final class SourceLibrary: ObservableObject {
return diagnostic.localizedCaseInsensitiveContains("showing cached results") return diagnostic.localizedCaseInsensitiveContains("showing cached results")
} }
private func shouldRefreshConnectedDeviceSource(_ source: MinecraftSource, device: ConnectedDevice) -> Bool {
guard !source.isScanning else {
return false
}
if connectedDeviceSourceHasRefreshDebt(source) {
return true
}
_ = device
return false
}
private func shouldRefreshConnectedDeviceOnReconnect(_ source: MinecraftSource, device: ConnectedDevice) -> Bool {
guard !source.isScanning else {
return false
}
if connectedDeviceSourceHasRefreshDebt(source) {
return true
}
guard let lastScanDate = source.lastScanDate else {
return true
}
let reconnectGracePeriod: TimeInterval = 5 * 60
if Date().timeIntervalSince(lastScanDate) < reconnectGracePeriod {
return false
}
return shouldRefreshConnectedDeviceSource(source, device: device)
}
private func connectedDeviceSourceHasRefreshDebt(_ source: MinecraftSource) -> Bool {
guard source.origin.kind == .connectedDevice else {
return false
}
guard !source.rawItems.isEmpty else {
return true
}
let itemCount = source.rawItems.count
return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
}
private func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool { private func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool {
let candidateEmbedded = isEmbeddedWorldPack(candidate) let candidateEmbedded = isEmbeddedWorldPack(candidate)
let existingEmbedded = isEmbeddedWorldPack(existing) let existingEmbedded = isEmbeddedWorldPack(existing)