field_ios

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

TeraScopeBackend.swift (11023B)


      1 import Foundation
      2 @testable import TeraApp
      3 
      4 actor TeraScopeBackend: TeraRuntimeBackend {
      5   let visibilityStorage = VisibilityTestStorage()
      6   let restoreStorage = RestoreTestStorage()
      7   let recoveryScheduleStorage = NativeRecoveryScheduleTestStorage()
      8   var nativeRepairValues: [String: TeraNativeRecoveryStatus] = [:]
      9   enum Call: Hashable { case snapshot, schemas, drafts, save, composer, composerLoad, composerList, legacyPage, draftStatus, probe, page, reconcile, refresh, search, me, subscribe, media, invalidate, cacheCleanup }
     10   struct Pending {
     11     let pause: ResourceTestPause
     12     let failure: TeraRuntimeFailure?
     13   }
     14 
     15   private(set) var value = TeraScopeFixtures.snapshot()
     16   private let composerStorage = ComposerTestStorage()
     17   private var composerPages: [String: TeraComposerPage] = [:]
     18   private var legacyPages: [String: TeraLegacyDraftPage] = [:]
     19   private(set) var recoveryLimits: [UInt16] = []
     20   private var pending: [Call: [Pending]] = [:]
     21   private(set) var counts: [Call: Int] = [:]
     22   private var receivers: [@Sendable (TeraRuntimeChange) async -> Void] = []
     23   private(set) var tokens: [ResourceTestToken] = []
     24   private(set) var lastMeContext: TeraLocalNetwork?
     25   private var drafts: [TeraDraftStatus] = [TeraScopeFixtures.draft("old")]
     26   private var media: TeraVerifiedMediaArtifact
     27   private var pages: [String: TeraTodayPage] = [:]
     28   private var meCards = [TeraScopeFixtures.card("old")]
     29   private var revision: UInt64 = 0
     30   private var syncReceipt: TeraTodaySyncReceipt?
     31   var recoveryCompletion: (@Sendable (TeraRecoveryUploadReceipt, TeraPreparedMediaHandle) async throws -> TeraRecoveryCompletionReceipt)?
     32   private var legacyCompletionFailure: TeraRuntimeFailure?
     33 
     34   func failLegacyCompletion(with error: TeraRuntimeFailure) {
     35     legacyCompletionFailure = error
     36   }
     37 
     38   func completeAddMediaBackground(input _: TeraNativeUploadCompletion) throws -> TeraDraftStatus {
     39     throw legacyCompletionFailure ?? TeraComposerAcknowledgment.unconfirmed
     40   }
     41 
     42   func setRecoveryCompletion(_ value: @escaping @Sendable (TeraRecoveryUploadReceipt, TeraPreparedMediaHandle) async throws -> TeraRecoveryCompletionReceipt) {
     43     recoveryCompletion = value
     44   }
     45 
     46   func recoveryParent(key: String) -> TeraRecoveryEntry? {
     47     drafts.first(where: { $0.id == key }).map { .init(key: $0.id, revision: $0.revision, owner: .legacy) }
     48   }
     49 
     50   func recoverNativeUpload(_ receipt: TeraRecoveryUploadReceipt, media: TeraPreparedMediaHandle) async throws -> TeraRecoveryCompletionReceipt {
     51     guard let recoveryCompletion else { throw TeraComposerAcknowledgment.unconfirmed }
     52     return try await recoveryCompletion(receipt, media)
     53   }
     54 
     55   private var reconciliationCards: [TeraTodayCard]?
     56   private var reconciliationGeneration: UInt64 = 1
     57   private(set) var reconciliationRequests: [TeraTodayReconcileRequest] = []
     58   private(set) var pageRequests: [TeraTodayPageRequest] = []
     59 
     60   init() throws {
     61     media = try TeraScopeFixtures.artifact("a")
     62   }
     63 
     64   func pause(_ call: Call, fails: Bool = false, failure: TeraRuntimeFailure? = nil) -> ResourceTestPause {
     65     let pause = ResourceTestPause()
     66     pending[call, default: []].append(Pending(pause: pause, failure: failure ?? (fails ? TeraScopeFixtures.failure() : nil)))
     67     return pause
     68   }
     69 
     70   func configure(_ value: TeraRuntimeSnapshot) {
     71     self.value = value
     72   }
     73 
     74   func setSyncReceipt(_ receipt: TeraTodaySyncReceipt) {
     75     syncReceipt = receipt
     76   }
     77 
     78   func setDrafts(_ values: [TeraDraftStatus]) {
     79     drafts = values
     80   }
     81 
     82   func setMedia(_ value: TeraVerifiedMediaArtifact) {
     83     media = value
     84   }
     85 
     86   func setPage(_ value: TeraTodayPage, cursor: String = "first") {
     87     pages[cursor] = value
     88   }
     89 
     90   func setMeCards(_ values: [TeraTodayCard]) {
     91     meCards = values
     92   }
     93 
     94   func setReconciliation(_ cards: [TeraTodayCard], generation: UInt64) {
     95     reconciliationCards = cards
     96     reconciliationGeneration = generation
     97   }
     98 
     99   func reconcileToday(request: TeraTodayReconcileRequest) async throws -> TeraTodayPage {
    100     reconciliationRequests.append(request)
    101     let saved = pages.isEmpty ? [TeraScopeFixtures.card(request.context.relayURLs.first ?? "none")] : pages.values.flatMap(\.items)
    102     let values = reconciliationCards ?? saved
    103     var seen = Set<String>()
    104     let items = values.filter { request.cardIDs.contains($0.id) && seen.insert($0.id).inserted }
    105     let generation = reconciliationGeneration
    106     try await wait(.reconcile)
    107     if let expected = request.expectedGeneration, expected != generation {
    108       throw TeraRuntimeFailure.local(operation: "test", code: "today_cursor_invalid", safeMessage: "Changed")
    109     }
    110     return TeraTodayPage(asOfUnixSeconds: request.asOfUnixSeconds, items: items, nextCursor: nil, projectionGeneration: generation, calendar: request.calendar)
    111   }
    112 
    113   private func wait(_ call: Call) async throws {
    114     counts[call, default: 0] += 1
    115     guard var queue = pending[call], !queue.isEmpty else { return }
    116     let next = queue.removeFirst()
    117     pending[call] = queue
    118     await next.pause.wait()
    119     if let failure = next.failure {
    120       throw failure
    121     }
    122   }
    123 
    124   func snapshot() async throws -> TeraRuntimeSnapshot {
    125     let snapshot = value
    126     try await wait(.snapshot)
    127     return snapshot
    128   }
    129 
    130   func addSchemas() async throws -> [TeraAddSchema] {
    131     let schemas = TeraAddSchemaFixtures.schemas()
    132     try await wait(.schemas)
    133     return schemas
    134   }
    135 
    136   func draftHeads(limit _: UInt16) async throws -> [TeraDraftStatus] {
    137     let result = drafts
    138     try await wait(.drafts)
    139     return result
    140   }
    141 
    142   func saveAddIntent(
    143     input: TeraAddRuntimeInput, existingDraftID _: String?, expectedRevision: UInt64?
    144   ) async throws -> TeraDraftStatus {
    145     let result = TeraScopeFixtures.draft(input.form.content, revision: (expectedRevision ?? 0) + 1)
    146     try await wait(.save)
    147     drafts = [result]
    148     return result
    149   }
    150 
    151   func reserveComposerID() async -> String {
    152     await composerStorage.reserve()
    153   }
    154 
    155   func saveComposer(request: TeraComposerSaveRequest) async throws -> TeraComposerSaveReceipt {
    156     try await wait(.composer)
    157     return try await composerStorage.save(request)
    158   }
    159 
    160   func loadComposer(scope: TeraComposerScope, id: String) async throws -> TeraComposerDraft {
    161     let saved = try await composerStorage.load(scope, id: id)
    162     try await wait(.composerLoad)
    163     return saved
    164   }
    165 
    166   func setComposerPage(_ page: TeraComposerPage, cursor: String = "first") {
    167     composerPages[cursor] = page
    168   }
    169 
    170   func setLegacyPage(_ page: TeraLegacyDraftPage, cursor: String = "first") {
    171     legacyPages[cursor] = page
    172   }
    173 
    174   func listComposers(scope: TeraComposerScope, limit: UInt16, cursor: String?) async throws -> TeraComposerPage {
    175     recoveryLimits.append(limit)
    176     let result = composerPages[cursor ?? "first"] ?? TeraComposerPage(scope: scope, entries: [], nextCursor: nil)
    177     try await wait(.composerList)
    178     return result
    179   }
    180 
    181   func legacyDraftPage(limit: UInt16, cursor: String?) async throws -> TeraLegacyDraftPage {
    182     recoveryLimits.append(limit)
    183     let result = legacyPages[cursor ?? "first"] ?? TeraLegacyDraftPage(authorPublicKey: value.identity.publicKeyHex, entries: [], nextCursor: nil)
    184     try await wait(.legacyPage)
    185     return result
    186   }
    187 
    188   func draftStatus(id: String) async throws -> TeraDraftStatus {
    189     guard let result = drafts.first(where: { $0.id == id }) else { throw TeraScopeFixtures.failure() }
    190     try await wait(.draftStatus)
    191     return result
    192   }
    193 
    194   func probeBlossom() async throws -> TeraBlossomEvidence {
    195     let fingerprint = value.blossomConfiguration?.configFingerprint ?? ""
    196     try await wait(.probe)
    197     return TeraBlossomEvidence(
    198       schemaVersion: 2, origin: "http://127.0.0.1:3000", configFingerprint: fingerprint,
    199       state: "reachable", lastSuccessfulState: "probe", transportSecurity: "loopback_plaintext",
    200       observedAtUnixMilliseconds: 1, httpStatus: 200, errorCode: nil, serverErrorCode: nil,
    201       errorPhase: nil, retryable: false, possibleOrphan: false, attempts: 1
    202     )
    203   }
    204 
    205   func todayPage(request: TeraTodayPageRequest) async throws -> TeraTodayPage {
    206     pageRequests.append(request)
    207     let result = pages[request.cursor ?? "first"] ?? TeraTodayPage(asOfUnixSeconds: 1, items: [TeraScopeFixtures.card(request.context.relayURLs.first ?? "none")], nextCursor: nil, calendar: TeraScopeFixtures.viewerCalendar(asOf: 1))
    208     try await wait(.page)
    209     return result
    210   }
    211 
    212   private(set) var lastBackfillCursor: String?
    213 
    214   func refreshToday(
    215     context _: TeraLocalNetwork, nowUnixSeconds _: UInt64, update: TeraTodayProjectionUpdate,
    216     backfillCursor: String?
    217   ) async throws -> TeraTodaySyncReceipt {
    218     lastBackfillCursor = backfillCursor
    219     let result = syncReceipt ?? TeraTodaySyncFixtures.receipt(update: update)
    220     try await wait(.refresh)
    221     return result
    222   }
    223 
    224   func search(
    225     context _: TeraLocalNetwork, query: String, limit _: UInt16, asOfUnixSeconds _: UInt64
    226   ) async throws -> [TeraSearchResult] {
    227     try await wait(.search)
    228     return [TeraSearchResult(type: .card, id: query, card: TeraScopeFixtures.card(query), profile: nil)]
    229   }
    230 
    231   func me(context: TeraLocalNetwork, asOfUnixSeconds _: UInt64) async throws -> TeraMeSnapshot {
    232     lastMeContext = context
    233     let result = TeraMeSnapshot(publicKey: value.identity.publicKeyHex, profile: nil, cards: meCards)
    234     try await wait(.me)
    235     return result
    236   }
    237 
    238   func retrieveMedia(context _: TeraLocalNetwork, reference _: TeraMediaReference) async throws -> TeraVerifiedMediaArtifact {
    239     let result = media
    240     try await wait(.media)
    241     return result
    242   }
    243 
    244   func cleanupMediaCache(context _: TeraLocalNetwork) async throws -> TeraMediaCacheCleanup {
    245     try await wait(.cacheCleanup)
    246     return TeraMediaCacheCleanup(invalidatedEntries: 2, retainedCandidates: 1, remainingEntries: 3)
    247   }
    248 
    249   func invalidateMediaArtifact(context _: TeraLocalNetwork, artifactID _: String) async throws -> Bool {
    250     try await wait(.invalidate)
    251     return true
    252   }
    253 
    254   func subscribe(
    255     bufferCapacity _: Int, receive: @escaping @Sendable (TeraRuntimeChange) async -> Void
    256   ) async throws -> any TeraRuntimeSubscriptionToken {
    257     let token = ResourceTestToken()
    258     receivers.append(receive)
    259     tokens.append(token)
    260     try await wait(.subscribe)
    261     return token
    262   }
    263 
    264   func emit(
    265     _ kind: TeraRuntimeChangeKind,
    266     delivery: TeraRuntimeChangeDelivery = .change,
    267     context: TeraLocalNetwork? = nil,
    268     exhausted: Bool = false
    269   ) async {
    270     revision += 1
    271     let emittedRevision = revision
    272     for receive in receivers {
    273       await receive(TeraRuntimeChange(
    274         schemaVersion: 3,
    275         scope: TeraRuntimeChangeScope(publicKey: String(repeating: "a", count: 64), sourceGeneration: String(repeating: "a", count: 64), context: context),
    276         epoch: String(repeating: "1", count: 32),
    277         revision: TeraProjectionRevision(rawValue: exhausted ? nil : emittedRevision),
    278         delivery: delivery, kind: kind, entityID: nil
    279       ))
    280     }
    281   }
    282 
    283   func shutdown() -> TeraRuntimeShutdownReceipt {
    284     TeraRuntimeShutdownReceipt(state: "closed", alreadyClosed: false)
    285   }
    286 }