Extract source sync runtime policy
This commit is contained in:
parent
47d62a5d51
commit
2df126ebe2
@ -10,7 +10,7 @@ import Foundation
|
||||
import OSLog
|
||||
|
||||
@MainActor
|
||||
final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePersistenceHosting, ConnectedDeviceRuntimeHosting, LocalSourceRuntimeHosting {
|
||||
final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePersistenceHosting, ConnectedDeviceRuntimeHosting, LocalSourceRuntimeHosting, SourceSyncRuntimeHosting {
|
||||
private static let enrichmentWorkerCount = 4
|
||||
private static let sizeWorkerCount = 2
|
||||
private static let minimumVisibleScanDuration: TimeInterval = 0.8
|
||||
@ -206,7 +206,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
||||
}
|
||||
}
|
||||
|
||||
private func startScan(for sourceID: URL, mode: SourceDiscoveryMode) {
|
||||
func startScan(for sourceID: URL, mode: SourceDiscoveryMode) {
|
||||
guard !isShuttingDown else {
|
||||
return
|
||||
}
|
||||
@ -416,53 +416,13 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
||||
}
|
||||
|
||||
func queueAutomaticSync(for sourceID: URL, reason: String, debounce: TimeInterval? = nil) {
|
||||
guard !isShuttingDown else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = source(withID: sourceID), source.availability == .available else {
|
||||
return
|
||||
}
|
||||
|
||||
if source.isScanning {
|
||||
return
|
||||
}
|
||||
|
||||
let resolvedDebounce = debounce ?? Self.automaticSyncDebounce
|
||||
|
||||
automaticSyncTasks[sourceID]?.cancel()
|
||||
updateSource(sourceID) { source in
|
||||
guard !source.isScanning else {
|
||||
return
|
||||
}
|
||||
|
||||
source.scanError = nil
|
||||
if isCachedAvailabilityDiagnostic(source.scanDiagnostic) {
|
||||
source.scanDiagnostic = nil
|
||||
}
|
||||
source.scanStatus = reason
|
||||
source.scanProgress = nil
|
||||
}
|
||||
|
||||
let mode: SourceDiscoveryMode = source.hasCachedContent ? .reconcile : .fullScan
|
||||
|
||||
let task = Task { [weak self] in
|
||||
do {
|
||||
try await Task.sleep(for: .seconds(resolvedDebounce))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
guard let self, !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.startScan(for: sourceID, mode: mode)
|
||||
}
|
||||
}
|
||||
|
||||
automaticSyncTasks[sourceID] = task
|
||||
SourceSyncRuntime.queueAutomaticSync(
|
||||
for: sourceID,
|
||||
reason: reason,
|
||||
debounce: debounce,
|
||||
defaultDebounce: Self.automaticSyncDebounce,
|
||||
on: self
|
||||
)
|
||||
}
|
||||
|
||||
private func purgeCachedArtifacts(for source: MinecraftSource) {
|
||||
@ -481,42 +441,15 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
||||
|
||||
@discardableResult
|
||||
func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
||||
let previousAvailability = source(withID: sourceID)?.availability ?? .unknown
|
||||
let becameAvailable = previousAvailability != .available && newAvailability == .available
|
||||
|
||||
updateSource(sourceID) { source in
|
||||
source.availability = newAvailability
|
||||
|
||||
guard !source.isScanning else {
|
||||
return
|
||||
}
|
||||
|
||||
if newAvailability == .available {
|
||||
source.scanError = nil
|
||||
if isCachedAvailabilityDiagnostic(source.scanDiagnostic) {
|
||||
source.scanDiagnostic = nil
|
||||
}
|
||||
if becameAvailable || source.scanStatus.isEmpty {
|
||||
source.scanStatus = source.indexedItemCount == 0
|
||||
? "No Minecraft items found."
|
||||
: "Loaded \(source.indexedDetailCount) items."
|
||||
}
|
||||
} else {
|
||||
source.scanError = nil
|
||||
source.scanProgress = nil
|
||||
source.scanStatus = source.availabilityDisplayText
|
||||
source.scanDiagnostic = source.cachedAvailabilityDetailText
|
||||
}
|
||||
}
|
||||
|
||||
return (previousAvailability, becameAvailable)
|
||||
SourceSyncRuntime.updateAvailability(for: sourceID, to: newAvailability, on: self)
|
||||
}
|
||||
|
||||
private func isCachedAvailabilityDiagnostic(_ diagnostic: String?) -> Bool {
|
||||
guard let diagnostic else {
|
||||
return false
|
||||
}
|
||||
func cancelAutomaticSync(for sourceID: URL) {
|
||||
automaticSyncTasks[sourceID]?.cancel()
|
||||
automaticSyncTasks[sourceID] = nil
|
||||
}
|
||||
|
||||
return diagnostic.localizedCaseInsensitiveContains("showing cached results")
|
||||
func storeAutomaticSyncTask(_ task: Task<Void, Never>, for sourceID: URL) {
|
||||
automaticSyncTasks[sourceID] = task
|
||||
}
|
||||
}
|
||||
|
||||
121
World Manager for Minecraft/Services/SourceSyncRuntime.swift
Normal file
121
World Manager for Minecraft/Services/SourceSyncRuntime.swift
Normal file
@ -0,0 +1,121 @@
|
||||
//
|
||||
// SourceSyncRuntime.swift
|
||||
// World Manager for Minecraft
|
||||
//
|
||||
// Created by OpenAI Codex on 2026-05-29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol SourceSyncRuntimeHosting: AnyObject {
|
||||
var isShuttingDown: Bool { get }
|
||||
|
||||
func source(withID sourceID: URL) -> MinecraftSource?
|
||||
func updateSource(_ sourceID: URL, mutate: (inout MinecraftSource) -> Void)
|
||||
func cancelAutomaticSync(for sourceID: URL)
|
||||
func storeAutomaticSyncTask(_ task: Task<Void, Never>, for sourceID: URL)
|
||||
func startScan(for sourceID: URL, mode: SourceDiscoveryMode)
|
||||
}
|
||||
|
||||
enum SourceSyncRuntime {
|
||||
static func queueAutomaticSync(
|
||||
for sourceID: URL,
|
||||
reason: String,
|
||||
debounce: TimeInterval?,
|
||||
defaultDebounce: TimeInterval,
|
||||
on host: SourceSyncRuntimeHosting
|
||||
) {
|
||||
guard !host.isShuttingDown else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = host.source(withID: sourceID), source.availability == .available else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !source.isScanning else {
|
||||
return
|
||||
}
|
||||
|
||||
let resolvedDebounce = debounce ?? defaultDebounce
|
||||
|
||||
host.cancelAutomaticSync(for: sourceID)
|
||||
host.updateSource(sourceID) { source in
|
||||
guard !source.isScanning else {
|
||||
return
|
||||
}
|
||||
|
||||
source.scanError = nil
|
||||
if isCachedAvailabilityDiagnostic(source.scanDiagnostic) {
|
||||
source.scanDiagnostic = nil
|
||||
}
|
||||
source.scanStatus = reason
|
||||
source.scanProgress = nil
|
||||
}
|
||||
|
||||
let mode: SourceDiscoveryMode = source.hasCachedContent ? .reconcile : .fullScan
|
||||
let task = Task { [weak host] in
|
||||
do {
|
||||
try await Task.sleep(for: .seconds(resolvedDebounce))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
guard let host, !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
host.startScan(for: sourceID, mode: mode)
|
||||
}
|
||||
}
|
||||
|
||||
host.storeAutomaticSyncTask(task, for: sourceID)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func updateAvailability(
|
||||
for sourceID: URL,
|
||||
to newAvailability: SourceAvailability,
|
||||
on host: SourceSyncRuntimeHosting
|
||||
) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
||||
let previousAvailability = host.source(withID: sourceID)?.availability ?? .unknown
|
||||
let becameAvailable = previousAvailability != .available && newAvailability == .available
|
||||
|
||||
host.updateSource(sourceID) { source in
|
||||
source.availability = newAvailability
|
||||
|
||||
guard !source.isScanning else {
|
||||
return
|
||||
}
|
||||
|
||||
if newAvailability == .available {
|
||||
source.scanError = nil
|
||||
if isCachedAvailabilityDiagnostic(source.scanDiagnostic) {
|
||||
source.scanDiagnostic = nil
|
||||
}
|
||||
if becameAvailable || source.scanStatus.isEmpty {
|
||||
source.scanStatus = source.indexedItemCount == 0
|
||||
? "No Minecraft items found."
|
||||
: "Loaded \(source.indexedDetailCount) items."
|
||||
}
|
||||
} else {
|
||||
source.scanError = nil
|
||||
source.scanProgress = nil
|
||||
source.scanStatus = source.availabilityDisplayText
|
||||
source.scanDiagnostic = source.cachedAvailabilityDetailText
|
||||
}
|
||||
}
|
||||
|
||||
return (previousAvailability, becameAvailable)
|
||||
}
|
||||
|
||||
private static func isCachedAvailabilityDiagnostic(_ diagnostic: String?) -> Bool {
|
||||
guard let diagnostic else {
|
||||
return false
|
||||
}
|
||||
|
||||
return diagnostic.localizedCaseInsensitiveContains("showing cached results")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user