TeraPublicationTargets.swift (4469B)
1 import Foundation 2 import TeraKitBindings 3 4 enum TeraPublicationTargetPolicy: Sendable, Equatable { 5 case any, all, quorum(UInt16), required([String]) 6 } 7 8 struct TeraPublicationTarget: Sendable, Equatable, Identifiable { 9 let id: String 10 let endpoint: String 11 let attempted: Bool 12 let accepted: Bool 13 let delivered: Bool 14 let rejected: Bool 15 let uncertain: Bool 16 let readBackObservedAtUnixMilliseconds: UInt64? 17 18 var summary: String { 19 if delivered { 20 return "Delivery confirmed by this relay." 21 } 22 if accepted { 23 return "Accepted by this relay." 24 } 25 if uncertain { 26 return "Delivery outcome is uncertain." 27 } 28 if rejected { 29 return "A refusal from this relay is recorded." 30 } 31 return attempted ? "Attempt recorded; acceptance is unconfirmed." : "No attempt is recorded for this relay." 32 } 33 } 34 35 struct TeraPublicationTargets: Sendable, Equatable { 36 let requiresDelivery: Bool 37 let policy: TeraPublicationTargetPolicy 38 let targets: [TeraPublicationTarget] 39 let readBackAvailable: Bool 40 let readBackComplete: Bool 41 42 var policySummary: String { 43 let level = requiresDelivery ? "delivery confirmation" : "acceptance" 44 switch policy { 45 case .any: return "Saved policy: \(level) from any one saved relay." 46 case .all: return "Saved policy: \(level) from every saved relay." 47 case let .quorum(count): return "Saved policy: \(level) from \(count) saved relays." 48 case let .required(ids): return "Saved policy: \(level) from \(ids.count) specifically required relays." 49 } 50 } 51 52 func isRequired(_ id: String) -> Bool { 53 if case let .required(ids) = policy { 54 return ids.contains(id) 55 } 56 return policy == .all 57 } 58 59 func follows(_ previous: Self) -> Bool { 60 guard policy == previous.policy, requiresDelivery == previous.requiresDelivery, 61 targets.map(\.id) == previous.targets.map(\.id) else { return false } 62 return zip(targets, previous.targets).allSatisfy { current, old in 63 current.endpoint == old.endpoint && (!old.attempted || current.attempted) 64 && (!old.accepted || current.accepted) && (!old.delivered || current.delivered) 65 && (!old.rejected || current.rejected) 66 } 67 } 68 69 static func decode(_ value: FfiPublicationTargetDetails) throws -> Self { 70 guard (1 ... 64).contains(value.targets.count), value.readBackAvailable || !value.readBackComplete 71 else { throw TeraGeneratedSubmission.mismatch() } 72 let targets = try value.targets.map { wire in 73 guard fingerprint(wire.fingerprint), !wire.endpoint.isEmpty, wire.endpoint.utf8.count <= 2048, 74 !wire.endpoint.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains), 75 !wire.accepted || wire.attempted, !wire.delivered || wire.accepted, 76 !wire.rejected || wire.attempted, 77 wire.readBackObservedAtUnixMs.map({ $0 > 0 && $0 <= UInt64(Int64.max) }) != false, 78 value.readBackAvailable || wire.readBackObservedAtUnixMs == nil 79 else { throw TeraGeneratedSubmission.mismatch() } 80 return TeraPublicationTarget(id: wire.fingerprint, endpoint: wire.endpoint, attempted: wire.attempted, 81 accepted: wire.accepted, delivered: wire.delivered, rejected: wire.rejected, 82 uncertain: wire.uncertain, readBackObservedAtUnixMilliseconds: wire.readBackObservedAtUnixMs) 83 } 84 let ids = Set(targets.map(\.id)) 85 guard ids.count == targets.count else { throw TeraGeneratedSubmission.mismatch() } 86 let policy: TeraPublicationTargetPolicy 87 switch value.policy { 88 case .any: policy = .any 89 case .all: policy = .all 90 case let .quorum(threshold): 91 guard threshold > 0, threshold <= targets.count else { throw TeraGeneratedSubmission.mismatch() } 92 policy = .quorum(threshold) 93 case let .required(required): 94 guard !required.isEmpty, required.count <= targets.count, Set(required).count == required.count, 95 required.allSatisfy(ids.contains) else { throw TeraGeneratedSubmission.mismatch() } 96 policy = .required(required) 97 } 98 return Self(requiresDelivery: value.requiresDelivery, policy: policy, targets: targets, 99 readBackAvailable: value.readBackAvailable, readBackComplete: value.readBackComplete) 100 } 101 102 private static func fingerprint(_ value: String) -> Bool { 103 value.utf8.count == 64 && value.utf8.allSatisfy { (48 ... 57).contains($0) || (97 ... 102).contains($0) } 104 } 105 }