TeraSupportingBackend.swift (8975B)
1 import Foundation 2 @testable import TeraApp 3 4 actor SupportingBackend: TeraRuntimeBackend { 5 struct SearchRequest: Sendable { 6 let contextID: String 7 let query: String 8 } 9 10 private let searchDelayNanoseconds: UInt64 11 private var request: SearchRequest? 12 private var closed = false 13 private var settings = SupportingBackend.initialSettings() 14 private var savedProfile: TeraProfileMetadataInput? 15 16 init(searchDelayNanoseconds: UInt64 = 0) { 17 self.searchDelayNanoseconds = searchDelayNanoseconds 18 } 19 20 func snapshotValue() -> TeraRuntimeSnapshot { 21 TeraRuntimeSnapshot( 22 identity: TeraRuntimeIdentity( 23 publicKeyHex: String(repeating: "a", count: 64), 24 hostSignerConfigured: true 25 ), 26 relay: nil, 27 blossomConfiguration: nil, 28 blossomEvidence: nil, 29 crateName: "tera_ffi", 30 crateVersion: "0.1.0-alpha", 31 isClosed: closed 32 ) 33 } 34 35 func snapshot() -> TeraRuntimeSnapshot { 36 snapshotValue() 37 } 38 39 func todayPage(request _: TeraTodayPageRequest) throws -> TeraTodayPage { 40 throw unsupported() 41 } 42 43 func refreshToday( 44 context _: TeraLocalNetwork, 45 nowUnixSeconds _: UInt64, 46 update _: TeraTodayProjectionUpdate, backfillCursor _: String? 47 ) throws -> TeraTodaySyncReceipt { 48 throw unsupported() 49 } 50 51 func search( 52 context: TeraLocalNetwork, 53 query: String, 54 limit _: UInt16, 55 asOfUnixSeconds _: UInt64 56 ) async throws -> [TeraSearchResult] { 57 request = SearchRequest(contextID: context.id, query: query) 58 if searchDelayNanoseconds > 0 { 59 try await Task.sleep(nanoseconds: searchDelayNanoseconds) 60 } 61 let card = Self.card() 62 let profile = Self.profile() 63 return [ 64 TeraSearchResult(type: .card, id: "card", card: card, profile: nil), 65 TeraSearchResult(type: .card, id: "card", card: card, profile: nil), 66 TeraSearchResult(type: .profile, id: "profile", card: nil, profile: profile), 67 ] 68 } 69 70 func me( 71 context _: TeraLocalNetwork, 72 asOfUnixSeconds _: UInt64 73 ) -> TeraMeSnapshot { 74 TeraMeSnapshot( 75 publicKey: String(repeating: "a", count: 64), 76 profile: Self.profile(), 77 cards: [Self.card()] 78 ) 79 } 80 81 func mobileSettings() -> TeraMobileSettings { 82 settings 83 } 84 85 func replaceMobileSettings( 86 input: TeraReplaceSettings 87 ) throws -> TeraSettingsTransition { 88 guard input.expectedRevision == settings.revision else { 89 throw failure(code: "settings_revision_conflict") 90 } 91 guard input.relays.allSatisfy({ $0.url.hasPrefix("ws://") || $0.url.hasPrefix("wss://") }) else { 92 throw failure(code: "invalid_relay_endpoint") 93 } 94 settings = TeraMobileSettings( 95 revision: settings.revision + 1, 96 identity: settings.identity, 97 networkEnvironment: input.networkEnvironment, 98 relays: input.relays, 99 blossomAuthority: input.blossomAuthority, 100 blossomPrimaryOrigin: input.blossomPrimaryOrigin, 101 blossomFallbackOrigins: input.blossomFallbackOrigins, 102 allowCellularDownloads: input.allowCellularDownloads, 103 allowCellularUploads: input.allowCellularUploads, 104 allowBackgroundTransfers: input.allowBackgroundTransfers, 105 mediaCacheBytes: input.mediaCacheBytes, 106 mediaCacheArtifacts: input.mediaCacheArtifacts 107 ) 108 return TeraSettingsTransition( 109 settings: settings, 110 runtimeRestartRequired: true, 111 outboxRequeueRequired: true, 112 mediaCacheInvalidationRequired: true 113 ) 114 } 115 116 func saveProfileMetadata(input: TeraProfileMetadataInput) -> TeraProfileStatus { 117 savedProfile = input 118 return profileStatus(state: .queued, revision: 1, settlement: nil) 119 } 120 121 func advanceProfile(operationID _: String) -> TeraProfileStatus { 122 profileStatus( 123 state: .complete, 124 revision: 2, 125 settlement: TeraOperationSettlement( 126 artifacts: 0, 127 signed: 1, 128 admitted: 1, 129 pending: 0, 130 retryable: 0, 131 indeterminate: 0, 132 failedTerminal: 0, 133 cancelled: 0, 134 deliveryPlans: 1, 135 deliverySatisfied: 1, 136 deliveryPending: 0, 137 deliveryRetryable: 0, 138 deliveryExhausted: 0, 139 deliveryFailedTerminal: 0, 140 deliveryCancelled: 0 141 ) 142 ) 143 } 144 145 func subscribe( 146 bufferCapacity _: Int, 147 receive _: @escaping @Sendable (TeraRuntimeChange) async -> Void 148 ) -> any TeraRuntimeSubscriptionToken { 149 SupportingSubscriptionToken() 150 } 151 152 func shutdown() -> TeraRuntimeShutdownReceipt { 153 let wasClosed = closed 154 closed = true 155 return TeraRuntimeShutdownReceipt(state: "closed", alreadyClosed: wasClosed) 156 } 157 158 func lastSearchRequest() -> SearchRequest? { 159 request 160 } 161 162 func settingsRevision() -> UInt64 { 163 settings.revision 164 } 165 } 166 167 extension SupportingBackend { 168 private func failure(code: String) -> TeraRuntimeFailure { 169 TeraRuntimeFailure( 170 schemaVersion: 1, 171 code: code, 172 category: "invalid_argument", 173 retryable: false, 174 recoveryActions: [], 175 operationID: "test.settings", 176 capabilityID: nil, 177 safeMessage: "The settings value is invalid." 178 ) 179 } 180 181 private func profileStatus( 182 state: TeraOutboxState, 183 revision: UInt64, 184 settlement: TeraOperationSettlement? 185 ) -> TeraProfileStatus { 186 TeraProfileStatus( 187 id: "profile-operation", 188 revision: revision, 189 authorPublicKey: String(repeating: "a", count: 64), 190 state: state, 191 deliveryID: state == .complete ? "delivery" : nil, 192 createdAtUnixMilliseconds: 1, 193 updatedAtUnixMilliseconds: revision, 194 settlement: settlement 195 ) 196 } 197 198 private static func initialSettings() -> TeraMobileSettings { 199 TeraMobileSettings( 200 revision: 1, 201 identity: TeraSettingsIdentityState( 202 identities: [ 203 TeraSettingsIdentity( 204 id: "identity", 205 publicKeyHex: String(repeating: "a", count: 64) 206 ), 207 ], 208 activeIdentityID: "identity", 209 lockState: .unlocked, 210 pendingImportOperationID: nil 211 ), 212 networkEnvironment: .simulator, 213 relays: [ 214 TeraRelayPreference(url: "ws://127.0.0.1:7447", access: .readWrite), 215 ], 216 blossomAuthority: .loopbackDevelopment, 217 blossomPrimaryOrigin: "http://127.0.0.1:3000", 218 blossomFallbackOrigins: [], 219 allowCellularDownloads: true, 220 allowCellularUploads: true, 221 allowBackgroundTransfers: true, 222 mediaCacheBytes: 256 * 1_048_576, 223 mediaCacheArtifacts: 1024 224 ) 225 } 226 227 private func unsupported() -> TeraRuntimeFailure { 228 .local( 229 operation: "test.supporting", 230 code: "test.unsupported", 231 safeMessage: "Unsupported test operation." 232 ) 233 } 234 235 private static func profile() -> TeraProfileSummary { 236 TeraProfileSummary( 237 authorPublicKey: String(repeating: "a", count: 64), 238 name: "moss", 239 displayName: "Moss Farm", 240 about: "Local roots", 241 picture: nil, 242 banner: nil, 243 nip05: "moss@example.com", 244 website: "https://moss.example", 245 lightningAddress: "moss@example.com" 246 ) 247 } 248 249 private static func card() -> TeraTodayCard { 250 TeraTodayCard( 251 id: "card", 252 type: .foodAvailability, 253 sourceEventID: String(repeating: "e", count: 64), 254 sourceAddress: nil, 255 authorPublicKey: String(repeating: "a", count: 64), 256 contractID: "radroots.food_availability.v1", 257 title: "Carrots", 258 content: "Freshly picked", 259 authoredAtUnixSeconds: 1_800_000_000, 260 effectiveAtUnixSeconds: 1_800_000_000, 261 calendarTiming: nil, 262 location: "Metchosin", 263 priceAmount: "3", 264 priceCurrency: "CAD", 265 priceUnit: "lb", 266 quantity: "12", 267 foodSummary: "Fresh carrots", 268 foodPublishedAtUnixSeconds: 1_800_000_000, 269 foodStatus: "active", 270 contextRank: 1, 271 inclusionReason: "local", 272 media: [], 273 lifecycle: .active, 274 rankDigest: nil, 275 authorProfile: profile(), 276 thread: [], 277 localOperationID: nil, 278 localOperationState: nil 279 ) 280 } 281 } 282 283 private actor SupportingSubscriptionToken: TeraRuntimeSubscriptionToken { 284 func cancel() {} 285 }