field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }