field_ios

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

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 }