Extract source sync runtime policy
This commit is contained in:
parent
47d62a5d51
commit
2df126ebe2
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
@MainActor
|
@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 enrichmentWorkerCount = 4
|
||||||
private static let sizeWorkerCount = 2
|
private static let sizeWorkerCount = 2
|
||||||
private static let minimumVisibleScanDuration: TimeInterval = 0.8
|
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 {
|
guard !isShuttingDown else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -416,53 +416,13 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
|||||||
}
|
}
|
||||||
|
|
||||||
func queueAutomaticSync(for sourceID: URL, reason: String, debounce: TimeInterval? = nil) {
|
func queueAutomaticSync(for sourceID: URL, reason: String, debounce: TimeInterval? = nil) {
|
||||||
guard !isShuttingDown else {
|
SourceSyncRuntime.queueAutomaticSync(
|
||||||
return
|
for: sourceID,
|
||||||
}
|
reason: reason,
|
||||||
|
debounce: debounce,
|
||||||
guard let source = source(withID: sourceID), source.availability == .available else {
|
defaultDebounce: Self.automaticSyncDebounce,
|
||||||
return
|
on: self
|
||||||
}
|
)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func purgeCachedArtifacts(for source: MinecraftSource) {
|
private func purgeCachedArtifacts(for source: MinecraftSource) {
|
||||||
@ -481,42 +441,15 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
||||||
let previousAvailability = source(withID: sourceID)?.availability ?? .unknown
|
SourceSyncRuntime.updateAvailability(for: sourceID, to: newAvailability, on: self)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isCachedAvailabilityDiagnostic(_ diagnostic: String?) -> Bool {
|
func cancelAutomaticSync(for sourceID: URL) {
|
||||||
guard let diagnostic else {
|
automaticSyncTasks[sourceID]?.cancel()
|
||||||
return false
|
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