TeraRevisionDetailStore.swift (3517B)
1 import Foundation 2 3 /// One selected revision. Rust owns actions; this owner only fences callbacks. 4 @MainActor 5 final class TeraRevisionDetailStore: ObservableObject { 6 @Published private(set) var status: TeraRevisionStatus? 7 @Published private(set) var message: String? 8 @Published private(set) var isWorking = false 9 private let operations: TeraRevisionDetailOperations 10 private var author: String? 11 private var selection: String? 12 private var generation = TeraSessionGeneration.initial 13 14 init(client: TeraRuntimeClient) { 15 operations = TeraRevisionDetailOperations( 16 load: { try await client.revisionStatus(operationID: $0) }, 17 resume: { try await client.advanceRevision(operationID: $0) }, 18 cancel: { try await client.cancelRevision(operationID: $0) } 19 ) 20 } 21 22 init(operations: TeraRevisionDetailOperations) { 23 self.operations = operations 24 } 25 26 func configure(author: String) { 27 guard self.author != author else { return } 28 stop() 29 self.author = author 30 } 31 32 func stop() { 33 generation = generation.invalidated() 34 selection = nil 35 status = nil 36 message = nil 37 isWorking = false 38 } 39 40 func load(_ id: String) async { 41 generation = generation.invalidated() 42 selection = id 43 status = nil 44 isWorking = false 45 await perform(.refresh) 46 } 47 48 func refresh() async { 49 await perform(.refresh) 50 } 51 52 func resume() async { 53 await perform(.resume) 54 } 55 56 func cancel() async { 57 await perform(.cancel) 58 } 59 60 private enum Action { case refresh, resume, cancel } 61 62 private func perform(_ action: Action) async { 63 guard let id = selection, let author, generation.isActive, !isWorking, !Task.isCancelled else { return } 64 let requested = generation 65 isWorking = true 66 message = nil 67 defer { 68 if requested == generation { 69 isWorking = false 70 } 71 } 72 do { 73 var current = try await operations.load(id) 74 try validate(current, id: id, author: author, generation: requested) 75 switch action { 76 case .refresh: break 77 case .resume: 78 if current.canResume { 79 current = try await operations.resume(id) 80 } 81 case .cancel: 82 if current.canCancel { 83 current = try await operations.cancel(id) 84 } 85 } 86 try validate(current, id: id, author: author, generation: requested) 87 status = current 88 } catch { 89 guard requested == generation, selection == id, self.author == author, !Task.isCancelled else { return } 90 status = nil 91 message = "Current revision details are unavailable. Saved work is preserved. Refresh to try again." 92 } 93 } 94 95 private func validate(_ value: TeraRevisionStatus, id: String, author: String, 96 generation requested: TeraSessionGeneration) throws 97 { 98 guard requested == generation, generation.isActive, selection == id, self.author == author, !Task.isCancelled, 99 value.operationID == id, value.replacement.id == id, value.replacement.isRevision, 100 value.replacement.authorPublicKey == author, value.original?.authorPublicKey == author, 101 value.retraction.map({ $0.authorPublicKey == author && $0.revisionParentID.map { $0 == id } != false }) != false 102 else { throw CancellationError() } 103 } 104 } 105 106 struct TeraRevisionDetailOperations: Sendable { 107 let load: @Sendable (String) async throws -> TeraRevisionStatus 108 let resume: @Sendable (String) async throws -> TeraRevisionStatus 109 let cancel: @Sendable (String) async throws -> TeraRevisionStatus 110 }