Extract local source runtime orchestration
This commit is contained in:
parent
cab38254dd
commit
5baf25f601
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePersistenceHosting, ConnectedDeviceRuntimeHosting {
|
final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePersistenceHosting, ConnectedDeviceRuntimeHosting, LocalSourceRuntimeHosting {
|
||||||
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
|
||||||
@ -226,7 +226,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
|||||||
scanTasks[sourceID] = task
|
scanTasks[sourceID] = task
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hasActiveScan: Bool {
|
var hasActiveScan: Bool {
|
||||||
sources.contains(where: \.isScanning)
|
sources.contains(where: \.isScanning)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,77 +477,15 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func runLocalSourceRefreshLoop() async {
|
private func runLocalSourceRefreshLoop() async {
|
||||||
while !Task.isCancelled && !isShuttingDown {
|
await LocalSourceRuntime.runRefreshLoop(
|
||||||
if hasActiveScan {
|
on: self,
|
||||||
do {
|
refreshInterval: Self.localSourceRefreshInterval,
|
||||||
try await Task.sleep(for: .seconds(Self.localSourceRefreshInterval))
|
accessMethod: sourceAccessMethod
|
||||||
} catch {
|
)
|
||||||
return
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
await refreshLocalSources()
|
|
||||||
|
|
||||||
do {
|
|
||||||
try await Task.sleep(for: .seconds(Self.localSourceRefreshInterval))
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshLocalSources() async {
|
func refreshLocalSources() async {
|
||||||
guard !hasActiveScan else {
|
await LocalSourceRuntime.refreshSources(on: self, using: sourceAccessMethod)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let localSourceIDs = sources
|
|
||||||
.filter { $0.origin.kind == .localFolder }
|
|
||||||
.map(\.id)
|
|
||||||
|
|
||||||
for sourceID in localSourceIDs {
|
|
||||||
guard !Task.isCancelled, !isShuttingDown else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let currentSource = source(withID: sourceID) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let availability = await sourceAccessMethod.availability(for: currentSource)
|
|
||||||
let transition = updateAvailability(for: sourceID, to: availability)
|
|
||||||
|
|
||||||
guard let refreshedSource = source(withID: sourceID) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if transition.becameAvailable {
|
|
||||||
queueAutomaticSync(
|
|
||||||
for: sourceID,
|
|
||||||
reason: refreshedSource.hasCachedContent
|
|
||||||
? "Folder available. Refreshing cached library..."
|
|
||||||
: "Folder available. Scanning Minecraft library..."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
guard refreshedSource.availability == SourceAvailability.available, !refreshedSource.isScanning else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if SourceRestoration.needsReconcile(
|
|
||||||
refreshedSource,
|
|
||||||
currentCollectionSnapshots: currentCollectionSnapshots(for:)
|
|
||||||
) {
|
|
||||||
queueAutomaticSync(
|
|
||||||
for: sourceID,
|
|
||||||
reason: refreshedSource.hasCachedContent
|
|
||||||
? "Detected changes. Refreshing cached library..."
|
|
||||||
: "Scanning Minecraft library..."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshConnectedDevices() async {
|
func refreshConnectedDevices() async {
|
||||||
|
|||||||
105
World Manager for Minecraft/Services/SourceLocalRuntime.swift
Normal file
105
World Manager for Minecraft/Services/SourceLocalRuntime.swift
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
//
|
||||||
|
// SourceLocalRuntime.swift
|
||||||
|
// World Manager for Minecraft
|
||||||
|
//
|
||||||
|
// Created by OpenAI Codex on 2026-05-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
protocol LocalSourceRuntimeHosting: AnyObject {
|
||||||
|
var sources: [MinecraftSource] { get }
|
||||||
|
var isShuttingDown: Bool { get }
|
||||||
|
var hasActiveScan: Bool { get }
|
||||||
|
|
||||||
|
func source(withID sourceID: URL) -> MinecraftSource?
|
||||||
|
func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool)
|
||||||
|
func queueAutomaticSync(for sourceID: URL, reason: String, debounce: TimeInterval?)
|
||||||
|
func currentCollectionSnapshots(for sourceURL: URL) -> [CollectionSnapshot]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LocalSourceRuntime {
|
||||||
|
static func runRefreshLoop(
|
||||||
|
on host: LocalSourceRuntimeHosting,
|
||||||
|
refreshInterval: TimeInterval,
|
||||||
|
accessMethod: SourceAccessMethod
|
||||||
|
) async {
|
||||||
|
while !Task.isCancelled && !host.isShuttingDown {
|
||||||
|
if host.hasActiveScan {
|
||||||
|
do {
|
||||||
|
try await Task.sleep(for: .seconds(refreshInterval))
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshSources(on: host, using: accessMethod)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await Task.sleep(for: .seconds(refreshInterval))
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func refreshSources(
|
||||||
|
on host: LocalSourceRuntimeHosting,
|
||||||
|
using accessMethod: SourceAccessMethod
|
||||||
|
) async {
|
||||||
|
guard !host.hasActiveScan else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let localSourceIDs = host.sources
|
||||||
|
.filter { $0.origin.kind == .localFolder }
|
||||||
|
.map(\.id)
|
||||||
|
|
||||||
|
for sourceID in localSourceIDs {
|
||||||
|
guard !Task.isCancelled, !host.isShuttingDown else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let currentSource = host.source(withID: sourceID) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let availability = await accessMethod.availability(for: currentSource)
|
||||||
|
let transition = host.updateAvailability(for: sourceID, to: availability)
|
||||||
|
|
||||||
|
guard let refreshedSource = host.source(withID: sourceID) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if transition.becameAvailable {
|
||||||
|
host.queueAutomaticSync(
|
||||||
|
for: sourceID,
|
||||||
|
reason: refreshedSource.hasCachedContent
|
||||||
|
? "Folder available. Refreshing cached library..."
|
||||||
|
: "Folder available. Scanning Minecraft library...",
|
||||||
|
debounce: nil
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard refreshedSource.availability == .available, !refreshedSource.isScanning else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if SourceRestoration.needsReconcile(
|
||||||
|
refreshedSource,
|
||||||
|
currentCollectionSnapshots: host.currentCollectionSnapshots(for:)
|
||||||
|
) {
|
||||||
|
host.queueAutomaticSync(
|
||||||
|
for: sourceID,
|
||||||
|
reason: refreshedSource.hasCachedContent
|
||||||
|
? "Detected changes. Refreshing cached library..."
|
||||||
|
: "Scanning Minecraft library...",
|
||||||
|
debounce: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user