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 }