Extract source persistence coordination
This commit is contained in:
parent
0acf9faa16
commit
1347bb15ae
@ -10,7 +10,7 @@ import Foundation
|
||||
import OSLog
|
||||
|
||||
@MainActor
|
||||
final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePersistenceHosting {
|
||||
private static let enrichmentWorkerCount = 4
|
||||
private static let sizeWorkerCount = 2
|
||||
private static let minimumVisibleScanDuration: TimeInterval = 0.8
|
||||
@ -27,7 +27,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
|
||||
@Published var sources: [MinecraftSource] = []
|
||||
@Published private(set) var connectedDevices: [ConnectedDeviceSidebarEntry] = []
|
||||
@Published private(set) var isRestoringPersistedSources = true
|
||||
@Published var isRestoringPersistedSources = true
|
||||
|
||||
private var scanTasks: [URL: Task<Void, Never>] = [:]
|
||||
private var automaticSyncTasks: [URL: Task<Void, Never>] = [:]
|
||||
@ -54,7 +54,10 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
self.notificationService = notificationService ?? ScanNotificationService.shared
|
||||
|
||||
Task { [weak self] in
|
||||
await self?.restorePersistedSources()
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
await SourcePersistenceCoordinator.restoreSources(on: self, using: self.persistenceStore)
|
||||
}
|
||||
|
||||
localSourceRefreshTask = Task { [weak self] in
|
||||
@ -110,7 +113,10 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
return
|
||||
}
|
||||
|
||||
await persistVisibleSourcesForShutdown()
|
||||
await SourcePersistenceCoordinator.persistVisibleSourcesForShutdown(
|
||||
from: visibleSources,
|
||||
using: persistenceStore
|
||||
)
|
||||
shutdown()
|
||||
try? await Task.sleep(for: .seconds(timeout))
|
||||
}
|
||||
@ -194,7 +200,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
scanTasks[sourceID]?.cancel()
|
||||
scanTasks[sourceID] = nil
|
||||
sources.removeAll { $0.id == sourceID }
|
||||
deletePersistedSource(withID: sourceID)
|
||||
SourcePersistenceCoordinator.deletePersistedSource(withID: sourceID, using: persistenceStore)
|
||||
if let removedSource {
|
||||
purgeCachedArtifacts(for: removedSource)
|
||||
}
|
||||
@ -249,7 +255,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
)
|
||||
}
|
||||
|
||||
private func rebuildNormalizedIndex(for sourceID: URL) {
|
||||
func rebuildNormalizedIndex(for sourceID: URL) {
|
||||
updateSource(sourceID) { source in
|
||||
let rawItems = source.rawItems.sorted(by: WorldScanner.sortItems)
|
||||
source.rawItems = rawItems
|
||||
@ -504,7 +510,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshLocalSources() async {
|
||||
func refreshLocalSources() async {
|
||||
guard !hasActiveScan else {
|
||||
return
|
||||
}
|
||||
@ -557,7 +563,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshConnectedDevices() async {
|
||||
func refreshConnectedDevices() async {
|
||||
guard !isShuttingDown else {
|
||||
return
|
||||
}
|
||||
@ -786,41 +792,23 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
}
|
||||
}
|
||||
|
||||
private func restorePersistedSources() async {
|
||||
defer {
|
||||
isRestoringPersistedSources = false
|
||||
func currentCollectionSnapshots(for sourceURL: URL) -> [CollectionSnapshot] {
|
||||
WorldScanner.collectionSnapshots(in: sourceURL)
|
||||
}
|
||||
|
||||
let records: [PersistedSourceRecord]
|
||||
do {
|
||||
records = try await persistenceStore.loadSources()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
for record in records {
|
||||
let source = SourceRestoration.restoredSource(from: record) { [connectedDeviceSourceFactory] device, container in
|
||||
func connectedDeviceDisplayName(for device: ConnectedDevice, container: DeviceAppContainer) -> String {
|
||||
connectedDeviceSourceFactory.displayName(for: device, container: container)
|
||||
}
|
||||
|
||||
func appendRestoredSource(_ source: MinecraftSource) {
|
||||
sources.append(source)
|
||||
rebuildNormalizedIndex(for: source.id)
|
||||
}
|
||||
|
||||
for record in records where record.needsRepair {
|
||||
Task.detached(priority: .utility) { [persistenceStore] in
|
||||
try? await persistenceStore.repair(record: record)
|
||||
}
|
||||
}
|
||||
|
||||
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
|
||||
await Task.yield()
|
||||
|
||||
for record in records {
|
||||
let restoredItems = await SourceRestoration.restoreCachedImages(in: record.rawItems)
|
||||
func applyRestoredItems(_ items: [MinecraftContentItem], from record: PersistedSourceRecord) {
|
||||
updateSource(record.sourceID) { source in
|
||||
SourceRestoration.applyRestoredItemState(
|
||||
restoredItems,
|
||||
items,
|
||||
lastScanDate: record.lastScanDate,
|
||||
snapshot: record.snapshot,
|
||||
to: &source
|
||||
@ -829,27 +817,8 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
rebuildNormalizedIndex(for: record.sourceID)
|
||||
}
|
||||
|
||||
await refreshConnectedDevices()
|
||||
await refreshLocalSources()
|
||||
scheduleRestoredSourceRefreshes(records: records)
|
||||
}
|
||||
|
||||
private func scheduleRestoredSourceRefreshes(records: [PersistedSourceRecord]) {
|
||||
let persistedRecordsByID = Dictionary(uniqueKeysWithValues: records.map { ($0.sourceID, $0) })
|
||||
|
||||
for source in sources {
|
||||
if let refreshReason = SourceRestoration.startupRefreshReason(
|
||||
for: source,
|
||||
persistedRecord: persistedRecordsByID[source.id],
|
||||
currentCollectionSnapshots: currentCollectionSnapshots(for:)
|
||||
) {
|
||||
queueAutomaticSync(for: source.id, reason: refreshReason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func currentCollectionSnapshots(for sourceURL: URL) -> [CollectionSnapshot] {
|
||||
WorldScanner.collectionSnapshots(in: sourceURL)
|
||||
func sortSourcesByDisplayName() {
|
||||
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
|
||||
}
|
||||
|
||||
private func buildDisplayItems(
|
||||
@ -891,30 +860,14 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting {
|
||||
}
|
||||
|
||||
func persistSourceIfAvailable(withID sourceID: URL) {
|
||||
guard let source = source(withID: sourceID) else {
|
||||
return
|
||||
SourcePersistenceCoordinator.persistSourceIfAvailable(
|
||||
withID: sourceID,
|
||||
on: self,
|
||||
using: persistenceStore
|
||||
)
|
||||
}
|
||||
|
||||
let persistedSource = source
|
||||
Task {
|
||||
try? await persistenceStore.save(source: persistedSource)
|
||||
}
|
||||
}
|
||||
|
||||
private func persistVisibleSourcesForShutdown() async {
|
||||
let persistedSources = sources
|
||||
for source in persistedSources {
|
||||
try? await persistenceStore.save(source: source)
|
||||
}
|
||||
}
|
||||
|
||||
private func deletePersistedSource(withID sourceID: URL) {
|
||||
Task {
|
||||
try? await persistenceStore.deleteSource(withID: sourceID)
|
||||
}
|
||||
}
|
||||
|
||||
private func queueAutomaticSync(for sourceID: URL, reason: String, debounce: TimeInterval? = nil) {
|
||||
func queueAutomaticSync(for sourceID: URL, reason: String, debounce: TimeInterval? = nil) {
|
||||
guard !isShuttingDown else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
//
|
||||
// SourcePersistenceCoordinator.swift
|
||||
// World Manager for Minecraft
|
||||
//
|
||||
// Created by OpenAI Codex on 2026-05-29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol SourcePersistenceHosting: AnyObject {
|
||||
var visibleSources: [MinecraftSource] { get }
|
||||
var isRestoringPersistedSources: Bool { get set }
|
||||
|
||||
func source(withID sourceID: URL) -> MinecraftSource?
|
||||
func appendRestoredSource(_ source: MinecraftSource)
|
||||
func applyRestoredItems(_ items: [MinecraftContentItem], from record: PersistedSourceRecord)
|
||||
func sortSourcesByDisplayName()
|
||||
func refreshConnectedDevices() async
|
||||
func refreshLocalSources() async
|
||||
func queueAutomaticSync(for sourceID: URL, reason: String, debounce: TimeInterval?)
|
||||
func currentCollectionSnapshots(for sourceURL: URL) -> [CollectionSnapshot]
|
||||
func connectedDeviceDisplayName(for device: ConnectedDevice, container: DeviceAppContainer) -> String
|
||||
}
|
||||
|
||||
enum SourcePersistenceCoordinator {
|
||||
static func restoreSources(
|
||||
on host: SourcePersistenceHosting,
|
||||
using persistenceStore: SourcePersistenceStore
|
||||
) async {
|
||||
defer {
|
||||
host.isRestoringPersistedSources = false
|
||||
}
|
||||
|
||||
let records: [PersistedSourceRecord]
|
||||
do {
|
||||
records = try await persistenceStore.loadSources()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
for record in records {
|
||||
let source = SourceRestoration.restoredSource(from: record) { device, container in
|
||||
host.connectedDeviceDisplayName(for: device, container: container)
|
||||
}
|
||||
|
||||
host.appendRestoredSource(source)
|
||||
}
|
||||
|
||||
for record in records where record.needsRepair {
|
||||
Task.detached(priority: .utility) {
|
||||
try? await persistenceStore.repair(record: record)
|
||||
}
|
||||
}
|
||||
|
||||
host.sortSourcesByDisplayName()
|
||||
await Task.yield()
|
||||
|
||||
for record in records {
|
||||
let restoredItems = await SourceRestoration.restoreCachedImages(in: record.rawItems)
|
||||
host.applyRestoredItems(restoredItems, from: record)
|
||||
}
|
||||
|
||||
await host.refreshConnectedDevices()
|
||||
await host.refreshLocalSources()
|
||||
scheduleRestoredSourceRefreshes(records: records, on: host)
|
||||
}
|
||||
|
||||
static func persistSourceIfAvailable(
|
||||
withID sourceID: URL,
|
||||
on host: SourcePersistenceHosting,
|
||||
using persistenceStore: SourcePersistenceStore
|
||||
) {
|
||||
guard let source = host.source(withID: sourceID) else {
|
||||
return
|
||||
}
|
||||
|
||||
let persistedSource = source
|
||||
Task {
|
||||
try? await persistenceStore.save(source: persistedSource)
|
||||
}
|
||||
}
|
||||
|
||||
static func persistVisibleSourcesForShutdown(
|
||||
from sources: [MinecraftSource],
|
||||
using persistenceStore: SourcePersistenceStore
|
||||
) async {
|
||||
for source in sources {
|
||||
try? await persistenceStore.save(source: source)
|
||||
}
|
||||
}
|
||||
|
||||
static func deletePersistedSource(
|
||||
withID sourceID: URL,
|
||||
using persistenceStore: SourcePersistenceStore
|
||||
) {
|
||||
Task {
|
||||
try? await persistenceStore.deleteSource(withID: sourceID)
|
||||
}
|
||||
}
|
||||
|
||||
private static func scheduleRestoredSourceRefreshes(
|
||||
records: [PersistedSourceRecord],
|
||||
on host: SourcePersistenceHosting
|
||||
) {
|
||||
let persistedRecordsByID = Dictionary(uniqueKeysWithValues: records.map { ($0.sourceID, $0) })
|
||||
|
||||
for source in host.visibleSources {
|
||||
if let refreshReason = SourceRestoration.startupRefreshReason(
|
||||
for: source,
|
||||
persistedRecord: persistedRecordsByID[source.id],
|
||||
currentCollectionSnapshots: host.currentCollectionSnapshots(for:)
|
||||
) {
|
||||
host.queueAutomaticSync(for: source.id, reason: refreshReason, debounce: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user