TeraMediaStore.swift (9034B)
1 import Foundation 2 import UIKit 3 4 enum TeraMediaPresentationState: Equatable { 5 case pending 6 case loading 7 case ready(TeraVerifiedMediaArtifact) 8 case unavailable 9 case networkUnavailable 10 case corrupt 11 case failed 12 13 var accessibilityLabel: String { 14 switch self { 15 case .pending: "Photo verification is pending" 16 case .loading: "Loading verified photo" 17 case .ready: "Verified photo" 18 case .unavailable: "Photo is not available locally" 19 case .networkUnavailable: "The photo service could not be reached" 20 case .corrupt: "The saved photo failed verification" 21 case .failed: "Photo could not be loaded" 22 } 23 } 24 } 25 26 @MainActor 27 final class TeraMediaStore: ObservableObject { 28 struct Request: Hashable { 29 let referenceID: String 30 let context: TeraLocalNetwork? 31 } 32 33 private struct Key: Hashable { 34 let account: String? 35 let context: TeraLocalNetwork 36 let reference: String 37 } 38 39 private struct CachedImage { 40 let image: UIImage 41 let cost: Int 42 } 43 44 @Published private var states: [Key: TeraMediaPresentationState] = [:] 45 private let runtimeClient: TeraRuntimeClient 46 private let limits: TeraMediaPresentationLimits 47 private let work: TeraMediaWorkQueue 48 private var owners: [Key: UUID] = [:] 49 private var images: [Key: CachedImage] = [:] 50 private var recent: [Key] = [] 51 private var revoked = Set<Key>() 52 private var visibilityOverflow = false 53 private var reauthorized = Set<Key>() 54 private var configuration: TeraPresentationConfiguration? 55 56 init(runtimeClient: TeraRuntimeClient, limits: TeraMediaPresentationLimits = .standard) { 57 self.runtimeClient = runtimeClient 58 self.limits = limits 59 work = TeraMediaWorkQueue(limits: limits) 60 } 61 62 var cachedByteCount: Int { 63 images.values.reduce(0) { $0 + $1.cost } 64 } 65 66 var stateCount: Int { 67 states.count 68 } 69 70 func image(for media: TeraMediaReference, context: TeraLocalNetwork?) -> UIImage? { 71 guard let context else { return nil } 72 let key = key(media: media, context: context) 73 guard allowed(key) else { return nil } 74 touch(key) 75 return images[key]?.image 76 } 77 78 func state(for media: TeraMediaReference, context: TeraLocalNetwork?) -> TeraMediaPresentationState { 79 guard let context else { return .unavailable } 80 let key = key(media: media, context: context) 81 guard allowed(key) else { return .unavailable } 82 if let state = states[key] { 83 return state 84 } 85 switch media.verification { 86 case .pending: return .pending 87 case .failed: return .failed 88 case .verified, .unavailable: return .unavailable 89 } 90 } 91 92 func load(media: TeraMediaReference, context: TeraLocalNetwork?) { 93 guard let context else { return } 94 let key = key(media: media, context: context) 95 guard allowed(key), states[key] == nil, owners[key] == nil else { return } 96 switch media.verification { 97 case .pending: set(.pending, for: key) 98 case .failed: set(.failed, for: key) 99 case .verified: 100 guard let artifactID = media.verifiedArtifactID else { set(.corrupt, for: key); return } 101 start(key: key, context: context) { [runtimeClient] in 102 try await runtimeClient.verifiedMediaArtifact(context: context, artifactID: artifactID) 103 } 104 case .unavailable: 105 start(key: key, context: context) { [runtimeClient] in 106 try await runtimeClient.retrieveMedia(context: context, reference: media) 107 } 108 } 109 } 110 111 func retry(media: TeraMediaReference, context: TeraLocalNetwork?) { 112 guard let context else { return } 113 let key = key(media: media, context: context) 114 guard allowed(key) else { return } 115 cancel(key) 116 start(key: key, context: context) { [runtimeClient] in 117 try await runtimeClient.retrieveMedia(context: context, reference: media) 118 } 119 } 120 121 func reconcileVisibility(previous: [TeraMediaReference], current: [TeraMediaReference], 122 context: TeraLocalNetwork?) 123 { 124 guard let context else { return } 125 let permitted = Set(current.prefix(limits.visibilityEntries).map { key(media: $0, context: context) }) 126 if current.count > limits.visibilityEntries { 127 visibilityOverflow = true 128 } 129 reauthorized = permitted 130 for key in permitted where revoked.remove(key) != nil { 131 remove(key) 132 } 133 for reference in previous { 134 let key = key(media: reference, context: context) 135 guard !permitted.contains(key) else { continue } 136 if !revoked.contains(key) { 137 if revoked.count < limits.visibilityEntries { 138 revoked.insert(key) 139 } else { 140 visibilityOverflow = true 141 } 142 } 143 cancel(key) 144 } 145 // Under admission pressure only the bounded, explicitly current window may 146 // load. Forgetting an old denial must never authorize a stale reference. 147 if visibilityOverflow { 148 for key in Array(owners.keys) where !allowed(key) { 149 cancel(key) 150 } 151 for key in Array(states.keys) where !allowed(key) { 152 remove(key) 153 } 154 } 155 } 156 157 func reset() { 158 work.cancelAll() 159 owners.removeAll() 160 states.removeAll() 161 images.removeAll() 162 recent.removeAll() 163 revoked.removeAll() 164 reauthorized.removeAll() 165 visibilityOverflow = false 166 } 167 168 func configure(snapshot: TeraRuntimeSnapshot) { 169 let updated = TeraPresentationConfiguration(snapshot: snapshot) 170 guard configuration != updated else { return } 171 reset() 172 configuration = updated 173 } 174 175 private func start(key: Key, context: TeraLocalNetwork, 176 operation: @escaping @Sendable () async throws -> TeraVerifiedMediaArtifact?) 177 { 178 let id = UUID() 179 owners[key] = id 180 set(.loading, for: key) 181 let accepted = work.submit(id: id) { [weak self, runtimeClient, limits] in 182 do { 183 try Task.checkCancellation() 184 guard let artifact = try await operation() else { 185 self?.complete(key: key, id: id, state: .unavailable) 186 return 187 } 188 guard self?.isCurrent(key, id) == true else { return } 189 do { 190 let image = try await TeraMediaThumbnail.prepare(artifact, limits: limits) 191 guard let self, isCurrent(key, id) else { return } 192 let cost = artifact.bytes.count + image.bytesPerRow * image.height 193 guard cost <= limits.cacheBytes else { 194 complete(key: key, id: id, state: .unavailable) 195 return 196 } 197 images[key] = CachedImage(image: UIImage(cgImage: image), cost: cost) 198 complete(key: key, id: id, state: .ready(artifact)) 199 } catch TeraMediaThumbnailFailure.corrupt { 200 guard self?.isCurrent(key, id) == true else { return } 201 _ = try? await runtimeClient.invalidateMediaArtifact(context: context, artifactID: artifact.artifactID) 202 self?.complete(key: key, id: id, state: .corrupt) 203 } catch TeraMediaThumbnailFailure.resourceLimit { 204 self?.complete(key: key, id: id, state: .unavailable) 205 } 206 } catch is CancellationError { 207 self?.complete(key: key, id: id, state: nil) 208 } catch { 209 self?.complete(key: key, id: id, state: Self.failureState(error)) 210 } 211 } 212 if !accepted { 213 owners[key] = nil; set(.unavailable, for: key) 214 } 215 } 216 217 private func isCurrent(_ key: Key, _ id: UUID) -> Bool { 218 owners[key] == id && allowed(key) && !Task.isCancelled 219 } 220 221 private func complete(key: Key, id: UUID, state: TeraMediaPresentationState?) { 222 guard owners[key] == id else { return } 223 owners[key] = nil 224 if Task.isCancelled || !allowed(key) { 225 remove(key) 226 } else { 227 set(state, for: key) 228 } 229 } 230 231 private func allowed(_ key: Key) -> Bool { 232 visibilityOverflow ? reauthorized.contains(key) : !revoked.contains(key) 233 } 234 235 private func cancel(_ key: Key) { 236 if let id = owners.removeValue(forKey: key) { 237 work.cancel(id: id) 238 } 239 remove(key) 240 } 241 242 private func remove(_ key: Key) { 243 states[key] = nil 244 images[key] = nil 245 recent.removeAll { $0 == key } 246 } 247 248 private func touch(_ key: Key) { 249 guard states[key] != nil else { return } 250 recent.removeAll { $0 == key } 251 recent.append(key) 252 } 253 254 private func set(_ state: TeraMediaPresentationState?, for key: Key) { 255 guard let state else { remove(key); return } 256 states[key] = state 257 touch(key) 258 while states.count > limits.cacheEntries || cachedByteCount > limits.cacheBytes { 259 guard let oldest = recent.first(where: { owners[$0] == nil }) else { break } 260 remove(oldest) 261 } 262 } 263 264 private func key(media: TeraMediaReference, context: TeraLocalNetwork) -> Key { 265 Key(account: configuration?.publicKey, context: context, reference: media.referenceFingerprint) 266 } 267 268 static func failureState(_ error: Error) -> TeraMediaPresentationState { 269 guard let failure = TeraRuntimeFailure.from(error) else { return .failed } 270 if failure.recovery.disposition == .mediaCorrupt { 271 return .corrupt 272 } 273 if failure.recovery.disposition == .networkUnavailable { 274 return .networkUnavailable 275 } 276 return .failed 277 } 278 }