field_ios

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

TeraEditingProtection.swift (3208B)


      1 import Foundation
      2 
      3 /// One replacement and one worker. These transient tokens are never persisted
      4 /// as composer identity, revision, or publication authority.
      5 @MainActor
      6 final class TeraEditingProtection: ObservableObject {
      7   enum Kind { case editing, reopen }
      8   @Published private(set) var failed = false
      9   @Published private(set) var isWorking = false
     10   @Published private(set) var reopened: UUID?
     11   var cancelled: () -> Void = {}
     12   private struct Pending {
     13     let token: UUID
     14     let kind: Kind
     15     let save: () async -> Bool
     16     let apply: () async -> Bool
     17   }
     18 
     19   private var pending: Pending?
     20   private var worker: Task<Bool, Never>?
     21   private var activeToken: UUID?
     22 
     23   var choiceToken: UUID? {
     24     pending?.token
     25   }
     26 
     27   deinit { worker?.cancel() }
     28 
     29   func replace(kind: Kind, save: @escaping () async -> Bool, apply: @escaping () async -> Bool) async -> Bool {
     30     guard let token = schedule(kind: kind, save: save, apply: apply), let worker else { return false }
     31     return await withTaskCancellationHandler { await worker.value } onCancel: {
     32       Task { @MainActor [weak self] in self?.cancel(token: token) }
     33     }
     34   }
     35 
     36   @discardableResult
     37   func schedule(kind: Kind, save: @escaping () async -> Bool, apply: @escaping () async -> Bool) -> UUID? {
     38     guard pending == nil, worker == nil, !Task.isCancelled else { return nil }
     39     let token = UUID()
     40     pending = Pending(token: token, kind: kind, save: save, apply: apply)
     41     start(save: true)
     42     return token
     43   }
     44 
     45   func retry() {
     46     start(save: true)
     47   }
     48 
     49   func discard() {
     50     start(save: false)
     51   }
     52 
     53   func discard(token: UUID) {
     54     guard pending?.token == token else { return }
     55     discard()
     56   }
     57 
     58   func cancel() {
     59     let hadRequest = pending != nil || activeToken != nil
     60     pending = nil
     61     activeToken = nil
     62     failed = false
     63     isWorking = false
     64     worker?.cancel()
     65     if hadRequest {
     66       cancelled()
     67     }
     68   }
     69 
     70   func editingChanged() {
     71     // Once the replacement is admitted, the Add owner's operation generation
     72     // fences its callbacks. Its own form assignment is not a new dirty edit.
     73     if pending != nil {
     74       cancel()
     75     }
     76   }
     77 
     78   func cancel(token: UUID) {
     79     if pending?.token == token || activeToken == token {
     80       cancel()
     81     }
     82   }
     83 
     84   private func start(save: Bool) {
     85     guard worker == nil, let pending, !Task.isCancelled else { return }
     86     failed = false
     87     isWorking = true
     88     activeToken = pending.token
     89     worker = Task { [weak self] in
     90       guard let self else { return false }
     91       return await run(pending, save: save)
     92     }
     93   }
     94 
     95   private func run(_ request: Pending, save: Bool) async -> Bool {
     96     defer {
     97       worker = nil
     98       isWorking = false
     99       activeToken = nil
    100     }
    101     let saved = if save {
    102       await request.save()
    103     } else {
    104       true
    105     }
    106     guard !Task.isCancelled, pending?.token == request.token, activeToken == request.token else { return false }
    107     guard saved else {
    108       failed = true
    109       return false
    110     }
    111     pending = nil
    112     let applied = await request.apply()
    113     guard !Task.isCancelled, activeToken == request.token, applied else { return false }
    114     if request.kind == .reopen {
    115       reopened = request.token
    116     }
    117     return true
    118   }
    119 }