commit 0f8abfc773ffa16f134598e585c97f2dc57ba182
parent 9b6df06a16aa67258d8ed6238a9bd9ef9629ac92
Author: triesap <tyson@radroots.org>
Date: Wed, 9 Sep 2026 12:50:49 +0000
ios: resnapshot recovered observers and coalesce updates
- Refresh current local state after subscription recovery and typed gaps.
- Coalesce fixed domains with one supervised worker and a final pending refresh.
- Preserve scoped startup inventory, upload reconciliation, and media paging.
- Verify actual stream recovery, notification storms, and negative regressions.
Diffstat:
15 files changed, 608 insertions(+), 66 deletions(-)
diff --git a/Package.swift b/Package.swift
@@ -72,6 +72,7 @@ let package = Package(
"Runtime/TeraUserMessageClassifier.swift",
"Runtime/TeraUserMessages.swift",
"State/TeraAddStore.swift",
+ "State/TeraAddStartupSnapshot.swift",
"State/TeraAddPresentation.swift",
"State/TeraStoreObservation.swift",
"State/TeraConfigurationStore.swift",
diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj
@@ -14,6 +14,7 @@
2AEAFDD0EECA3B7F799B1287 /* TeraScopedAddTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A756C16ACFABBD36C48DF62 /* TeraScopedAddTests.swift */; };
2B50FA35AD753EFA15CF00CD /* TeraRuntimeBackpressureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8591841AD9AB9B718A37EB47 /* TeraRuntimeBackpressureTests.swift */; };
2C6DDB502DFA11C2CBCA49C4 /* TeraApp in Frameworks */ = {isa = PBXBuildFile; productRef = A1979630FE6BC343F0E59F24 /* TeraApp */; };
+ 30EDD3EB0C09D864FC1DD3D4 /* TeraStoreRecoveryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39C5F3460EEF38B98D78B4 /* TeraStoreRecoveryTests.swift */; };
31CBA09815D972CCE187009F /* TeraSessionResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B870D20178E633610113 /* TeraSessionResourceTests.swift */; };
38EF7FA8874E82B28110A232 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6967B3EAC4CF1A410B26DD1A /* PrivacyInfo.xcprivacy */; };
39B47EE698E9D7CC716A372F /* TeraRuntimeShutdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91E82E561F73A1BE6BBF09B7 /* TeraRuntimeShutdownTests.swift */; };
@@ -36,6 +37,7 @@
9AD968F0B37C64B5AF8D9A68 /* TeraRootShellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8677670E203FF68EC88983 /* TeraRootShellTests.swift */; };
9B37084901EC52EB44969304 /* TeraRuntimeResourceLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226F5C2370522AAFAD9D1C3D /* TeraRuntimeResourceLifecycleTests.swift */; };
A1AD2A380F09994786889AF1 /* TeraAddStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F22CFABCF27A28408F68D25 /* TeraAddStoreTests.swift */; };
+ AC33144E09CB0384C4FB090C /* TeraObservationCoalescingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD785439F415278214D746 /* TeraObservationCoalescingTests.swift */; };
B1B83B8673660E0EC109F74C /* TeraStateTestFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD45A3134A16704CBE4B1A5 /* TeraStateTestFixtures.swift */; };
B2DAAE01D16E1FAC35816DED /* RadrootsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1A9F6E32DE0C3728D4DB8E60 /* RadrootsKit */; };
B4AE86BE0A9F7FBE0DEBD9AC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = ADFD21D28C5F9E58B70E76CA /* Localizable.strings */; };
@@ -48,6 +50,7 @@
DF97F408D7D32F72C5AF5939 /* TeraRootShellUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5238B5F236C392C82CEF9838 /* TeraRootShellUITests.swift */; };
E508A7061082B091B733D62B /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67ACCE25979BA86C1EC5BFD6 /* App.swift */; };
EF7293C553BF1DB45EEEBD49 /* local-social-personas.v1.json in Resources */ = {isa = PBXBuildFile; fileRef = 27D9D40699A01FFB02F30B11 /* local-social-personas.v1.json */; };
+ F4AF91E71B691FE30C191852 /* TeraAddObservationStartupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A4460928B20A21A94BECCE /* TeraAddObservationStartupTests.swift */; };
F8C87782857DC930D47E6A90 /* TeraRuntimeResourceTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */; };
FBA9C3463CD1827392AF9599 /* TeraAddSchemaFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8ED2718D5E9E8980ADF3423 /* TeraAddSchemaFixtures.swift */; };
/* End PBXBuildFile section */
@@ -73,6 +76,7 @@
02F322CF98F8BEB908B93215 /* TeraMutationAdmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMutationAdmissionTests.swift; sourceTree = "<group>"; };
0CDE3D76E1EDF909C19D0506 /* TeraUITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = TeraUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
11E9C876EA26B30C8AFE116F /* TeraScopeFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopeFixtures.swift; sourceTree = "<group>"; };
+ 21DD785439F415278214D746 /* TeraObservationCoalescingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraObservationCoalescingTests.swift; sourceTree = "<group>"; };
226F5C2370522AAFAD9D1C3D /* TeraRuntimeResourceLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceLifecycleTests.swift; sourceTree = "<group>"; };
27D9D40699A01FFB02F30B11 /* local-social-personas.v1.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "local-social-personas.v1.json"; sourceTree = "<group>"; };
2A4FFC0023C620B68D0EC0ED /* TeraRuntimeBoundedTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeBoundedTaskTests.swift; sourceTree = "<group>"; };
@@ -81,6 +85,7 @@
3A756C16ACFABBD36C48DF62 /* TeraScopedAddTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopedAddTests.swift; sourceTree = "<group>"; };
3B808E67E85CD36E49C38B6F /* TeraCalendarFFICompatibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraCalendarFFICompatibilityTests.swift; sourceTree = "<group>"; };
4383B870D20178E633610113 /* TeraSessionResourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSessionResourceTests.swift; sourceTree = "<group>"; };
+ 44A4460928B20A21A94BECCE /* TeraAddObservationStartupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraAddObservationStartupTests.swift; sourceTree = "<group>"; };
46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraUserMessagesTests.swift; sourceTree = "<group>"; };
5238B5F236C392C82CEF9838 /* TeraRootShellUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRootShellUITests.swift; sourceTree = "<group>"; };
57CA176D0A7566A4ADD2CEA4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
@@ -104,6 +109,7 @@
AB8A30A75EFD65EFD938D248 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
ADFD21D28C5F9E58B70E76CA /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
BAD45A3134A16704CBE4B1A5 /* TeraStateTestFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraStateTestFixtures.swift; sourceTree = "<group>"; };
+ BD39C5F3460EEF38B98D78B4 /* TeraStoreRecoveryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraStoreRecoveryTests.swift; sourceTree = "<group>"; };
C0636AEDEB50DDDD328374A9 /* TeraSupportingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSupportingStoreTests.swift; sourceTree = "<group>"; };
C1DBB5D9AC2FE8EA3AE68458 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceTaskTests.swift; sourceTree = "<group>"; };
@@ -178,6 +184,7 @@
50B89388753A1FCD1B2A4F38 /* TeraTests */ = {
isa = PBXGroup;
children = (
+ 44A4460928B20A21A94BECCE /* TeraAddObservationStartupTests.swift */,
D8ED2718D5E9E8980ADF3423 /* TeraAddSchemaFixtures.swift */,
9F22CFABCF27A28408F68D25 /* TeraAddStoreTests.swift */,
61D8C0835D5A1D5C36D80020 /* TeraBackgroundUploadFixtures.swift */,
@@ -187,6 +194,7 @@
C890BDE885536BC2AB215393 /* TeraLifecycleTests.swift */,
61B6F0B16BFD8F046E9C163F /* TeraMediaStoreTests.swift */,
02F322CF98F8BEB908B93215 /* TeraMutationAdmissionTests.swift */,
+ 21DD785439F415278214D746 /* TeraObservationCoalescingTests.swift */,
E351F52E77C74A4E99095E45 /* TeraRemoteQualificationTests.swift */,
6B8677670E203FF68EC88983 /* TeraRootShellTests.swift */,
8591841AD9AB9B718A37EB47 /* TeraRuntimeBackpressureTests.swift */,
@@ -208,6 +216,7 @@
4383B870D20178E633610113 /* TeraSessionResourceTests.swift */,
98E969BCFF7CED33A1D4A040 /* TeraStateMigrationTests.swift */,
BAD45A3134A16704CBE4B1A5 /* TeraStateTestFixtures.swift */,
+ BD39C5F3460EEF38B98D78B4 /* TeraStoreRecoveryTests.swift */,
C0636AEDEB50DDDD328374A9 /* TeraSupportingStoreTests.swift */,
794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */,
46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */,
@@ -418,6 +427,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ F4AF91E71B691FE30C191852 /* TeraAddObservationStartupTests.swift in Sources */,
FBA9C3463CD1827392AF9599 /* TeraAddSchemaFixtures.swift in Sources */,
A1AD2A380F09994786889AF1 /* TeraAddStoreTests.swift in Sources */,
6F53FF88A29F8DF93F074EB4 /* TeraBackgroundUploadFixtures.swift in Sources */,
@@ -427,6 +437,7 @@
671EF50DB4EFBC25AABF26DC /* TeraLifecycleTests.swift in Sources */,
CDBE38E6B521611436F28858 /* TeraMediaStoreTests.swift in Sources */,
BB71614D2A6017C071A39BED /* TeraMutationAdmissionTests.swift in Sources */,
+ AC33144E09CB0384C4FB090C /* TeraObservationCoalescingTests.swift in Sources */,
81AF9DDA0E64DC4EEFCEF25D /* TeraRemoteQualificationTests.swift in Sources */,
9AD968F0B37C64B5AF8D9A68 /* TeraRootShellTests.swift in Sources */,
2B50FA35AD753EFA15CF00CD /* TeraRuntimeBackpressureTests.swift in Sources */,
@@ -448,6 +459,7 @@
31CBA09815D972CCE187009F /* TeraSessionResourceTests.swift in Sources */,
852FF7514FF7F682A246FB47 /* TeraStateMigrationTests.swift in Sources */,
B1B83B8673660E0EC109F74C /* TeraStateTestFixtures.swift in Sources */,
+ 30EDD3EB0C09D864FC1DD3D4 /* TeraStoreRecoveryTests.swift in Sources */,
2479571FA99A7962794999C4 /* TeraSupportingStoreTests.swift in Sources */,
64D49ADFD3FDD54CBA509DC5 /* TeraTodayStoreTests.swift in Sources */,
4734A842C366608EC1BD31C5 /* TeraUserMessagesTests.swift in Sources */,
diff --git a/Tera/State/TeraAddStartupSnapshot.swift b/Tera/State/TeraAddStartupSnapshot.swift
@@ -0,0 +1,18 @@
+struct TeraAddStartupSnapshot {
+ let schemas: [TeraAddSchema]
+ let drafts: [TeraDraftStatus]
+ let support: TeraAddMediaSupport
+
+ @MainActor
+ static func load(
+ client: TeraRuntimeClient, support: @MainActor () async throws -> TeraAddMediaSupport
+ ) async throws -> Self {
+ async let schemaResult = client.addSchemas()
+ async let draftResult = client.draftHeads(limit: 100)
+ async let supportResult = support()
+ return try await Self(
+ schemas: TeraProductSurfaceContract.validate(schemas: schemaResult),
+ drafts: draftResult, support: supportResult
+ )
+ }
+}
diff --git a/Tera/State/TeraAddStore.swift b/Tera/State/TeraAddStore.swift
@@ -34,6 +34,7 @@ final class TeraAddStore: ObservableObject {
private let observation = TeraStoreObservation()
private var configuration: TeraPresentationConfiguration?
private var draftsGeneration = TeraSessionGeneration.initial
+ private var appliedDraftsGeneration = TeraSessionGeneration.initial
private var probeGeneration = TeraSessionGeneration.initial
private var blossomGeneration = TeraSessionGeneration.initial
private var revisionTarget: TeraRevisionTarget?
@@ -133,28 +134,21 @@ final class TeraAddStore: ObservableObject {
generation = generation.invalidated()
let requestedGeneration = generation
draftsGeneration = draftsGeneration.invalidated()
- let draftRequest = draftsGeneration
- let serviceRequest = blossomGeneration
+ let draftRequest = appliedDraftsGeneration
+ let serviceConfiguration = configuration
do {
- async let schemaResult = runtimeClient.addSchemas()
- async let draftResult = runtimeClient.draftHeads(limit: 100)
- async let supportResult = loadMediaSupport()
- let (loadedSchemas, loadedDrafts, loadedSupport) = try await (
- schemaResult,
- draftResult,
- supportResult
- )
+ let loaded = try await TeraAddStartupSnapshot.load(client: runtimeClient, support: loadMediaSupport)
guard isCurrent(requestedGeneration) else { return }
- if draftRequest == draftsGeneration {
- try await media?.reconcileBackgroundUploads(drafts: loadedDrafts)
- }
+ let inventory = draftRequest == appliedDraftsGeneration ? loaded.drafts : drafts
+ try await media?.reconcileBackgroundUploads(drafts: inventory)
try ensureCurrent(requestedGeneration)
- schemas = try TeraProductSurfaceContract.validate(schemas: loadedSchemas)
- if draftRequest == draftsGeneration {
- drafts = TeraAddPresentation.sorted(loadedDrafts)
+ schemas = loaded.schemas
+ if draftRequest == appliedDraftsGeneration {
+ appliedDraftsGeneration = try appliedDraftsGeneration.next()
+ drafts = TeraAddPresentation.sorted(loaded.drafts)
}
- if serviceRequest == blossomGeneration {
- mediaSupport = loadedSupport
+ if serviceConfiguration == configuration {
+ mediaSupport = loaded.support
}
state = .ready
} catch {
@@ -669,6 +663,7 @@ final class TeraAddStore: ObservableObject {
do {
let loaded = try await runtimeClient.draftHeads(limit: 100)
guard isCurrent(requestedGeneration), request == draftsGeneration else { return }
+ appliedDraftsGeneration = try appliedDraftsGeneration.next()
drafts = TeraAddPresentation.sorted(loaded)
if let activeDraft, let current = loaded.first(where: { $0.id == activeDraft.id }),
current.revision >= activeDraft.revision
@@ -702,16 +697,17 @@ final class TeraAddStore: ObservableObject {
private func startObservation() {
observation.start(
- client: runtimeClient, capacity: 16, delay: observationDelay,
+ client: runtimeClient, buffer: (capacity: 16, delay: observationDelay),
state: { [weak self] in self?.observationState = $0 },
- change: { [weak self] change in
- guard let self, change.matches(context: configuration?.context) else { return }
+ accepts: { [weak self] in $0.matches(context: self?.configuration?.context) },
+ refresh: { [weak self] batch in
+ guard let self else { return }
let requested = generation
- if change.kind == .drafts || change.kind == .media {
+ if batch.contains(anyOf: [.drafts, .media]) {
await reloadDrafts()
}
guard isCurrent(requested) else { return }
- if change.kind == .media || change.kind == .settings {
+ if batch.contains(anyOf: [.media, .settings]) {
await refreshBlossomSnapshot()
}
}
@@ -731,6 +727,7 @@ final class TeraAddStore: ObservableObject {
) throws {
try ensureCurrent(requested)
draftsGeneration = draftsGeneration.invalidated()
+ appliedDraftsGeneration = try appliedDraftsGeneration.next()
activeDraft = status
if let form = status.form {
self.form = form
diff --git a/Tera/State/TeraStoreObservation.swift b/Tera/State/TeraStoreObservation.swift
@@ -13,31 +13,65 @@ struct TeraPresentationConfiguration: Equatable {
}
}
-/// Each store owns one observer. An old subscription or waiter cannot update or
-/// clear a replacement observer, including while subscription creation is pending.
+/// At most one entry per fixed notification domain; a gap subsumes every domain.
+struct TeraObservationBatch: Equatable {
+ private(set) var domains: Set<TeraRuntimeChangeKind> = []
+ private(set) var resnapshot = false
+
+ static var currentState: Self {
+ Self(resnapshot: true)
+ }
+
+ var isEmpty: Bool {
+ !resnapshot && domains.isEmpty
+ }
+
+ func contains(anyOf kinds: Set<TeraRuntimeChangeKind>) -> Bool {
+ resnapshot || !domains.isDisjoint(with: kinds)
+ }
+
+ mutating func insert(_ change: TeraRuntimeChange) {
+ if change.delivery == .resnapshotRequired || change.revision.rawValue == nil {
+ self = .currentState
+ } else if !resnapshot, change.kind != .initial {
+ domains.insert(change.kind)
+ }
+ }
+}
+
+/// One stream and one refresh worker per store. Ingestion never waits for a
+/// query; hints arriving during that query retain a bounded final refresh.
@MainActor
final class TeraStoreObservation {
private var generation = TeraSessionGeneration.initial
private var task: Task<Void, Never>?
+ private var refreshTask: Task<Void, Never>?
+ private var pending = TeraObservationBatch()
var isActive: Bool {
task != nil
}
- deinit { task?.cancel() }
+ deinit {
+ task?.cancel()
+ refreshTask?.cancel()
+ }
func stop() {
generation = generation.invalidated()
task?.cancel()
task = nil
+ refreshTask?.cancel()
+ refreshTask = nil
+ pending = TeraObservationBatch()
}
func start(
client: TeraRuntimeClient,
- capacity: Int,
- delay: @escaping @Sendable (UInt32) async throws -> Void,
+ buffer: (capacity: Int, delay: @Sendable (UInt32) async throws -> Void),
state: @escaping @MainActor (TeraRuntimeObservationState) -> Void,
- change: @escaping @MainActor (TeraRuntimeChange) async -> Void
+ accepts: @escaping @MainActor (TeraRuntimeChange) -> Bool,
+ refresh: @escaping @MainActor (TeraObservationBatch) async -> Void
) {
guard task == nil, generation.isActive else { return }
generation = generation.invalidated()
@@ -49,13 +83,17 @@ final class TeraStoreObservation {
state(.subscribing(attempt: attempt == .max ? .max : attempt + 1))
var message = TeraUserMessages.text(.runtimeObservationUnavailable)
do {
- let changes = try await client.changes(bufferCapacity: capacity)
+ let changes = try await client.changes(bufferCapacity: buffer.capacity)
guard self?.isCurrent(requested) == true else { break }
state(.active)
+ self?.requireCurrentState(requested, refresh: refresh)
for await value in changes {
guard self?.isCurrent(requested) == true else { break }
attempt = 0
- await change(value)
+ if accepts(value) {
+ self?.pending.insert(value)
+ self?.startRefresh(requested, refresh: refresh)
+ }
}
} catch {
message = TeraUserMessages.text(for: error, fallback: .runtimeObservationUnavailable)
@@ -63,18 +101,48 @@ final class TeraStoreObservation {
guard self?.isCurrent(requested) == true else { break }
attempt = attempt == .max ? .max : attempt + 1
state(.retrying(attempt: attempt, message: message))
- do { try await delay(attempt) } catch { break }
+ do { try await buffer.delay(attempt) } catch { break }
}
self?.finish(requested, state: state)
}
}
+ private func requireCurrentState(
+ _ requested: TeraSessionGeneration,
+ refresh: @escaping @MainActor (TeraObservationBatch) async -> Void
+ ) {
+ pending = .currentState
+ startRefresh(requested, refresh: refresh)
+ }
+
+ private func startRefresh(
+ _ requested: TeraSessionGeneration,
+ refresh: @escaping @MainActor (TeraObservationBatch) async -> Void
+ ) {
+ guard refreshTask == nil, !pending.isEmpty, isCurrent(requested) else { return }
+ refreshTask = Task { [weak self] in
+ while let batch = self?.takePending(requested) {
+ await refresh(batch)
+ }
+ guard self?.generation == requested else { return }
+ self?.refreshTask = nil
+ }
+ }
+
+ private func takePending(_ requested: TeraSessionGeneration) -> TeraObservationBatch? {
+ guard isCurrent(requested), !pending.isEmpty else { return nil }
+ let batch = pending
+ pending = TeraObservationBatch()
+ return batch
+ }
+
private func finish(
_ requested: TeraSessionGeneration, state: @MainActor (TeraRuntimeObservationState) -> Void
) {
guard generation == requested else { return }
- task = nil
- if Task.isCancelled {
+ let cancelled = Task.isCancelled
+ stop()
+ if cancelled {
state(.stopped)
}
}
diff --git a/Tera/State/TeraSupportingStores.swift b/Tera/State/TeraSupportingStores.swift
@@ -108,6 +108,9 @@ final class TeraMeStore: ObservableObject {
private var context: TeraLocalNetwork?
private var generation = TeraSessionGeneration.initial
private let observation = TeraStoreObservation()
+ private var reloadTask: Task<Void, Never>?
+
+ deinit { reloadTask?.cancel() }
init(
runtimeClient: TeraRuntimeClient,
@@ -123,6 +126,8 @@ final class TeraMeStore: ObservableObject {
func configure(context: TeraLocalNetwork?) {
guard self.context != context else { return }
generation = generation.invalidated()
+ reloadTask?.cancel()
+ reloadTask = nil
self.context = context
snapshot = nil
state = .idle
@@ -134,7 +139,10 @@ final class TeraMeStore: ObservableObject {
func start() async {
startObservation()
- await reload()
+ reloadTask?.cancel()
+ reloadTask = Task { [weak self] in await self?.reload() }
+ let task = reloadTask
+ await withTaskCancellationHandler { await task?.value } onCancel: { task?.cancel() }
}
func reload() async {
@@ -165,6 +173,8 @@ final class TeraMeStore: ObservableObject {
func stop() {
generation = generation.invalidated()
observation.stop()
+ reloadTask?.cancel()
+ reloadTask = nil
observationState = .stopped
snapshot = nil
state = .idle
@@ -172,16 +182,14 @@ final class TeraMeStore: ObservableObject {
private func startObservation() {
observation.start(
- client: runtimeClient, capacity: 8, delay: observationDelay,
+ client: runtimeClient, buffer: (capacity: 8, delay: observationDelay),
state: { [weak self] in self?.observationState = $0 },
- change: { [weak self] change in
- guard change.matches(context: self?.context) else { return }
- switch change.kind {
- case .today, .identity, .profile, .media, .drafts:
- await self?.reload()
- case .initial, .settings, .relay, .lifecycle:
- break
- }
+ accepts: { [weak self] in $0.matches(context: self?.context) },
+ refresh: { [weak self] batch in
+ guard batch.contains(anyOf: [.today, .identity, .profile, .media, .drafts]) else { return }
+ await self?.reloadTask?.value
+ guard !Task.isCancelled else { return }
+ await self?.reload()
}
)
}
diff --git a/Tera/State/TeraTodayStore.swift b/Tera/State/TeraTodayStore.swift
@@ -83,7 +83,9 @@ final class TeraTodayStore: ObservableObject {
func start() async {
guard !observation.isActive, !Task.isCancelled else { return }
startObservation()
- await reload()
+ scheduleReload()
+ let task = reloadTask
+ await withTaskCancellationHandler { await task?.value } onCancel: { task?.cancel() }
}
func stop() {
@@ -235,16 +237,16 @@ final class TeraTodayStore: ObservableObject {
private func startObservation() {
observation.start(
- client: runtimeClient, capacity: 16, delay: observationDelay,
+ client: runtimeClient, buffer: (capacity: 16, delay: observationDelay),
state: { [weak self] in self?.observationState = $0 },
- change: { [weak self] change in
- guard change.matches(context: self?.selectedContext) else { return }
- switch change.kind {
- case .today, .drafts, .media, .identity, .profile:
- await self?.reload(refreshProjection: false)
- case .initial, .settings, .relay, .lifecycle:
- break
- }
+ accepts: { [weak self] in $0.matches(context: self?.selectedContext) },
+ refresh: { [weak self] batch in
+ // Ordinary media progress belongs to the media presentation;
+ // it must not reset the feed's loaded pages and cursor.
+ guard batch.contains(anyOf: [.today, .drafts, .identity, .profile]) else { return }
+ await self?.reloadTask?.value
+ guard !Task.isCancelled else { return }
+ await self?.reload(refreshProjection: false)
}
)
}
diff --git a/TeraTests/TeraAddObservationStartupTests.swift b/TeraTests/TeraAddObservationStartupTests.swift
@@ -0,0 +1,86 @@
+@testable import TeraApp
+import XCTest
+
+@MainActor
+final class TeraAddObservationStartupTests: XCTestCase {
+ func testPendingObserverCannotDiscardUsableStartupInventory() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let media = ObservationStartupMedia()
+ let subscription = await backend.pause(.subscribe)
+ let initialDrafts = await backend.pause(.drafts)
+ let store = TeraAddStore(runtimeClient: client, media: media)
+ store.configure(snapshot: TeraScopeFixtures.snapshot())
+ let startup = Task { await store.start() }
+ await initialDrafts.entered.wait()
+ await media.supportPause.entered.wait()
+ let observerDrafts = await backend.pause(.drafts)
+ await initialDrafts.resume.open()
+ await subscription.resume.open()
+ await observerDrafts.entered.wait()
+ await media.supportPause.resume.open()
+ await startup.value
+ XCTAssertEqual(store.drafts.first?.form?.content, "old")
+ XCTAssertEqual(store.mediaSupport, .init(library: true, camera: true))
+ XCTAssertEqual(store.state, .ready)
+ let inventories = await media.inventories
+ XCTAssertEqual(inventories.map { $0.first?.revision }, [1])
+ await observerDrafts.resume.open()
+ store.stop()
+ _ = try await client.stop()
+ }
+
+ func testStartupReconcilesTheAppliedObserverInventoryAndKeepsMediaSupport() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let media = ObservationStartupMedia()
+ let subscription = await backend.pause(.subscribe)
+ let initialDrafts = await backend.pause(.drafts)
+ let store = TeraAddStore(runtimeClient: client, media: media)
+ store.configure(snapshot: TeraScopeFixtures.snapshot())
+ let startup = Task { await store.start() }
+ await initialDrafts.entered.wait()
+ await media.supportPause.entered.wait()
+ await initialDrafts.resume.open()
+ await backend.setDrafts([TeraScopeFixtures.draft("new", revision: 2)])
+ await backend.configure(TeraScopeFixtures.snapshot(evidence: TeraScopeFixtures.evidence(observedAt: 2)))
+ await subscription.resume.open()
+ await TeraScopeFixtures.eventually {
+ store.drafts.first?.revision == 2 && store.blossomEvidence?.observedAtUnixMilliseconds == 2
+ }
+ await media.supportPause.resume.open()
+ await startup.value
+ XCTAssertEqual(store.drafts.first?.revision, 2)
+ XCTAssertEqual(store.mediaSupport, .init(library: true, camera: true))
+ let inventories = await media.inventories
+ XCTAssertEqual(inventories.map { $0.first?.revision }, [2])
+ store.stop()
+ _ = try await client.stop()
+ }
+}
+
+private actor ObservationStartupMedia: TeraAddMediaHandling {
+ let supportPause = ResourceTestPause()
+ private(set) var inventories: [[TeraDraftStatus]] = []
+
+ func support() async -> TeraAddMediaSupport {
+ await supportPause.wait()
+ return .init(library: true, camera: true)
+ }
+
+ func reconcileBackgroundUploads(drafts: [TeraDraftStatus]) {
+ inventories.append(drafts)
+ }
+
+ func importImages(limit _: Int) throws -> [TeraPreparedMedia] {
+ throw TeraScopeFixtures.failure()
+ }
+
+ func captureImage() throws -> TeraPreparedMedia {
+ throw TeraScopeFixtures.failure()
+ }
+
+ func open(_: [TeraPreparedMedia]) throws -> TeraOpenedMedia {
+ throw TeraScopeFixtures.failure()
+ }
+}
diff --git a/TeraTests/TeraObservationCoalescingTests.swift b/TeraTests/TeraObservationCoalescingTests.swift
@@ -0,0 +1,181 @@
+@testable import TeraApp
+import XCTest
+
+@MainActor
+final class TeraObservationCoalescingTests: XCTestCase {
+ func testRecoveryResnapshotsWithoutAnInitialHintOrAnotherEvent() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let observation = TeraStoreObservation()
+ let retry = ResourceTestPause()
+ var batches: [TeraObservationBatch] = []
+ observation.start(
+ client: client, buffer: (capacity: 8, delay: { _ in await retry.wait() }), state: { _ in },
+ accepts: { _ in true }, refresh: { batches.append($0) }
+ )
+ await TeraScopeFixtures.eventually { batches.count == 1 }
+ await client.suspend()
+ await retry.entered.wait()
+ XCTAssertEqual(batches.count, 1)
+ await retry.resume.open()
+ await TeraScopeFixtures.eventually { batches.count == 2 }
+ XCTAssertTrue(batches.allSatisfy(\.resnapshot))
+ let subscriptions = await backend.counts[.subscribe]
+ XCTAssertEqual(subscriptions, 2)
+ observation.stop()
+ _ = try await client.stop()
+ }
+
+ func testStormCoalescesFixedDomainsAndDisplaysOneFinalRevision() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let observation = TeraStoreObservation()
+ let firstRead = ResourceTestPause()
+ let kinds: [TeraRuntimeChangeKind] = [.today, .drafts, .media, .settings, .identity, .profile, .relay, .lifecycle]
+ var batches: [TeraObservationBatch] = []
+ let durable = ObservationTestState()
+ var visibleRevisions: [Int] = []
+ var seen: UInt64 = 0
+ observation.start(
+ client: client, buffer: (capacity: 8, delay: { _ in throw CancellationError() }), state: { _ in },
+ accepts: { seen = $0.revision.rawValue ?? seen; return true },
+ refresh: { batch in
+ batches.append(batch)
+ let captured = durable.revision
+ if batches.count == 1 {
+ await firstRead.wait()
+ }
+ visibleRevisions.append(captured)
+ }
+ )
+ await firstRead.entered.wait()
+ for index in 1 ... 256 {
+ durable.revision = index
+ await backend.emit(kinds[(index - 1) % kinds.count])
+ await TeraScopeFixtures.eventually { seen == UInt64(index) }
+ }
+ XCTAssertEqual(batches.count, 1)
+ await firstRead.resume.open()
+ await TeraScopeFixtures.eventually { visibleRevisions.count == 2 }
+ XCTAssertEqual(visibleRevisions, [0, 256])
+ XCTAssertEqual(try XCTUnwrap(batches.last).domains, Set(kinds))
+ XCTAssertFalse(try XCTUnwrap(batches.last).resnapshot)
+ observation.stop()
+ _ = try await client.stop()
+ }
+
+ func testGapOrExhaustedRevisionForcesFinalSnapshotAfterSilence() async throws {
+ for exhausted in [false, true] {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let observation = TeraStoreObservation()
+ let first = ResourceTestPause()
+ var seen = false
+ var batches: [TeraObservationBatch] = []
+ observation.start(
+ client: client, buffer: (capacity: 1, delay: { _ in throw CancellationError() }), state: { _ in },
+ accepts: { _ in seen = true; return true }, refresh: {
+ batches.append($0)
+ if batches.count == 1 {
+ await first.wait()
+ }
+ }
+ )
+ await first.entered.wait()
+ await backend.emit(.initial, delivery: exhausted ? .change : .resnapshotRequired, exhausted: exhausted)
+ await TeraScopeFixtures.eventually { seen }
+ XCTAssertEqual(batches.count, 1)
+ await first.resume.open()
+ await TeraScopeFixtures.eventually { batches.count == 2 }
+ XCTAssertTrue(try XCTUnwrap(batches.last).resnapshot)
+ XCTAssertTrue(try XCTUnwrap(batches.last).domains.isEmpty)
+ observation.stop()
+ _ = try await client.stop()
+ }
+ }
+
+ func testOldRefreshCompletionCannotClearReplacementOrItsPendingWork() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let observation = TeraStoreObservation()
+ let oldRead = ResourceTestPause()
+ let newRead = ResourceTestPause()
+ let oldFinished = ResourceTestGate()
+ observation.start(
+ client: client, buffer: (capacity: 8, delay: { _ in throw CancellationError() }), state: { _ in },
+ accepts: { _ in true }, refresh: { _ in await oldRead.wait(); await oldFinished.open() }
+ )
+ await oldRead.entered.wait()
+ observation.stop()
+ var seen = false
+ var newBatches: [TeraObservationBatch] = []
+ observation.start(
+ client: client, buffer: (capacity: 8, delay: { _ in throw CancellationError() }), state: { _ in },
+ accepts: { _ in seen = true; return true }, refresh: {
+ newBatches.append($0)
+ if newBatches.count == 1 {
+ await newRead.wait()
+ }
+ }
+ )
+ await newRead.entered.wait()
+ await oldRead.resume.open()
+ await oldFinished.wait()
+ await backend.emit(.drafts)
+ await TeraScopeFixtures.eventually { seen }
+ XCTAssertEqual(newBatches.count, 1)
+ await newRead.resume.open()
+ await TeraScopeFixtures.eventually { newBatches.count == 2 }
+ XCTAssertEqual(try XCTUnwrap(newBatches.last).domains, [.drafts])
+ observation.stop()
+ _ = try await client.stop()
+ }
+
+ func testOldCompletionCannotOrphanReplacementRefreshFromStop() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let observation = TeraStoreObservation()
+ let oldRead = ResourceTestPause()
+ let newRead = ResourceTestPause()
+ let newFinished = ResourceTestGate()
+ weak var oldLifetime: ObservationTaskLifetime?
+ do {
+ let lifetime = ObservationTaskLifetime()
+ oldLifetime = lifetime
+ observation.start(
+ client: client, buffer: (capacity: 8, delay: { _ in throw CancellationError() }), state: { _ in },
+ accepts: { _ in true }, refresh: { [lifetime] _ in
+ await oldRead.wait()
+ withExtendedLifetime(lifetime) {}
+ }
+ )
+ }
+ await oldRead.entered.wait()
+ observation.stop()
+ var cancelled: Bool?
+ observation.start(
+ client: client, buffer: (capacity: 8, delay: { _ in throw CancellationError() }), state: { _ in },
+ accepts: { _ in true }, refresh: { _ in
+ await newRead.wait()
+ cancelled = Task.isCancelled
+ await newFinished.open()
+ }
+ )
+ await newRead.entered.wait()
+ await oldRead.resume.open()
+ await TeraScopeFixtures.eventually { oldLifetime == nil }
+ observation.stop()
+ await newRead.resume.open()
+ await newFinished.wait()
+ XCTAssertEqual(cancelled, true, "Stop must still own and cancel the replacement refresh")
+ _ = try await client.stop()
+ }
+}
+
+@MainActor
+private final class ObservationTaskLifetime {}
+
+@MainActor
+private final class ObservationTestState {
+ var revision = 0
+}
diff --git a/TeraTests/TeraScopeBackend.swift b/TeraTests/TeraScopeBackend.swift
@@ -16,6 +16,9 @@ actor TeraScopeBackend: TeraRuntimeBackend {
private(set) var lastMeContext: TeraLocalNetwork?
private var drafts: [TeraDraftStatus] = [TeraScopeFixtures.draft("old")]
private var media: TeraVerifiedMediaArtifact
+ private var pages: [String: TeraTodayPage] = [:]
+ private var meCards = [TeraScopeFixtures.card("old")]
+ private var revision: UInt64 = 0
init() throws {
media = try TeraScopeFixtures.artifact("a")
@@ -39,6 +42,14 @@ actor TeraScopeBackend: TeraRuntimeBackend {
media = value
}
+ func setPage(_ value: TeraTodayPage, cursor: String = "first") {
+ pages[cursor] = value
+ }
+
+ func setMeCards(_ values: [TeraTodayCard]) {
+ meCards = values
+ }
+
private func wait(_ call: Call) async throws {
counts[call, default: 0] += 1
guard var queue = pending[call], !queue.isEmpty else { return }
@@ -87,7 +98,7 @@ actor TeraScopeBackend: TeraRuntimeBackend {
}
func todayPage(request: TeraTodayPageRequest) async throws -> TeraTodayPage {
- let result = TeraTodayPage(
+ let result = pages[request.cursor ?? "first"] ?? TeraTodayPage(
asOfUnixSeconds: 1, items: [TeraScopeFixtures.card(request.context.relayURLs.first ?? "none")], nextCursor: nil
)
try await wait(.page)
@@ -112,7 +123,7 @@ actor TeraScopeBackend: TeraRuntimeBackend {
func me(context: TeraLocalNetwork, asOfUnixSeconds _: UInt64) async throws -> TeraMeSnapshot {
lastMeContext = context
- let result = TeraMeSnapshot(publicKey: value.identity.publicKeyHex, profile: nil, cards: [TeraScopeFixtures.card("old")])
+ let result = TeraMeSnapshot(publicKey: value.identity.publicKeyHex, profile: nil, cards: meCards)
try await wait(.me)
return result
}
@@ -138,13 +149,21 @@ actor TeraScopeBackend: TeraRuntimeBackend {
return token
}
- func emit(_ kind: TeraRuntimeChangeKind) async {
+ func emit(
+ _ kind: TeraRuntimeChangeKind,
+ delivery: TeraRuntimeChangeDelivery = .change,
+ context: TeraLocalNetwork? = nil,
+ exhausted: Bool = false
+ ) async {
+ revision += 1
+ let emittedRevision = revision
for receive in receivers {
await receive(TeraRuntimeChange(
schemaVersion: 3,
- scope: TeraRuntimeChangeScope(publicKey: String(repeating: "a", count: 64), sourceGeneration: String(repeating: "a", count: 64), context: nil),
+ scope: TeraRuntimeChangeScope(publicKey: String(repeating: "a", count: 64), sourceGeneration: String(repeating: "a", count: 64), context: context),
epoch: String(repeating: "1", count: 32),
- revision: TeraProjectionRevision(rawValue: 1), delivery: .change, kind: kind, entityID: nil
+ revision: TeraProjectionRevision(rawValue: exhausted ? nil : emittedRevision),
+ delivery: delivery, kind: kind, entityID: nil
))
}
}
diff --git a/TeraTests/TeraScopedObservationTests.swift b/TeraTests/TeraScopedObservationTests.swift
@@ -36,16 +36,16 @@ final class TeraScopedObservationTests: XCTestCase {
var state = TeraRuntimeObservationState.inactive
let pause = await backend.pause(.subscribe, fails: true)
observation.start(
- client: client, capacity: 8, delay: { _ in throw CancellationError() },
- state: { state = $0 }, change: { _ in }
+ client: client, buffer: (capacity: 8, delay: { _ in throw CancellationError() }),
+ state: { state = $0 }, accepts: { _ in true }, refresh: { _ in }
)
await pause.entered.wait()
await pause.resume.open()
await TeraScopeFixtures.eventually { !observation.isActive }
guard case .retrying = state else { return XCTFail("The failed retry remains visible") }
observation.start(
- client: client, capacity: 8, delay: { _ in throw CancellationError() },
- state: { state = $0 }, change: { _ in }
+ client: client, buffer: (capacity: 8, delay: { _ in throw CancellationError() }),
+ state: { state = $0 }, accepts: { _ in true }, refresh: { _ in }
)
await TeraScopeFixtures.eventually { state == .active }
let count = await backend.counts[.subscribe]
diff --git a/TeraTests/TeraStoreRecoveryTests.swift b/TeraTests/TeraStoreRecoveryTests.swift
@@ -0,0 +1,147 @@
+@testable import TeraApp
+import XCTest
+
+@MainActor
+final class TeraStoreRecoveryTests: XCTestCase {
+ func testRecoveredTodayAddAndMeReadCurrentStateWithoutAnotherEvent() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let retry = ResourceTestPause()
+ let today = TeraTodayStore(runtimeClient: client, observationDelay: { _ in await retry.wait() })
+ let add = TeraAddStore(runtimeClient: client, observationDelay: { _ in await retry.wait() })
+ let me = TeraMeStore(runtimeClient: client, observationDelay: { _ in await retry.wait() })
+ configure(today: today, add: add, me: me)
+ await today.start()
+ await add.start()
+ await me.start()
+ await TeraScopeFixtures.eventually {
+ today.observationState == .active && add.observationState == .active && me.observationState == .active
+ }
+ await client.suspend()
+ await TeraScopeFixtures.eventually {
+ self.isRetrying(today.observationState) && self.isRetrying(add.observationState) && self.isRetrying(me.observationState)
+ }
+ await setCurrentState(backend, revision: 2)
+ await retry.resume.open()
+ await TeraScopeFixtures.eventually {
+ today.cards.first?.id == "revision-2" && add.drafts.first?.revision == 2
+ && me.snapshot?.cards.first?.id == "revision-2" && add.blossomEvidence?.observedAtUnixMilliseconds == 2
+ }
+ let subscriptions = await backend.counts[.subscribe]
+ XCTAssertEqual(subscriptions, 6)
+ today.stop()
+ add.stop()
+ me.stop()
+ _ = try await client.stop()
+ }
+
+ func testFinalGapRefreshesAllStoreDomainsWithoutOverwritingTheEditingForm() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let today = TeraTodayStore(runtimeClient: client)
+ let add = TeraAddStore(runtimeClient: client)
+ let me = TeraMeStore(runtimeClient: client)
+ configure(today: today, add: add, me: me)
+ await today.start()
+ await add.start()
+ await me.start()
+ await TeraScopeFixtures.eventually {
+ today.observationState == .active && add.observationState == .active && me.observationState == .active
+ }
+ add.updateForm(\.content, "Unsaved local editing")
+ await setCurrentState(backend, revision: 9)
+ await backend.emit(.initial, delivery: .resnapshotRequired)
+ await TeraScopeFixtures.eventually {
+ today.cards.first?.id == "revision-9" && add.drafts.first?.revision == 9
+ && me.snapshot?.cards.first?.id == "revision-9" && add.blossomEvidence?.observedAtUnixMilliseconds == 9
+ }
+ XCTAssertEqual(add.form.content, "Unsaved local editing")
+ let refreshes = await backend.counts[.refresh]
+ XCTAssertEqual(refreshes, 1, "Observation recovery reads local state without another network refresh")
+ today.stop()
+ add.stop()
+ me.stop()
+ _ = try await client.stop()
+ }
+
+ func testTodayStormDuringSlowReadEventuallyDisplaysTheFinalDurablePage() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let store = TeraTodayStore(runtimeClient: client)
+ store.configure(snapshot: TeraScopeFixtures.snapshot())
+ await store.start()
+ await TeraScopeFixtures.eventually { store.observationState == .active && !store.cards.isEmpty }
+ let first = await backend.pause(.page)
+ await backend.emit(.today)
+ await first.entered.wait()
+ let before = await backend.counts[.page, default: 0]
+ await setCurrentState(backend, revision: 256)
+ for _ in 1 ... 256 {
+ await backend.emit(.today)
+ }
+ let paused = await backend.counts[.page, default: 0]
+ XCTAssertEqual(paused, before, "Only one observer query may execute at a time")
+ await first.resume.open()
+ await TeraScopeFixtures.eventually { store.cards.first?.id == "revision-256" }
+ XCTAssertEqual(store.state, .loaded)
+ store.stop()
+ _ = try await client.stop()
+ }
+
+ func testOrdinaryMediaProgressPreservesLoadedPagesAndTheNextCursor() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let subscription = await backend.pause(.subscribe)
+ await backend.setPage(page("one", next: "two"))
+ await backend.setPage(page("two", next: "three"), cursor: "two")
+ await backend.setPage(page("three"), cursor: "three")
+ let store = TeraTodayStore(runtimeClient: client)
+ store.configure(snapshot: TeraScopeFixtures.snapshot())
+ await store.start()
+ await backend.setPage(page("current-one", next: "two"))
+ await subscription.resume.open()
+ await TeraScopeFixtures.eventually { store.cards.first?.id == "current-one" }
+ await store.loadNextPage()
+ XCTAssertEqual(store.cards.map(\.id), ["current-one", "two"])
+ let before = await backend.counts[.page, default: 0]
+ // Space hints beyond one actor turn so this exercises ordinary progress,
+ // independently of C029's intentionally stronger overflow resnapshot.
+ for _ in 1 ... 8 {
+ await backend.emit(.media)
+ try await Task.sleep(for: .milliseconds(10))
+ }
+ let after = await backend.counts[.page, default: 0]
+ XCTAssertEqual(after, before)
+ XCTAssertEqual(store.cards.map(\.id), ["current-one", "two"])
+ XCTAssertTrue(store.canLoadNextPage)
+ await store.loadNextPage()
+ XCTAssertEqual(store.cards.map(\.id), ["current-one", "two", "three"])
+ store.stop()
+ _ = try await client.stop()
+ }
+
+ private func configure(today: TeraTodayStore, add: TeraAddStore, me: TeraMeStore) {
+ let snapshot = TeraScopeFixtures.snapshot()
+ today.configure(snapshot: snapshot)
+ add.configure(snapshot: snapshot)
+ me.configure(context: .defaultContext(snapshot: snapshot))
+ }
+
+ private func setCurrentState(_ backend: TeraScopeBackend, revision: UInt64) async {
+ await backend.setPage(page("revision-\(revision)"))
+ await backend.setDrafts([TeraScopeFixtures.draft("revision-\(revision)", revision: revision)])
+ await backend.setMeCards([TeraScopeFixtures.card("revision-\(revision)")])
+ await backend.configure(TeraScopeFixtures.snapshot(evidence: TeraScopeFixtures.evidence(observedAt: revision)))
+ }
+
+ private func page(_ id: String, next: String? = nil) -> TeraTodayPage {
+ TeraTodayPage(asOfUnixSeconds: 1, items: [TeraScopeFixtures.card(id)], nextCursor: next)
+ }
+
+ private func isRetrying(_ value: TeraRuntimeObservationState) -> Bool {
+ if case .retrying = value {
+ return true
+ }
+ return false
+ }
+}
diff --git a/project.yml b/project.yml
@@ -68,6 +68,9 @@ targets:
- path: TeraTests/TeraScopedAddTests.swift
- path: TeraTests/TeraScopedMediaTests.swift
- path: TeraTests/TeraScopedObservationTests.swift
+ - path: TeraTests/TeraObservationCoalescingTests.swift
+ - path: TeraTests/TeraStoreRecoveryTests.swift
+ - path: TeraTests/TeraAddObservationStartupTests.swift
- path: TeraTests/TeraMediaStoreTests.swift
- path: TeraTests/TeraAddStoreTests.swift
- path: TeraTests/TeraBackgroundUploadFixtures.swift
diff --git a/release/provenance.json b/release/provenance.json
@@ -6,7 +6,7 @@
"info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef",
"privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12",
"sbom_sha256": "6b5ce897d5273290febc8b831663e12dea759cb0084f182ba7949e1c593fc410",
- "xcode_project_sha256": "63f1a2832f546e957d756cad896e085a76e28283c04b8adfdc2e7ff02521cbab"
+ "xcode_project_sha256": "1d54cb56afd86377fa813d17527958bfa4b387f23e5cb6b6045980c2f0d99785"
},
"disposition": "unsigned",
"platforms": [
diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json
@@ -1 +1 @@
-[{"text":" init(","violation":{"location":{"line":83,"file":"Tera\/App\/TeraAppModel.swift","character":3},"severity":"error","ruleIdentifier":"function_body_length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"location":{"line":73,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1},"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"#endif","violation":{"location":{"line":480,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 480","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"location":{"line":111,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":15},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 545 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":1657,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1657","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraRuntimeClient {","violation":{"location":{"line":291,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1},"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 787 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"location":{"line":703,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":3},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":1159,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1159","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"}","violation":{"location":{"line":1215,"file":"Tera\/Runtime\/TeraRuntimeModels.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1215","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"location":{"line":11,"file":"Tera\/State\/TeraAddStore.swift","character":7},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 734 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func retract(_ card: TeraTodayCard) async {","violation":{"location":{"line":465,"file":"Tera\/State\/TeraAddStore.swift","character":3},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":799,"file":"Tera\/State\/TeraAddStore.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 799","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraConfigurationStore {","violation":{"location":{"line":78,"file":"Tera\/State\/TeraConfigurationStore.swift","character":1},"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"location":{"line":148,"file":"Tera\/State\/TeraConfigurationStore.swift","character":5},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":759,"file":"Tera\/State\/TeraConfigurationStore.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 759","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraSessionStore {","violation":{"location":{"line":39,"file":"Tera\/State\/TeraSessionStore.swift","character":1},"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"location":{"line":125,"file":"Tera\/State\/TeraSessionStore.swift","character":11},"severity":"error","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."}},{"text":"}","violation":{"location":{"line":452,"file":"Tera\/State\/TeraSessionStore.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 452","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"struct TeraAddView: View {","violation":{"location":{"line":4,"file":"Tera\/Views\/TeraAddView.swift","character":1},"severity":"error","ruleIdentifier":"type_body_length","reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":560,"file":"Tera\/Views\/TeraAddView.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 560","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"struct TeraSettingsView: View {","violation":{"location":{"line":365,"file":"Tera\/Views\/TeraSupportingViews.swift","character":1},"severity":"error","ruleIdentifier":"type_body_length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":629,"file":"Tera\/Views\/TeraSupportingViews.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 629","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"}","violation":{"location":{"line":493,"file":"Tera\/Views\/TeraTodayView.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 493","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"location":{"line":6,"file":"TeraTests\/TeraAddStoreTests.swift","character":7},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"private actor AddBackend: TeraRuntimeBackend {","violation":{"location":{"line":847,"file":"TeraTests\/TeraAddStoreTests.swift","character":9},"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" private func makeStatus(","violation":{"location":{"line":1283,"file":"TeraTests\/TeraAddStoreTests.swift","character":11},"severity":"error","ruleIdentifier":"function_parameter_count","reason":"Function should have 5 parameters or less: it currently has 8","ruleName":"Function Parameter Count","ruleDescription":"Number of function parameters should be low."}},{"text":"}","violation":{"location":{"line":1434,"file":"TeraTests\/TeraAddStoreTests.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1434","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"location":{"line":7,"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":7},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"location":{"line":45,"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"location":{"line":212,"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"location":{"line":4,"file":"TeraTests\/TeraRuntimeClientTests.swift","character":7},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"location":{"line":6,"file":"TeraTests\/TeraStateMigrationTests.swift","character":7},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"location":{"line":7,"file":"TeraTests\/TeraStateMigrationTests.swift","character":5},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":622,"file":"TeraTests\/TeraStateMigrationTests.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 622","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"location":{"line":207,"file":"TeraTests\/TeraSupportingStoreTests.swift","character":9},"severity":"error","ruleIdentifier":"type_body_length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":487,"file":"TeraTests\/TeraSupportingStoreTests.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 487","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"location":{"line":4,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":7},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"location":{"line":62,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"location":{"line":174,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"location":{"line":247,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"location":{"line":573,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11},"severity":"error","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"location":{"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11},"severity":"error","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 11","ruleName":"Cyclomatic Complexity","ruleDescription":"Complexity of function bodies should be limited."}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"location":{"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" static func environment(","violation":{"location":{"line":1736,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":10},"severity":"error","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"location":{"line":2212,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":1},"severity":"error","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 2212","ruleName":"File Length","ruleDescription":"Files should not span too many lines."}}]
-\ No newline at end of file
+[{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/App\/TeraAppModel.swift","character":3,"line":83},"reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" init("},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1,"line":73},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1,"line":480},"reason":"File should contain 400 lines or less: currently contains 480","ruleDescription":"Files should not span too many lines."},"text":"#endif"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":15,"line":111},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 545 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","character":1,"line":1657},"reason":"File should contain 1000 lines or less: currently contains 1657","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1,"line":291},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 787 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraRuntimeClient {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":3,"line":703},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":1,"line":1159},"reason":"File should contain 1000 lines or less: currently contains 1159","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","character":1,"line":1215},"reason":"File should contain 1000 lines or less: currently contains 1215","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","character":7,"line":11},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","character":3,"line":459},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","character":1,"line":796},"reason":"File should contain 400 lines or less: currently contains 796","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":78},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":5,"line":148},"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","character":1,"line":759},"reason":"File should contain 400 lines or less: currently contains 759","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":1,"line":39},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraSessionStore {"},{"violation":{"ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":11,"line":125},"reason":"Function should have complexity 10 or less; currently complexity is 13","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","character":1,"line":452},"reason":"File should contain 400 lines or less: currently contains 452","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":4},"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraAddView: View {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":560},"reason":"File should contain 400 lines or less: currently contains 560","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":365},"reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":629},"reason":"File should contain 400 lines or less: currently contains 629","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraTodayView.swift","character":1,"line":493},"reason":"File should contain 400 lines or less: currently contains 493","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":7,"line":6},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":9,"line":847},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor AddBackend: TeraRuntimeBackend {"},{"violation":{"ruleIdentifier":"function_parameter_count","ruleName":"Function Parameter Count","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":11,"line":1283},"reason":"Function should have 5 parameters or less: it currently has 8","ruleDescription":"Number of function parameters should be low."},"text":" private func makeStatus("},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":1,"line":1434},"reason":"File should contain 1000 lines or less: currently contains 1434","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":7,"line":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":45},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","character":3,"line":212},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","character":7,"line":4},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":7,"line":6},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":5,"line":7},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":1,"line":622},"reason":"File should contain 400 lines or less: currently contains 622","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":9,"line":207},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":1,"line":487},"reason":"File should contain 400 lines or less: currently contains 487","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":7,"line":4},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":62},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":174},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":3,"line":247},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":573},"reason":"Function should have complexity 10 or less; currently complexity is 18","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":767},"reason":"Function should have complexity 10 or less; currently complexity is 11","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11,"line":767},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":10,"line":1736},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" static func environment("},{"violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":1,"line":2212},"reason":"File should contain 1000 lines or less: currently contains 2212","ruleDescription":"Files should not span too many lines."},"text":"}"}]
+\ No newline at end of file