field_ios

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

TeraComposerSaveState.swift (3106B)


      1 import Foundation
      2 
      3 enum TeraAddLoadState: Sendable, Equatable {
      4   case idle
      5   case loading
      6   case ready
      7   case failed(String)
      8 }
      9 
     10 enum TeraComposerSaveState: Equatable {
     11   case idle
     12   case unsaved
     13   case saving
     14   case saved
     15   case failed
     16   case revision
     17 
     18   var label: String {
     19     switch self {
     20     case .idle: "Edits save on this device."
     21     case .unsaved: "Changes have not been saved."
     22     case .saving: "Saving changes on this device…"
     23     case .saved: "Changes saved on this device."
     24     case .failed: "Changes are not confirmed saved. Use Save draft to retry."
     25     case .revision: "Use Save draft to save this revision before leaving."
     26     }
     27   }
     28 }
     29 
     30 struct TeraComposerPersistence: Sendable {
     31   var confirm: @Sendable (TeraComposerDraft) async throws -> Void = { _ in }
     32   var reserve: @Sendable () async throws -> String
     33   var save: @Sendable (TeraComposerSaveRequest) async throws -> TeraComposerSaveReceipt
     34   var load: @Sendable (TeraComposerScope, String) async throws -> TeraComposerDraft
     35 
     36   init(client: TeraRuntimeClient) {
     37     reserve = { try await client.reserveComposerID() }
     38     save = { try await client.saveComposer(request: $0) }
     39     load = { try await client.loadComposer(scope: $0, id: $1) }
     40   }
     41 
     42   init(
     43     reserve: @escaping @Sendable () async throws -> String,
     44     save: @escaping @Sendable (TeraComposerSaveRequest) async throws -> TeraComposerSaveReceipt,
     45     load: @escaping @Sendable (TeraComposerScope, String) async throws -> TeraComposerDraft
     46   ) {
     47     self.reserve = reserve
     48     self.save = save
     49     self.load = load
     50   }
     51 
     52   func protectingMedia(_ media: (any TeraAddMediaHandling)?) -> Self {
     53     var guarded = Self(reserve: reserve, save: { request in
     54       try await Self.confirm(request.form.media, using: media)
     55       let receipt = try await save(request)
     56       try await Self.confirm(receipt.draft.form.media, using: media)
     57       return receipt
     58     }, load: { scope, id in
     59       let draft = try await load(scope, id)
     60       try await Self.confirm(draft.form.media, using: media)
     61       return draft
     62     })
     63     guarded.confirm = { try await Self.confirm($0.form.media, using: media) }
     64     return guarded
     65   }
     66 
     67   private static func confirm(_ references: [TeraComposerMedia], using media: (any TeraAddMediaHandling)?) async throws {
     68     guard !references.isEmpty else { return }
     69     guard let media else { throw TeraComposerAcknowledgment.unconfirmed }
     70     try await media.confirmDurableComposerMedia(references)
     71   }
     72 }
     73 
     74 enum TeraComposerAcknowledgment {
     75   static func matches(_ draft: TeraComposerDraft, request: TeraComposerSaveRequest) -> Bool {
     76     let (revision, overflow) = (request.expectedRevision ?? 0).addingReportingOverflow(1)
     77     return !overflow && draft.scope == request.scope && draft.id == request.id
     78       && draft.revision == revision && draft.editSequence == request.editSequence && draft.form == request.form
     79   }
     80 
     81   static var unconfirmed: TeraRuntimeFailure {
     82     .local(operation: "add.composer.save", code: "ios.composer.save_unconfirmed",
     83            safeMessage: "These changes are not confirmed saved. Keep this draft open and retry Save.")
     84   }
     85 }