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 }