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 }