commit d81684032d11bdc8b89b8613b6129873d1800e9b
parent a9977d1ec88e74fd0943dff1b04642cd98760db0
Author: triesap <tyson@radroots.org>
Date: Thu, 10 Sep 2026 03:58:53 +0000
today: Bound rendering and cached query work
- Replace full history generation reads with bounded owner queries
- Preserve full detail while bounding feed text and thumbnails
- Supervise raster decoding with validated cache and worker budgets
- Qualify real SQLite performance and native resource boundaries
Diffstat:
24 files changed, 1445 insertions(+), 164 deletions(-)
diff --git a/Package.swift b/Package.swift
@@ -90,6 +90,9 @@ let package = Package(
"State/TeraConfigurationStore.swift",
"State/TeraIdentityStore.swift",
"State/TeraMediaStore.swift",
+ "State/TeraMediaPresentationLimits.swift",
+ "State/TeraMediaWorkQueue.swift",
+ "State/TeraMediaThumbnail.swift",
"State/TeraSessionStore.swift",
"State/TeraSessionBootstrap.swift",
"State/TeraSupportingStores.swift",
@@ -105,6 +108,7 @@ let package = Package(
"Views/TeraTodayView.swift",
"Views/TeraTodayCurrentDetail.swift",
"Views/TeraTodayDetailView.swift",
+ "Views/TeraTodayCardPresentation.swift",
"Views/TeraTodayStatusView.swift",
"Views/TeraTodayPagingStatusView.swift",
"Views/RuntimeStatusView.swift",
diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
095FBECA7268AA594F614EFA /* TeraScopeBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED6D3E35C3235B6068E1902 /* TeraScopeBackend.swift */; };
141327BC6BAE2CFA2C62C584 /* TeraTodayReconciliationTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB21C33D6173E4FED05FDA0 /* TeraTodayReconciliationTaskTests.swift */; };
+ 17805D95512D7144DAC59D85 /* TeraTodayRenderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7283D1C8F6E90A236BC84AF6 /* TeraTodayRenderingTests.swift */; };
1D45B3D566A0A60063190854 /* TeraApp in Frameworks */ = {isa = PBXBuildFile; productRef = E666175A800272732A27CBEB /* TeraApp */; };
229C9AC991E67663DA53D339 /* TeraRuntimeBoundedTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A4FFC0023C620B68D0EC0ED /* TeraRuntimeBoundedTaskTests.swift */; };
2479571FA99A7962794999C4 /* TeraSupportingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0636AEDEB50DDDD328374A9 /* TeraSupportingStoreTests.swift */; };
@@ -39,6 +40,7 @@
827D8CC5C0488F0D5D15CE99 /* TeraMediaOwnershipFFITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2521493B0CD2B82445974E81 /* TeraMediaOwnershipFFITests.swift */; };
8422EAEECA73BB72974D002A /* TeraTodayBackfillTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D3FEE2830E056E82AF05FD /* TeraTodayBackfillTests.swift */; };
852FF7514FF7F682A246FB47 /* TeraStateMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E969BCFF7CED33A1D4A040 /* TeraStateMigrationTests.swift */; };
+ 85404A5125CF3E64E6009DC1 /* TeraMediaBudgetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F596D3A9B9E236963DCE15D7 /* TeraMediaBudgetTests.swift */; };
912C8743116D929BB6A02778 /* TeraRuntimeClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB77C087F15C2F42CD959607 /* TeraRuntimeClientTests.swift */; };
9384AAECB22885E2D05C54A8 /* TeraScopedMediaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFBAA57ABEAA4B84C73EE8E8 /* TeraScopedMediaTests.swift */; };
9995BC15D67E1C9C29B4A7DC /* TeraTodayContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFECCBF9A84D1D65544C6094 /* TeraTodayContextTests.swift */; };
@@ -118,6 +120,7 @@
6967B3EAC4CF1A410B26DD1A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
6B8677670E203FF68EC88983 /* TeraRootShellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRootShellTests.swift; sourceTree = "<group>"; };
714B4DD70B881D29A89812C9 /* TeraTodayReconciliationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayReconciliationTests.swift; sourceTree = "<group>"; };
+ 7283D1C8F6E90A236BC84AF6 /* TeraTodayRenderingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayRenderingTests.swift; sourceTree = "<group>"; };
73EBD10E201ECA26DCED523F /* TeraRuntimeInvalidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeInvalidationTests.swift; sourceTree = "<group>"; };
74FCF1C4A8A7137623BBE223 /* TeraOpenedMediaCloseFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraOpenedMediaCloseFixture.swift; sourceTree = "<group>"; };
794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayStoreTests.swift; sourceTree = "<group>"; };
@@ -154,6 +157,7 @@
EFBAA57ABEAA4B84C73EE8E8 /* TeraScopedMediaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopedMediaTests.swift; sourceTree = "<group>"; };
EFECCBF9A84D1D65544C6094 /* TeraTodayContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayContextTests.swift; sourceTree = "<group>"; };
F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceFixtures.swift; sourceTree = "<group>"; };
+ F596D3A9B9E236963DCE15D7 /* TeraMediaBudgetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaBudgetTests.swift; sourceTree = "<group>"; };
F65B01F6F5F7E125148EC4FD /* TeraTodayReconciliationFFITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayReconciliationFFITests.swift; sourceTree = "<group>"; };
FB77C087F15C2F42CD959607 /* TeraRuntimeClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeClientTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -227,6 +231,7 @@
2A506546B0EC1792240C36F7 /* TeraClockTests.swift */,
93A3358AFC23C567DB9CB02F /* TeraErrorRecoveryTests.swift */,
C890BDE885536BC2AB215393 /* TeraLifecycleTests.swift */,
+ F596D3A9B9E236963DCE15D7 /* TeraMediaBudgetTests.swift */,
D3C914B349DAE2EF66658229 /* TeraMediaFileFixture.swift */,
2521493B0CD2B82445974E81 /* TeraMediaOwnershipFFITests.swift */,
61B6F0B16BFD8F046E9C163F /* TeraMediaStoreTests.swift */,
@@ -267,6 +272,7 @@
F65B01F6F5F7E125148EC4FD /* TeraTodayReconciliationFFITests.swift */,
5EB21C33D6173E4FED05FDA0 /* TeraTodayReconciliationTaskTests.swift */,
714B4DD70B881D29A89812C9 /* TeraTodayReconciliationTests.swift */,
+ 7283D1C8F6E90A236BC84AF6 /* TeraTodayRenderingTests.swift */,
794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */,
5854504D0CAD5ABC29302292 /* TeraTodaySyncFixtures.swift */,
9BC55E41FBE589010038FDA5 /* TeraTodaySyncReceiptTests.swift */,
@@ -487,6 +493,7 @@
70417FB635F06CC9829FB38C /* TeraClockTests.swift in Sources */,
D918A664D9A4B9859A1A6503 /* TeraErrorRecoveryTests.swift in Sources */,
671EF50DB4EFBC25AABF26DC /* TeraLifecycleTests.swift in Sources */,
+ 85404A5125CF3E64E6009DC1 /* TeraMediaBudgetTests.swift in Sources */,
D64AC2FDC6FE05A39114D2DB /* TeraMediaFileFixture.swift in Sources */,
827D8CC5C0488F0D5D15CE99 /* TeraMediaOwnershipFFITests.swift in Sources */,
CDBE38E6B521611436F28858 /* TeraMediaStoreTests.swift in Sources */,
@@ -527,6 +534,7 @@
C0C7D6F0CBCE262C348D3C3E /* TeraTodayReconciliationFFITests.swift in Sources */,
141327BC6BAE2CFA2C62C584 /* TeraTodayReconciliationTaskTests.swift in Sources */,
D86A417A36A2186AB7777753 /* TeraTodayReconciliationTests.swift in Sources */,
+ 17805D95512D7144DAC59D85 /* TeraTodayRenderingTests.swift in Sources */,
64D49ADFD3FDD54CBA509DC5 /* TeraTodayStoreTests.swift in Sources */,
E4E85524E39F13D6C76650F1 /* TeraTodaySyncFixtures.swift in Sources */,
DBA4B403D65289242AFDDB07 /* TeraTodaySyncReceiptTests.swift in Sources */,
diff --git a/Tera/State/TeraMediaPresentationLimits.swift b/Tera/State/TeraMediaPresentationLimits.swift
@@ -0,0 +1,46 @@
+import Foundation
+
+/// Presentation budgets only. Eviction never mutates an authored artifact.
+struct TeraMediaPresentationLimits: Sendable, Equatable {
+ let encodedBytes: Int
+ let sourcePixels: Int
+ let thumbnailDimension: Int
+ let cacheBytes: Int
+ let cacheEntries: Int
+ let workers: Int
+ let queuedRequests: Int
+ let visibilityEntries: Int
+
+ static let standard: Self = {
+ guard let limits = Self(encodedBytes: 8 * 1024 * 1024, sourcePixels: 16_000_000,
+ thumbnailDimension: 1024, cacheBytes: 32 * 1024 * 1024, cacheEntries: 64,
+ workers: 2, queuedRequests: 16, visibilityEntries: 4096)
+ else {
+ preconditionFailure("Bundled media presentation limits must be valid")
+ }
+ return limits
+ }()
+
+ init?(encodedBytes: Int, sourcePixels: Int, thumbnailDimension: Int, cacheBytes: Int,
+ cacheEntries: Int, workers: Int, queuedRequests: Int, visibilityEntries: Int)
+ {
+ guard (1 ... 8 * 1024 * 1024).contains(encodedBytes),
+ (1 ... 16_000_000).contains(sourcePixels), (1 ... 1024).contains(thumbnailDimension),
+ (1 ... 32 * 1024 * 1024).contains(cacheBytes), (1 ... 64).contains(cacheEntries),
+ (1 ... 2).contains(workers), (1 ... 16).contains(queuedRequests),
+ (1 ... 4096).contains(visibilityEntries), workers + queuedRequests <= cacheEntries else { return nil }
+ self.encodedBytes = encodedBytes
+ self.sourcePixels = sourcePixels
+ self.thumbnailDimension = thumbnailDimension
+ self.cacheBytes = cacheBytes
+ self.cacheEntries = cacheEntries
+ self.workers = workers
+ self.queuedRequests = queuedRequests
+ self.visibilityEntries = visibilityEntries
+ }
+
+ func accepts(byteCount: Int, width: Int, height: Int) -> Bool {
+ byteCount > 0 && byteCount <= encodedBytes && width > 0 && height > 0
+ && width <= sourcePixels / height
+ }
+}
diff --git a/Tera/State/TeraMediaStore.swift b/Tera/State/TeraMediaStore.swift
@@ -36,34 +36,50 @@ final class TeraMediaStore: ObservableObject {
let reference: String
}
- private struct Work {
- let id: UUID
- let task: Task<Void, Never>
+ private struct CachedImage {
+ let image: UIImage
+ let cost: Int
}
@Published private var states: [Key: TeraMediaPresentationState] = [:]
-
private let runtimeClient: TeraRuntimeClient
- private var tasks: [Key: Work] = [:]
+ private let limits: TeraMediaPresentationLimits
+ private let work: TeraMediaWorkQueue
+ private var owners: [Key: UUID] = [:]
+ private var images: [Key: CachedImage] = [:]
+ private var recent: [Key] = []
private var revoked = Set<Key>()
+ private var visibilityOverflow = false
+ private var reauthorized = Set<Key>()
private var configuration: TeraPresentationConfiguration?
- init(runtimeClient: TeraRuntimeClient) {
+ init(runtimeClient: TeraRuntimeClient, limits: TeraMediaPresentationLimits = .standard) {
self.runtimeClient = runtimeClient
+ self.limits = limits
+ work = TeraMediaWorkQueue(limits: limits)
}
- deinit {
- for task in tasks.values {
- task.task.cancel()
- }
+ var cachedByteCount: Int {
+ images.values.reduce(0) { $0 + $1.cost }
}
- func state(
- for media: TeraMediaReference,
- context: TeraLocalNetwork?
- ) -> TeraMediaPresentationState {
+ var stateCount: Int {
+ states.count
+ }
+
+ func image(for media: TeraMediaReference, context: TeraLocalNetwork?) -> UIImage? {
+ guard let context else { return nil }
+ let key = key(media: media, context: context)
+ guard allowed(key) else { return nil }
+ touch(key)
+ return images[key]?.image
+ }
+
+ func state(for media: TeraMediaReference, context: TeraLocalNetwork?) -> TeraMediaPresentationState {
guard let context else { return .unavailable }
- if let state = states[key(media: media, context: context)] {
+ let key = key(media: media, context: context)
+ guard allowed(key) else { return .unavailable }
+ if let state = states[key] {
return state
}
switch media.verification {
@@ -76,22 +92,14 @@ final class TeraMediaStore: ObservableObject {
func load(media: TeraMediaReference, context: TeraLocalNetwork?) {
guard let context else { return }
let key = key(media: media, context: context)
- guard !revoked.contains(key), states[key] == nil, tasks[key] == nil else { return }
+ guard allowed(key), states[key] == nil, owners[key] == nil else { return }
switch media.verification {
- case .pending:
- states[key] = .pending
- case .failed:
- states[key] = .failed
+ case .pending: set(.pending, for: key)
+ case .failed: set(.failed, for: key)
case .verified:
- guard let artifactID = media.verifiedArtifactID else {
- states[key] = .corrupt
- return
- }
+ guard let artifactID = media.verifiedArtifactID else { set(.corrupt, for: key); return }
start(key: key, context: context) { [runtimeClient] in
- try await runtimeClient.verifiedMediaArtifact(
- context: context,
- artifactID: artifactID
- )
+ try await runtimeClient.verifiedMediaArtifact(context: context, artifactID: artifactID)
}
case .unavailable:
start(key: key, context: context) { [runtimeClient] in
@@ -103,40 +111,58 @@ final class TeraMediaStore: ObservableObject {
func retry(media: TeraMediaReference, context: TeraLocalNetwork?) {
guard let context else { return }
let key = key(media: media, context: context)
- guard !revoked.contains(key) else { return }
- tasks[key]?.task.cancel()
- tasks[key] = nil
- states[key] = nil
+ guard allowed(key) else { return }
+ cancel(key)
start(key: key, context: context) { [runtimeClient] in
try await runtimeClient.retrieveMedia(context: context, reference: media)
}
}
- func reconcileVisibility(
- previous: [TeraMediaReference], current: [TeraMediaReference], context: TeraLocalNetwork?
- ) {
+ func reconcileVisibility(previous: [TeraMediaReference], current: [TeraMediaReference],
+ context: TeraLocalNetwork?)
+ {
guard let context else { return }
- let allowed = Set(current.map { key(media: $0, context: context) })
- for key in allowed where revoked.remove(key) != nil {
- states[key] = nil
+ let permitted = Set(current.prefix(limits.visibilityEntries).map { key(media: $0, context: context) })
+ if current.count > limits.visibilityEntries {
+ visibilityOverflow = true
+ }
+ reauthorized = permitted
+ for key in permitted where revoked.remove(key) != nil {
+ remove(key)
}
for reference in previous {
let key = key(media: reference, context: context)
- guard !allowed.contains(key) else { continue }
- revoked.insert(key)
- tasks[key]?.task.cancel()
- tasks[key] = nil
- states[key] = .unavailable
+ guard !permitted.contains(key) else { continue }
+ if !revoked.contains(key) {
+ if revoked.count < limits.visibilityEntries {
+ revoked.insert(key)
+ } else {
+ visibilityOverflow = true
+ }
+ }
+ cancel(key)
+ }
+ // Under admission pressure only the bounded, explicitly current window may
+ // load. Forgetting an old denial must never authorize a stale reference.
+ if visibilityOverflow {
+ for key in Array(owners.keys) where !allowed(key) {
+ cancel(key)
+ }
+ for key in Array(states.keys) where !allowed(key) {
+ remove(key)
+ }
}
}
func reset() {
- for task in tasks.values {
- task.task.cancel()
- }
- tasks.removeAll(keepingCapacity: false)
- states.removeAll(keepingCapacity: false)
- revoked.removeAll(keepingCapacity: false)
+ work.cancelAll()
+ owners.removeAll()
+ states.removeAll()
+ images.removeAll()
+ recent.removeAll()
+ revoked.removeAll()
+ reauthorized.removeAll()
+ visibilityOverflow = false
}
func configure(snapshot: TeraRuntimeSnapshot) {
@@ -146,46 +172,93 @@ final class TeraMediaStore: ObservableObject {
configuration = updated
}
- private func start(
- key: Key,
- context: TeraLocalNetwork,
- operation: @escaping @Sendable () async throws -> TeraVerifiedMediaArtifact?
- ) {
- states[key] = .loading
+ private func start(key: Key, context: TeraLocalNetwork,
+ operation: @escaping @Sendable () async throws -> TeraVerifiedMediaArtifact?)
+ {
let id = UUID()
- let task = Task { [weak self] in
+ owners[key] = id
+ set(.loading, for: key)
+ let accepted = work.submit(id: id) { [weak self, runtimeClient, limits] in
do {
- let artifact = try await operation()
- guard let self, isCurrent(key: key, id: id) else { return }
- guard let artifact else {
- complete(key: key, id: id, state: .unavailable)
+ try Task.checkCancellation()
+ guard let artifact = try await operation() else {
+ self?.complete(key: key, id: id, state: .unavailable)
return
}
- guard UIImage(data: artifact.bytes) != nil else {
- _ = try? await runtimeClient.invalidateMediaArtifact(
- context: context, artifactID: artifact.artifactID
- )
- complete(key: key, id: id, state: .corrupt)
- return
+ guard self?.isCurrent(key, id) == true else { return }
+ do {
+ let image = try await TeraMediaThumbnail.prepare(artifact, limits: limits)
+ guard let self, isCurrent(key, id) else { return }
+ let cost = artifact.bytes.count + image.bytesPerRow * image.height
+ guard cost <= limits.cacheBytes else {
+ complete(key: key, id: id, state: .unavailable)
+ return
+ }
+ images[key] = CachedImage(image: UIImage(cgImage: image), cost: cost)
+ complete(key: key, id: id, state: .ready(artifact))
+ } catch TeraMediaThumbnailFailure.corrupt {
+ guard self?.isCurrent(key, id) == true else { return }
+ _ = try? await runtimeClient.invalidateMediaArtifact(context: context, artifactID: artifact.artifactID)
+ self?.complete(key: key, id: id, state: .corrupt)
+ } catch TeraMediaThumbnailFailure.resourceLimit {
+ self?.complete(key: key, id: id, state: .unavailable)
}
- complete(key: key, id: id, state: .ready(artifact))
} catch is CancellationError {
self?.complete(key: key, id: id, state: nil)
} catch {
self?.complete(key: key, id: id, state: Self.failureState(error))
}
}
- tasks[key] = Work(id: id, task: task)
+ if !accepted {
+ owners[key] = nil; set(.unavailable, for: key)
+ }
}
- private func isCurrent(key: Key, id: UUID) -> Bool {
- tasks[key]?.id == id && !Task.isCancelled
+ private func isCurrent(_ key: Key, _ id: UUID) -> Bool {
+ owners[key] == id && allowed(key) && !Task.isCancelled
}
private func complete(key: Key, id: UUID, state: TeraMediaPresentationState?) {
- guard tasks[key]?.id == id else { return }
- tasks[key] = nil
- states[key] = Task.isCancelled ? nil : state
+ guard owners[key] == id else { return }
+ owners[key] = nil
+ if Task.isCancelled || !allowed(key) {
+ remove(key)
+ } else {
+ set(state, for: key)
+ }
+ }
+
+ private func allowed(_ key: Key) -> Bool {
+ visibilityOverflow ? reauthorized.contains(key) : !revoked.contains(key)
+ }
+
+ private func cancel(_ key: Key) {
+ if let id = owners.removeValue(forKey: key) {
+ work.cancel(id: id)
+ }
+ remove(key)
+ }
+
+ private func remove(_ key: Key) {
+ states[key] = nil
+ images[key] = nil
+ recent.removeAll { $0 == key }
+ }
+
+ private func touch(_ key: Key) {
+ guard states[key] != nil else { return }
+ recent.removeAll { $0 == key }
+ recent.append(key)
+ }
+
+ private func set(_ state: TeraMediaPresentationState?, for key: Key) {
+ guard let state else { remove(key); return }
+ states[key] = state
+ touch(key)
+ while states.count > limits.cacheEntries || cachedByteCount > limits.cacheBytes {
+ guard let oldest = recent.first(where: { owners[$0] == nil }) else { break }
+ remove(oldest)
+ }
}
private func key(media: TeraMediaReference, context: TeraLocalNetwork) -> Key {
diff --git a/Tera/State/TeraMediaThumbnail.swift b/Tera/State/TeraMediaThumbnail.swift
@@ -0,0 +1,57 @@
+import Foundation
+import ImageIO
+
+nonisolated enum TeraMediaThumbnailFailure: Error {
+ case corrupt
+ case resourceLimit
+}
+
+nonisolated enum TeraMediaThumbnail {
+ /// The caller owns this task and its worker slot through decode completion.
+ static func prepare(_ artifact: TeraVerifiedMediaArtifact,
+ limits: TeraMediaPresentationLimits) async throws -> CGImage
+ {
+ let task = Task.detached(priority: .utility) {
+ try decode(artifact, limits: limits)
+ }
+ return try await withTaskCancellationHandler {
+ try await task.value
+ } onCancel: {
+ task.cancel()
+ }
+ }
+
+ private static func decode(_ artifact: TeraVerifiedMediaArtifact,
+ limits: TeraMediaPresentationLimits) throws -> CGImage
+ {
+ assert(!Thread.isMainThread, "Raster decoding must stay off the UI thread")
+ try Task.checkCancellation()
+ guard limits.accepts(byteCount: artifact.bytes.count, width: Int(artifact.width),
+ height: Int(artifact.height)) else { throw TeraMediaThumbnailFailure.resourceLimit }
+ guard let source = CGImageSourceCreateWithData(artifact.bytes as CFData,
+ [kCGImageSourceShouldCache: false] as CFDictionary),
+ let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],
+ let width = properties[kCGImagePropertyPixelWidth] as? Int,
+ let height = properties[kCGImagePropertyPixelHeight] as? Int,
+ width == Int(artifact.width), height == Int(artifact.height)
+ else {
+ throw TeraMediaThumbnailFailure.corrupt
+ }
+ guard limits.accepts(byteCount: artifact.bytes.count, width: width, height: height) else {
+ throw TeraMediaThumbnailFailure.resourceLimit
+ }
+ let options: [CFString: Any] = [
+ kCGImageSourceCreateThumbnailFromImageAlways: true,
+ kCGImageSourceThumbnailMaxPixelSize: limits.thumbnailDimension,
+ kCGImageSourceCreateThumbnailWithTransform: true,
+ kCGImageSourceShouldCacheImmediately: true,
+ ]
+ guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary),
+ image.width <= limits.thumbnailDimension, image.height <= limits.thumbnailDimension
+ else {
+ throw TeraMediaThumbnailFailure.corrupt
+ }
+ try Task.checkCancellation()
+ return image
+ }
+}
diff --git a/Tera/State/TeraMediaWorkQueue.swift b/Tera/State/TeraMediaWorkQueue.swift
@@ -0,0 +1,61 @@
+import Foundation
+
+/// Cancelled work retains its physical slot until its operation returns.
+@MainActor
+final class TeraMediaWorkQueue {
+ private struct Pending {
+ let id: UUID
+ let operation: @MainActor () async -> Void
+ }
+
+ private let limits: TeraMediaPresentationLimits
+ private var active: [UUID: Task<Void, Never>] = [:]
+ private var pending: [Pending] = []
+
+ init(limits: TeraMediaPresentationLimits) {
+ self.limits = limits
+ }
+
+ deinit { for task in active.values {
+ task.cancel()
+ } }
+
+ var activeCount: Int {
+ active.count
+ }
+
+ var queuedCount: Int {
+ pending.count
+ }
+
+ func submit(id: UUID, operation: @escaping @MainActor () async -> Void) -> Bool {
+ guard active[id] == nil, !pending.contains(where: { $0.id == id }),
+ active.count < limits.workers || pending.count < limits.queuedRequests else { return false }
+ pending.append(Pending(id: id, operation: operation))
+ drain()
+ return true
+ }
+
+ func cancel(id: UUID) {
+ pending.removeAll { $0.id == id }
+ active[id]?.cancel()
+ }
+
+ func cancelAll() {
+ pending.removeAll()
+ for task in active.values {
+ task.cancel()
+ }
+ }
+
+ private func drain() {
+ while active.count < limits.workers, !pending.isEmpty {
+ let work = pending.removeFirst()
+ active[work.id] = Task { [weak self] in
+ await work.operation()
+ self?.active[work.id] = nil
+ self?.drain()
+ }
+ }
+ }
+}
diff --git a/Tera/Views/TeraTodayCardPresentation.swift b/Tera/Views/TeraTodayCardPresentation.swift
@@ -0,0 +1,69 @@
+import Foundation
+
+enum TeraTodayCardPresentation {
+ case feed
+ case detail
+
+ static let excerptCharacters = 640
+ static let excerptBytes = 4096
+ static let labelCharacters = 160
+ static let labelBytes = 1024
+ static let thumbnails = 3
+
+ func content(_ value: String) -> String {
+ self == .detail ? value : Self.excerpt(value, characters: Self.excerptCharacters, bytes: Self.excerptBytes)
+ }
+
+ func label(_ value: String) -> String {
+ self == .detail ? value : Self.excerpt(value, characters: Self.labelCharacters, bytes: Self.labelBytes)
+ }
+
+ func media(_ values: [TeraMediaReference]) -> [TeraMediaReference] {
+ self == .detail ? values : Array(values.prefix(Self.thumbnails))
+ }
+
+ func accessibility(_ card: TeraTodayCard) -> String {
+ if self == .detail {
+ return card.accessibilitySummary
+ }
+ var parts = [card.type.label, "by \(label(card.authorName))"]
+ if let title = card.title {
+ parts.append(label(title))
+ }
+ parts.append(content(card.content))
+ if let price = card.priceSummary {
+ parts.append(label(price))
+ }
+ if card.lifecycle != .active {
+ parts.append(card.lifecycle.rawValue)
+ }
+ if let operation = card.localOperationState {
+ parts.append(label(operation))
+ }
+ return parts.joined(separator: ", ")
+ }
+
+ /// Bound bytes before walking graphemes: a single combining sequence can be
+ /// arbitrarily long. Drop an incomplete final cluster rather than split it.
+ static func excerpt(_ value: String, characters: Int, bytes: Int) -> String {
+ precondition(characters > 0 && bytes >= 3)
+ let prefix = Array(value.utf8.prefix(bytes + 1))
+ let byteTruncated = prefix.count > bytes
+ var bounded: String
+ if byteTruncated {
+ var encoded = Array(prefix.prefix(bytes - 3))
+ while String(bytes: encoded, encoding: .utf8) == nil {
+ encoded.removeLast()
+ }
+ bounded = String(bytes: encoded, encoding: .utf8) ?? ""
+ if !bounded.isEmpty {
+ bounded.removeLast()
+ }
+ } else {
+ bounded = value
+ }
+ let clusterPrefix = bounded.prefix(characters + 1)
+ let truncated = byteTruncated || clusterPrefix.count > characters
+ return truncated ? String(clusterPrefix.prefix(characters - 1)) + "…" : String(clusterPrefix)
+ }
+}
diff --git a/Tera/Views/TeraTodayDetailView.swift b/Tera/Views/TeraTodayDetailView.swift
@@ -13,7 +13,7 @@ struct TeraTodayDetailView: View {
var body: some View {
List {
Section {
- TeraTodayCardView(card: card, context: context, mediaStore: mediaStore)
+ TeraTodayCardView(card: card, context: context, mediaStore: mediaStore, presentation: .detail)
}
if !card.thread.isEmpty {
Section("Conversation") {
diff --git a/Tera/Views/TeraTodayView.swift b/Tera/Views/TeraTodayView.swift
@@ -194,10 +194,12 @@ struct TeraTodayCardView: View {
let context: TeraLocalNetwork?
@ObservedObject var mediaStore: TeraMediaStore
+ var presentation: TeraTodayCardPresentation = .feed
+
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .firstTextBaseline) {
- Text(card.authorName)
+ Text(presentation.label(card.authorName))
.font(.subheadline.weight(.semibold))
Spacer()
Text(card.type.label)
@@ -208,11 +210,11 @@ struct TeraTodayCardView: View {
}
if let title = card.title {
- Text(title)
+ Text(presentation.label(title))
.font(.headline)
}
if !card.content.isEmpty {
- Text(card.content)
+ Text(presentation.content(card.content))
.font(card.type == .ask ? .headline : .body)
}
@@ -223,7 +225,7 @@ struct TeraTodayCardView: View {
foodMetadata
}
- ForEach(card.media) { media in
+ ForEach(presentation.media(card.media)) { media in
TeraTrustedMediaView(media: media, context: context, store: mediaStore)
}
@@ -246,7 +248,7 @@ struct TeraTodayCardView: View {
}
.padding(.vertical, 8)
.accessibilityElement(children: .combine)
- .accessibilityLabel(card.accessibilitySummary)
+ .accessibilityLabel(presentation.accessibility(card))
}
private var eventMetadata: some View {
@@ -260,7 +262,7 @@ struct TeraTodayCardView: View {
)
}
if let location = card.location {
- Label(location, systemImage: "mappin.and.ellipse")
+ Label(presentation.label(location), systemImage: "mappin.and.ellipse")
}
}
.font(.subheadline)
@@ -281,13 +283,13 @@ struct TeraTodayCardView: View {
@ViewBuilder
private var foodMetadataLabels: some View {
if let price = card.priceSummary {
- Label(price, systemImage: "tag")
+ Label(presentation.label(price), systemImage: "tag")
}
if let quantity = card.quantity, let unit = card.priceUnit {
- Label("\(quantity) \(unit) available", systemImage: "basket")
+ Label(presentation.label("\(quantity) \(unit) available"), systemImage: "basket")
}
if let location = card.location {
- Label(location, systemImage: "mappin.and.ellipse")
+ Label(presentation.label(location), systemImage: "mappin.and.ellipse")
}
}
}
@@ -314,8 +316,8 @@ struct TeraLocalMediaContent: View {
let state = store.state(for: media, context: context)
Group {
switch state {
- case let .ready(artifact):
- if let image = UIImage(data: artifact.bytes) {
+ case .ready:
+ if let image = store.image(for: media, context: context) {
Image(uiImage: image)
.resizable()
.scaledToFill()
@@ -366,6 +368,6 @@ struct TeraLocalMediaContent: View {
private func accessibilityLabel(for state: TeraMediaPresentationState) -> String {
guard let alt = media.alt, !alt.isEmpty else { return state.accessibilityLabel }
- return "\(alt). \(state.accessibilityLabel)"
+ return "\(TeraTodayCardPresentation.feed.label(alt)). \(state.accessibilityLabel)"
}
}
diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json
@@ -22,9 +22,9 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 71475528,
+ "bytes": 71483336,
"path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a",
- "sha256": "fc75faf80679711dc0826952a756e15a55749c03b0297982fbe99c24983bea1a"
+ "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff"
},
{
"bytes": 71393,
@@ -37,9 +37,9 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 71528104,
+ "bytes": 71534912,
"path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a",
- "sha256": "25c2115bfd6c7f581111c6b20bab43bdc9b514cbd82aca0c7717796393345967"
+ "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae"
},
{
"bytes": 42544,
@@ -79,32 +79,32 @@
{
"bytes": 19948832,
"path": "native/aarch64-apple-darwin/libtera_ffi.dylib",
- "sha256": "18f8312d4ec4e6f45aca1b5f6698ae1cb13cb7957ce064028aff529c64e5f402"
+ "sha256": "eb9be3233d95cfce09cc7e64a84e7a5b9d83d10ae1132efb57263c02165441e3"
},
{
- "bytes": 71475528,
+ "bytes": 71483336,
"path": "native/aarch64-apple-ios-sim/libtera_ffi.a",
- "sha256": "fc75faf80679711dc0826952a756e15a55749c03b0297982fbe99c24983bea1a"
+ "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff"
},
{
- "bytes": 71528104,
+ "bytes": 71534912,
"path": "native/aarch64-apple-ios/libtera_ffi.a",
- "sha256": "25c2115bfd6c7f581111c6b20bab43bdc9b514cbd82aca0c7717796393345967"
+ "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae"
},
{
- "bytes": 66342,
+ "bytes": 66937,
"path": "source/aarch64-apple-darwin.json",
- "sha256": "402223883665871ba2fb4a229a5483a1d52948f7e0855dca3436368683ac5f61"
+ "sha256": "06721ef698bd79864cc5a690d0bd027ea66cd6079b739b834a79c143abf65170"
},
{
- "bytes": 66186,
+ "bytes": 66781,
"path": "source/aarch64-apple-ios-sim.json",
- "sha256": "5eb6c319a529465870f04ca46f7e247c8694e5f5b4f14562d951ab58cbe26f62"
+ "sha256": "e26e968883e963e27ec3dbe1b077d9d85f818a894951bd643902ea6c81e11219"
},
{
- "bytes": 66182,
+ "bytes": 66777,
"path": "source/aarch64-apple-ios.json",
- "sha256": "764e8b804218f1bd1b7993f42a31ae621f8c3ccb32a9ce2f0adf1b363d6cc11f"
+ "sha256": "9c651b6591d8d0caf19970ed2fddb9eb53132abc749a310e99896b570458386d"
}
],
"language": "swift",
@@ -112,7 +112,7 @@
"schema": "radroots.artifact-manifest.v2",
"source": {
"repository": "https://github.com/radrootslabs/tera",
- "tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e"
+ "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd"
},
"source_records": {
"aarch64-apple-darwin": "source/aarch64-apple-darwin.json",
@@ -139,9 +139,9 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 71475528,
+ "bytes": 71483336,
"path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a",
- "sha256": "fc75faf80679711dc0826952a756e15a55749c03b0297982fbe99c24983bea1a"
+ "sha256": "df300d70e6f32b68f5ac4366b3342a692c8d8a3ba243965d9c9ad60634d897ff"
},
{
"bytes": 71393,
@@ -154,9 +154,9 @@
"sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c"
},
{
- "bytes": 71528104,
+ "bytes": 71534912,
"path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a",
- "sha256": "25c2115bfd6c7f581111c6b20bab43bdc9b514cbd82aca0c7717796393345967"
+ "sha256": "b6a0d44f19783376dd493b50bdc06ed61baf76c9e7a9b47b730a55198c5d22ae"
},
{
"bytes": 512103,
@@ -174,19 +174,19 @@
"sha256": "4cc03b03dd54db7b4ab22684ff819233444ae0a43a770ab875148892bed1e3f2"
},
{
- "bytes": 66342,
+ "bytes": 66937,
"path": "TeraFFI/source/aarch64-apple-darwin.json",
- "sha256": "402223883665871ba2fb4a229a5483a1d52948f7e0855dca3436368683ac5f61"
+ "sha256": "06721ef698bd79864cc5a690d0bd027ea66cd6079b739b834a79c143abf65170"
},
{
- "bytes": 66186,
+ "bytes": 66781,
"path": "TeraFFI/source/aarch64-apple-ios-sim.json",
- "sha256": "5eb6c319a529465870f04ca46f7e247c8694e5f5b4f14562d951ab58cbe26f62"
+ "sha256": "e26e968883e963e27ec3dbe1b077d9d85f818a894951bd643902ea6c81e11219"
},
{
- "bytes": 66182,
+ "bytes": 66777,
"path": "TeraFFI/source/aarch64-apple-ios.json",
- "sha256": "764e8b804218f1bd1b7993f42a31ae621f8c3ccb32a9ce2f0adf1b363d6cc11f"
+ "sha256": "9c651b6591d8d0caf19970ed2fddb9eb53132abc749a310e99896b570458386d"
}
],
"schema": "tera.installed-native-artifacts.v1"
diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock
@@ -1,7 +1,7 @@
schema = "tera.installed-source.v1"
repository = "https://github.com/radrootslabs/tera"
-source_tree = "3ff40c5420af5cfa8f2726bf65285c69a5a0386e"
-manifest_sha256 = "9061000a642f27b7ae4a4c8fb91e9d0be789ca4badedbecece5f18b0fdd3bd73"
+source_tree = "703c94b59618f9ca1dd16c17df8fb07fb7b860bd"
+manifest_sha256 = "f174e6196e6b8ffdadaabcf52c6d697ee7ba9e3acc55ff265cd928c1131d4010"
source_date_epoch = 1787871027
[foundation]
diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json
@@ -721,10 +721,10 @@
"sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4"
},
"core/crates/tera_core/src/runtime/product_surface/today.rs": {
- "bytes": 140031,
- "git_blob": "eaf6fd526b5d5ed8bc1d85f1d365689424bb12ed",
+ "bytes": 140918,
+ "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278",
"mode": "100644",
- "sha256": "6f5c6df78c457e87bc02fcf6759b241ee81bfdbb67ac4cd35b2a02bea4b350d2"
+ "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99"
},
"core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": {
"bytes": 14686,
@@ -744,17 +744,29 @@
"mode": "100644",
"sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6"
},
+ "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": {
+ "bytes": 17547,
+ "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790",
+ "mode": "100644",
+ "sha256": "31590402860e49ec6a980b5822ab129207852ba59d9516bd7d23952071fe2a88"
+ },
"core/crates/tera_core/src/runtime/product_surface/today_paging_scope.rs": {
"bytes": 1703,
"git_blob": "cc1fb36406b9ad236a5653bbd59d8256dd91bd19",
"mode": "100644",
"sha256": "e9ea1a0ee6c52031895184cd0a2b3c526318fd521ab2bd6b1b08ef53f3c9ff66"
},
+ "core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs": {
+ "bytes": 5938,
+ "git_blob": "70a56d607ba4e59305e4f7488b20e509764fbee8",
+ "mode": "100644",
+ "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c"
+ },
"core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": {
- "bytes": 2055,
- "git_blob": "d615a82f5e06bfcae0582bb2069b0cf168caee9d",
+ "bytes": 2036,
+ "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427",
"mode": "100644",
- "sha256": "df4e51923b6e352faf7ce6590b6e969e4853cb9d3d0a906892d0a6d50c60b51f"
+ "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de"
},
"core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": {
"bytes": 3424,
@@ -1429,13 +1441,13 @@
"sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 116498,
- "git_blob": "4a0af9069c1be7f07205d40c2272d7f1d417ddf4",
+ "bytes": 116926,
+ "git_blob": "c3a54831006843505ce452a194ab72352dbac12a",
"mode": "100644",
- "sha256": "3db541aae6722c0ebfacdbceec33a0c8415d17b9b915d8a3a3a97217ed7f52e5"
+ "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0"
}
},
"policy": "staged_inputs",
- "tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e"
+ "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd"
}
}
diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json
@@ -717,10 +717,10 @@
"sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4"
},
"core/crates/tera_core/src/runtime/product_surface/today.rs": {
- "bytes": 140031,
- "git_blob": "eaf6fd526b5d5ed8bc1d85f1d365689424bb12ed",
+ "bytes": 140918,
+ "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278",
"mode": "100644",
- "sha256": "6f5c6df78c457e87bc02fcf6759b241ee81bfdbb67ac4cd35b2a02bea4b350d2"
+ "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99"
},
"core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": {
"bytes": 14686,
@@ -740,17 +740,29 @@
"mode": "100644",
"sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6"
},
+ "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": {
+ "bytes": 17547,
+ "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790",
+ "mode": "100644",
+ "sha256": "31590402860e49ec6a980b5822ab129207852ba59d9516bd7d23952071fe2a88"
+ },
"core/crates/tera_core/src/runtime/product_surface/today_paging_scope.rs": {
"bytes": 1703,
"git_blob": "cc1fb36406b9ad236a5653bbd59d8256dd91bd19",
"mode": "100644",
"sha256": "e9ea1a0ee6c52031895184cd0a2b3c526318fd521ab2bd6b1b08ef53f3c9ff66"
},
+ "core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs": {
+ "bytes": 5938,
+ "git_blob": "70a56d607ba4e59305e4f7488b20e509764fbee8",
+ "mode": "100644",
+ "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c"
+ },
"core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": {
- "bytes": 2055,
- "git_blob": "d615a82f5e06bfcae0582bb2069b0cf168caee9d",
+ "bytes": 2036,
+ "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427",
"mode": "100644",
- "sha256": "df4e51923b6e352faf7ce6590b6e969e4853cb9d3d0a906892d0a6d50c60b51f"
+ "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de"
},
"core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": {
"bytes": 3424,
@@ -1425,13 +1437,13 @@
"sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 116498,
- "git_blob": "4a0af9069c1be7f07205d40c2272d7f1d417ddf4",
+ "bytes": 116926,
+ "git_blob": "c3a54831006843505ce452a194ab72352dbac12a",
"mode": "100644",
- "sha256": "3db541aae6722c0ebfacdbceec33a0c8415d17b9b915d8a3a3a97217ed7f52e5"
+ "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0"
}
},
"policy": "staged_inputs",
- "tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e"
+ "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd"
}
}
diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json
@@ -717,10 +717,10 @@
"sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4"
},
"core/crates/tera_core/src/runtime/product_surface/today.rs": {
- "bytes": 140031,
- "git_blob": "eaf6fd526b5d5ed8bc1d85f1d365689424bb12ed",
+ "bytes": 140918,
+ "git_blob": "c8be6532fdb641bb5d0056cad8e07bd7ed6ae278",
"mode": "100644",
- "sha256": "6f5c6df78c457e87bc02fcf6759b241ee81bfdbb67ac4cd35b2a02bea4b350d2"
+ "sha256": "1c76aafbd3d7ed373579a8658d47f9208059d2578db466e11d4148da663d4e99"
},
"core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": {
"bytes": 14686,
@@ -740,17 +740,29 @@
"mode": "100644",
"sha256": "6f5810256ca221ca66ec4056e07fc635103e0ed02da01f5a8c53161535d7eaf6"
},
+ "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs": {
+ "bytes": 17547,
+ "git_blob": "de61bd9c5fefa9cabf4818d84f9498564a6e5790",
+ "mode": "100644",
+ "sha256": "31590402860e49ec6a980b5822ab129207852ba59d9516bd7d23952071fe2a88"
+ },
"core/crates/tera_core/src/runtime/product_surface/today_paging_scope.rs": {
"bytes": 1703,
"git_blob": "cc1fb36406b9ad236a5653bbd59d8256dd91bd19",
"mode": "100644",
"sha256": "e9ea1a0ee6c52031895184cd0a2b3c526318fd521ab2bd6b1b08ef53f3c9ff66"
},
+ "core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs": {
+ "bytes": 5938,
+ "git_blob": "70a56d607ba4e59305e4f7488b20e509764fbee8",
+ "mode": "100644",
+ "sha256": "1158622c28cca9b6843b48199f4eb6d286a01cc15d7530e11b0fa25a2e00b93c"
+ },
"core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs": {
- "bytes": 2055,
- "git_blob": "d615a82f5e06bfcae0582bb2069b0cf168caee9d",
+ "bytes": 2036,
+ "git_blob": "a85b9bc3a43885415147fef2d9b9a57338ed7427",
"mode": "100644",
- "sha256": "df4e51923b6e352faf7ce6590b6e969e4853cb9d3d0a906892d0a6d50c60b51f"
+ "sha256": "2000088c53e5e34b95d88e6de1615631a3ecb2dabf36e831f504dd0791afc5de"
},
"core/crates/tera_core/src/runtime/product_surface/today_reconciliation_tests.rs": {
"bytes": 3424,
@@ -1425,13 +1437,13 @@
"sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d"
},
"test-fixtures/legacy-identifiers.v1.json": {
- "bytes": 116498,
- "git_blob": "4a0af9069c1be7f07205d40c2272d7f1d417ddf4",
+ "bytes": 116926,
+ "git_blob": "c3a54831006843505ce452a194ab72352dbac12a",
"mode": "100644",
- "sha256": "3db541aae6722c0ebfacdbceec33a0c8415d17b9b915d8a3a3a97217ed7f52e5"
+ "sha256": "20f5bc3bdcfaaa8eb6816543849e75107c498a52e45399f3f84314e5ecf6c8f0"
}
},
"policy": "staged_inputs",
- "tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e"
+ "tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd"
}
}
diff --git a/TeraTests/TeraMediaBudgetTests.swift b/TeraTests/TeraMediaBudgetTests.swift
@@ -0,0 +1,181 @@
+import Foundation
+@testable import TeraApp
+import UIKit
+import XCTest
+
+final class TeraMediaBudgetTests: XCTestCase {
+ func testStartupLimitsRejectZeroAndEveryMaximumPlusOne() throws {
+ let maximum = [8 * 1024 * 1024, 16_000_000, 1024, 32 * 1024 * 1024, 64, 2, 16, 4096]
+ func limits(_ values: [Int]) -> TeraMediaPresentationLimits? {
+ TeraMediaPresentationLimits(encodedBytes: values[0], sourcePixels: values[1],
+ thumbnailDimension: values[2], cacheBytes: values[3], cacheEntries: values[4],
+ workers: values[5], queuedRequests: values[6], visibilityEntries: values[7])
+ }
+ XCTAssertEqual(limits(maximum), .standard)
+ for index in maximum.indices {
+ var invalid = maximum
+ invalid[index] += 1
+ XCTAssertNil(limits(invalid))
+ invalid[index] = 0
+ XCTAssertNil(limits(invalid))
+ }
+ let policy = try XCTUnwrap(limits(maximum))
+ XCTAssertTrue(policy.accepts(byteCount: maximum[0], width: 4000, height: 4000))
+ XCTAssertFalse(policy.accepts(byteCount: maximum[0] + 1, width: 4000, height: 4000))
+ XCTAssertFalse(policy.accepts(byteCount: maximum[0], width: 16_000_001, height: 1))
+ XCTAssertFalse(policy.accepts(byteCount: 1, width: Int.max, height: Int.max))
+ }
+
+ @MainActor
+ func testDecodeUsesBoundedRasterOffUIActorAndRejectsCorruption() async throws {
+ let renderer = UIGraphicsImageRenderer(size: CGSize(width: 2048, height: 1024), format: {
+ let format = UIGraphicsImageRendererFormat()
+ format.scale = 1
+ return format
+ }())
+ let bytes = renderer.pngData { context in
+ UIColor.green.setFill()
+ context.fill(CGRect(x: 0, y: 0, width: 2048, height: 1024))
+ }
+ let artifact = try XCTUnwrap(TeraVerifiedMediaArtifact(artifactID: String(repeating: "a", count: 64),
+ bytes: bytes, byteSize: UInt64(bytes.count), mediaType: "image/png", width: 2048, height: 1024))
+ let image = try await TeraMediaThumbnail.prepare(artifact, limits: .standard)
+ XCTAssertEqual(image.width, 1024)
+ XCTAssertEqual(image.height, 512)
+ do {
+ _ = try await TeraMediaThumbnail.prepare(TeraScopeFixtures.artifact("a", corrupt: true), limits: .standard)
+ XCTFail("Corrupt raster must fail")
+ } catch TeraMediaThumbnailFailure.corrupt {}
+ }
+
+ func testActualDecoderRejectsEncodedAndDeclaredPixelMaximumPlusOne() async throws {
+ let original = try TeraScopeFixtures.artifact("a")
+ for (bytes, width, height) in [(Data(repeating: 0, count: 8 * 1024 * 1024 + 1), UInt32(1), UInt32(1)),
+ (original.bytes, UInt32(16_000_001), UInt32(1))]
+ {
+ let artifact = try XCTUnwrap(TeraVerifiedMediaArtifact(artifactID: original.artifactID,
+ bytes: bytes, byteSize: UInt64(bytes.count), mediaType: "image/png", width: width, height: height))
+ do {
+ _ = try await TeraMediaThumbnail.prepare(artifact, limits: .standard)
+ XCTFail("Resource admission must precede raster decoding")
+ } catch TeraMediaThumbnailFailure.resourceLimit {}
+ }
+ }
+
+ @MainActor
+ func testQueueMaxPlusOneAndCancellationKeepPhysicalWorkersBounded() async {
+ let queue = TeraMediaWorkQueue(limits: .standard)
+ let firstPause = ResourceTestPause()
+ let secondPause = ResourceTestPause()
+ let first = UUID()
+ var started = 0
+ XCTAssertTrue(queue.submit(id: first) { started += 1; await firstPause.wait() })
+ XCTAssertTrue(queue.submit(id: UUID()) { started += 1; await secondPause.wait() })
+ for _ in 0 ..< 16 {
+ XCTAssertTrue(queue.submit(id: UUID()) { started += 1 })
+ }
+ await firstPause.entered.wait()
+ await secondPause.entered.wait()
+ XCTAssertEqual(started, 2)
+ XCTAssertEqual(queue.activeCount, 2)
+ XCTAssertEqual(queue.queuedCount, 16)
+ XCTAssertFalse(queue.submit(id: UUID()) {})
+ queue.cancel(id: first)
+ XCTAssertEqual(queue.activeCount, 2)
+ XCTAssertFalse(queue.submit(id: UUID()) {})
+ queue.cancelAll()
+ XCTAssertEqual(queue.activeCount, 2)
+ XCTAssertEqual(queue.queuedCount, 0)
+ await firstPause.resume.open()
+ await secondPause.resume.open()
+ await TeraScopeFixtures.eventually { queue.activeCount == 0 }
+ XCTAssertEqual(started, 2)
+ }
+
+ @MainActor
+ func testPresentationCacheEvictsOnlyTransientBytesAtCapacity() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let policy = try XCTUnwrap(TeraMediaPresentationLimits(encodedBytes: 1024, sourcePixels: 16_000_000,
+ thumbnailDimension: 1024, cacheBytes: 200, cacheEntries: 3, workers: 1, queuedRequests: 1,
+ visibilityEntries: 4096))
+ let store = TeraMediaStore(runtimeClient: client, limits: policy)
+ let context = TeraLocalNetwork.defaultContext(snapshot: TeraScopeFixtures.snapshot())
+ for index in 0 ..< 4 {
+ var reference = TeraScopeFixtures.reference()
+ reference = TeraMediaReference(referenceFingerprint: String(repeating: String(index), count: 64),
+ url: reference.url, sha256: reference.sha256, mediaType: reference.mediaType,
+ width: reference.width, height: reference.height, byteSize: reference.byteSize,
+ alt: reference.alt, verification: reference.verification)
+ store.load(media: reference, context: context)
+ await TeraScopeFixtures.eventually { store.state(for: reference, context: context) != .loading }
+ XCTAssertLessThanOrEqual(store.stateCount, policy.cacheEntries)
+ XCTAssertLessThanOrEqual(store.cachedByteCount, policy.cacheBytes)
+ XCTAssertGreaterThan(store.cachedByteCount, 0)
+ }
+ let invalidations = await backend.counts[.invalidate]
+ XCTAssertNil(invalidations)
+ store.reset()
+ XCTAssertEqual(store.cachedByteCount, 0)
+ _ = try await client.stop()
+ }
+
+ @MainActor
+ func testVisibilityCapacityFailsClosedAndExplicitCurrentReferencesCanReturn() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let limits = try XCTUnwrap(TeraMediaPresentationLimits(encodedBytes: 1024,
+ sourcePixels: 16_000_000, thumbnailDimension: 1024, cacheBytes: 1024,
+ cacheEntries: 64, workers: 2, queuedRequests: 16, visibilityEntries: 2))
+ let store = TeraMediaStore(runtimeClient: client, limits: limits)
+ let context = TeraLocalNetwork.defaultContext(snapshot: TeraScopeFixtures.snapshot())
+ let references = (0 ..< 3).map { reference($0, verification: .unavailable) }
+ store.reconcileVisibility(previous: Array(references.prefix(2)), current: [], context: context)
+ store.reconcileVisibility(previous: [references[0]], current: [], context: context)
+ let artifact = try TeraScopeFixtures.artifact("a")
+ store.load(media: references[2], context: context)
+ await TeraScopeFixtures.eventually { store.state(for: references[2], context: context) == .ready(artifact) }
+ store.reconcileVisibility(previous: [references[2]], current: [], context: context)
+ for value in references {
+ store.load(media: value, context: context)
+ store.retry(media: value, context: context)
+ XCTAssertEqual(store.state(for: value, context: context), .unavailable)
+ }
+ let calls = await backend.counts[.media]
+ XCTAssertEqual(calls, 1)
+ store.reconcileVisibility(previous: [], current: [references[2]], context: context)
+ store.load(media: references[2], context: context)
+ await TeraScopeFixtures.eventually { store.state(for: references[2], context: context) == .ready(artifact) }
+ store.retry(media: references[0], context: context)
+ let finalCalls = await backend.counts[.media]
+ XCTAssertEqual(finalCalls, 2)
+ store.reset()
+ _ = try await client.stop()
+ }
+
+ @MainActor
+ func testPendingStateEntryMaximumPlusOneIsBounded() async throws {
+ let backend = try TeraScopeBackend()
+ let client = try await TeraScopeFixtures.client(backend)
+ let store = TeraMediaStore(runtimeClient: client)
+ let context = TeraLocalNetwork.defaultContext(snapshot: TeraScopeFixtures.snapshot())
+ for index in 0 ..< 64 {
+ store.load(media: reference(index, verification: .pending), context: context)
+ }
+ XCTAssertEqual(store.stateCount, 64)
+ store.load(media: reference(64, verification: .pending), context: context)
+ XCTAssertEqual(store.stateCount, 64)
+ let calls = await backend.counts[.media]
+ XCTAssertNil(calls)
+ store.reset()
+ _ = try await client.stop()
+ }
+
+ private func reference(_ index: Int, verification: TeraMediaVerificationState) -> TeraMediaReference {
+ let original = TeraScopeFixtures.reference()
+ return TeraMediaReference(referenceFingerprint: String(format: "%064x", index),
+ url: original.url, sha256: original.sha256, mediaType: original.mediaType,
+ width: original.width, height: original.height, byteSize: original.byteSize,
+ alt: original.alt, verification: verification)
+ }
+}
diff --git a/TeraTests/TeraTodayRenderingTests.swift b/TeraTests/TeraTodayRenderingTests.swift
@@ -0,0 +1,44 @@
+@testable import TeraApp
+import XCTest
+
+final class TeraTodayRenderingTests: XCTestCase {
+ func testExcerptCharacterAndByteLimitsPreserveFullDetail() {
+ let feed = TeraTodayCardPresentation.feed
+ let detail = TeraTodayCardPresentation.detail
+ let maximum = String(repeating: "a", count: 640)
+ XCTAssertEqual(feed.content(maximum), maximum)
+ XCTAssertEqual(feed.content(maximum + "b").count, 640)
+ XCTAssertTrue(feed.content(maximum + "b").hasSuffix("…"))
+ for value in [String(repeating: "👨👩👧👦", count: 1000),
+ "e" + String(repeating: "\u{301}", count: 100_000),
+ String(repeating: "農", count: 2000), String(repeating: "👩🏽🌾", count: 1000)]
+ {
+ let excerpt = feed.content(value)
+ XCTAssertLessThanOrEqual(excerpt.utf8.count, 4096)
+ XCTAssertLessThanOrEqual(excerpt.count, 640)
+ XCTAssertFalse(excerpt.contains("�"))
+ XCTAssertEqual(detail.content(value), value)
+ }
+ }
+
+ func testByteBoundaryNeverIncludesAPartialGrapheme() {
+ let cluster = "👩🏽🌾"
+ let value = "abc" + cluster + "end"
+ for bytes in 3 ... value.utf8.count {
+ let result = TeraTodayCardPresentation.excerpt(value, characters: 100, bytes: bytes)
+ XCTAssertLessThanOrEqual(result.utf8.count, bytes)
+ XCTAssertTrue(value.hasPrefix(String(result.dropLast())) || result == value)
+ XCTAssertFalse(result.contains("�"))
+ }
+ }
+
+ func testFeedBoundsLabelsAndThumbnailsWhileDetailRetainsEveryReference() {
+ let label = String(repeating: "a", count: 160)
+ XCTAssertEqual(TeraTodayCardPresentation.feed.label(label), label)
+ XCTAssertEqual(TeraTodayCardPresentation.feed.label(label + "b").count, 160)
+ let references = Array(repeating: TeraScopeFixtures.reference(), count: 4)
+ XCTAssertEqual(TeraTodayCardPresentation.feed.media(Array(references.prefix(3))).count, 3)
+ XCTAssertEqual(TeraTodayCardPresentation.feed.media(references).count, 3)
+ XCTAssertEqual(TeraTodayCardPresentation.detail.media(references), references)
+ }
+}
diff --git a/core/crates/tera_core/src/runtime/product_surface/today.rs b/core/crates/tera_core/src/runtime/product_surface/today.rs
@@ -74,6 +74,10 @@ pub use reconciliation::TodayReconciliation;
mod reconciliation_tests;
#[cfg(test)]
+#[path = "today_performance_tests.rs"]
+mod performance_tests;
+
+#[cfg(test)]
#[path = "today_scope_tests.rs"]
mod scope_tests;
@@ -391,17 +395,14 @@ impl TeraRuntime {
if request.limit == 0 || request.limit > TODAY_PAGE_LIMIT_MAX {
return Err(TodayError::InvalidRequest);
}
- let storage = self
- .client
- .storage()
- .map_err(|_| TodayError::RuntimeUnavailable)?;
- let event_status = EventStore::status(storage).await?;
- let algorithm_generation = projection_generation()?;
- let projection_id = projection_id()?;
+ // Decode and validate all caller-owned scope before any storage I/O.
+ let decoded_scope = request
+ .cursor
+ .as_deref()
+ .map(TodayCursor::scope)
+ .transpose()?;
let query_scope = paging_scope::query_scope(context, self.store_public_key)?;
-
- let (scope, snapshot, after) = if let Some(cursor) = request.cursor.as_deref() {
- let scope = TodayCursor::scope(cursor)?;
+ if let Some(scope) = &decoded_scope {
if scope.context_id != context.id
|| scope.context_generation != context.generation
|| scope.query_scope != query_scope
@@ -411,7 +412,19 @@ impl TeraRuntime {
if request.as_of.is_some_and(|as_of| as_of != scope.as_of) {
return Err(CursorError::SnapshotMismatch.into());
}
- if scope.store_generation != *event_status.generation().as_bytes() {
+ } else if request.as_of.is_none_or(|value| value == 0) {
+ return Err(TodayError::InvalidRequest);
+ }
+ let storage = self
+ .client
+ .storage()
+ .map_err(|_| TodayError::RuntimeUnavailable)?;
+ let store_generation = current_store_generation(storage).await?;
+ let algorithm_generation = projection_generation()?;
+ let projection_id = projection_id()?;
+ let (scope, snapshot, after) = if let Some(cursor) = request.cursor.as_deref() {
+ let scope = decoded_scope.expect("cursor presence was decoded above");
+ if scope.store_generation != store_generation {
return Err(CursorError::Stale.into());
}
let position = TodayCursor::decode(cursor, &scope)?;
@@ -453,8 +466,7 @@ impl TeraRuntime {
.ok_or(TodayError::ProjectionMissing)?
}
};
- if state.store_generation != *event_status.generation().as_bytes()
- || state.query_scope != Some(query_scope)
+ if state.store_generation != store_generation || state.query_scope != Some(query_scope)
{
return Err(CursorError::Stale.into());
}
@@ -1322,6 +1334,17 @@ fn refresh_thread_profiles(state: &mut TodayProjectionState) {
}
}
+// A bounded owner query authenticates the source generation without invoking
+// full visibility/status reduction. The backend may read one look-ahead row.
+async fn current_store_generation(storage: &dyn EventStore) -> Result<[u8; 32], TodayError> {
+ Ok(
+ *EventStore::query_raw(storage, EventQuery::all(EventQueryBounds::first(1)?))
+ .await?
+ .generation()
+ .as_bytes(),
+ )
+}
+
async fn query_all_visible(
storage: &dyn radroots_storage::Storage,
) -> Result<Vec<radroots_storage::event::StoredVisibleEvent>, TodayError> {
@@ -2248,7 +2271,7 @@ mod tests {
SignedEvent::from_wire_verified_id(wire, raw).expect("signed event")
}
- fn visible_admission(event: SignedEvent, observed_at: u64) -> EventAdmission {
+ pub(super) fn visible_admission(event: SignedEvent, observed_at: u64) -> EventAdmission {
let selected = admit_verified_event(
verify_nip01_event(event.envelope().clone()).expect("codec verification"),
)
diff --git a/core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs
@@ -0,0 +1,500 @@
+// Test-only forwarding adapter. Every operation reaches the real SDK-owned SQLite
+// store. Counts describe SPI calls and returned rows, not disabled SQL telemetry.
+use radroots_storage::{
+ Error, atomic::*, authored::*, authored_atomic::*, authored_delivery::*, authored_draft::*,
+ backup::*, event::*, journal::*, outbox::*, private_artifact::*, projection::*, status::*,
+};
+use std::sync::{Arc, Mutex};
+
+#[derive(Default)]
+pub(super) struct QueryCounts(Mutex<std::collections::BTreeMap<&'static str, u64>>);
+impl QueryCounts {
+ fn add(&self, key: &'static str, amount: u64) {
+ *self.0.lock().unwrap().entry(key).or_default() += amount;
+ }
+ pub(super) fn take(&self) -> std::collections::BTreeMap<&'static str, u64> {
+ std::mem::take(&mut self.0.lock().unwrap())
+ }
+}
+pub(super) struct CountedStorage {
+ pub(super) client: radroots_sdk::Client,
+ pub(super) counts: Arc<QueryCounts>,
+}
+impl CountedStorage {
+ fn storage(&self) -> &dyn radroots_storage::Storage {
+ self.client.storage().unwrap()
+ }
+}
+
+impl EventStore for CountedStorage {
+ fn status(&self) -> BoxFuture<'_, Result<EventStoreStatus, Error>> {
+ Box::pin(async move {
+ self.counts.add("event.status", 1);
+ EventStore::status(self.storage()).await
+ })
+ }
+ fn admit(&self, admission: EventAdmission) -> BoxFuture<'_, Result<AdmissionReceipt, Error>> {
+ Box::pin(async move {
+ self.counts.add("event.admit", 1);
+ EventStore::admit(self.storage(), admission).await
+ })
+ }
+ fn query_raw(
+ &self,
+ query: EventQuery,
+ ) -> BoxFuture<'_, Result<EventPage<StoredRawEvent>, Error>> {
+ Box::pin(async move {
+ self.counts.add("event.query_raw", 1);
+ let result = EventStore::query_raw(self.storage(), query).await;
+ if let Ok(page) = &result {
+ self.counts.add("event.rows", page.items().len() as u64);
+ }
+ result
+ })
+ }
+ fn query_verified(
+ &self,
+ query: EventQuery,
+ ) -> BoxFuture<'_, Result<EventPage<StoredVerifiedEvent>, Error>> {
+ Box::pin(async move {
+ self.counts.add("event.query_verified", 1);
+ let result = EventStore::query_verified(self.storage(), query).await;
+ if let Ok(page) = &result {
+ self.counts.add("event.rows", page.items().len() as u64);
+ }
+ result
+ })
+ }
+ fn query_visible(
+ &self,
+ query: EventQuery,
+ ) -> BoxFuture<'_, Result<EventPage<StoredVisibleEvent>, Error>> {
+ Box::pin(async move {
+ self.counts.add("event.query_visible", 1);
+ let result = EventStore::query_visible(self.storage(), query).await;
+ if let Ok(page) = &result {
+ self.counts.add("event.rows", page.items().len() as u64);
+ }
+ result
+ })
+ }
+ fn rebuild_visibility(&self) -> BoxFuture<'_, Result<VisibilitySnapshot, Error>> {
+ Box::pin(async move {
+ self.counts.add("event.rebuild_visibility", 1);
+ EventStore::rebuild_visibility(self.storage()).await
+ })
+ }
+ fn query_provenance(
+ &self,
+ event_id: EventId,
+ bounds: EventQueryBounds,
+ ) -> BoxFuture<'_, Result<EventPage<StoredEventProvenance>, Error>> {
+ Box::pin(async move {
+ self.counts.add("event.query_provenance", 1);
+ EventStore::query_provenance(self.storage(), event_id, bounds).await
+ })
+ }
+}
+
+impl ProjectionStore for CountedStorage {
+ fn status(
+ &self,
+ projection_id: ProjectionId,
+ ) -> BoxFuture<'_, Result<Option<ProjectionStatus>, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.status", 1);
+ ProjectionStore::status(self.storage(), projection_id).await
+ })
+ }
+ fn checkpoint(
+ &self,
+ checkpoint: ProjectionCheckpoint,
+ ) -> BoxFuture<'_, Result<ProjectionStatus, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.checkpoint", 1);
+ ProjectionStore::checkpoint(self.storage(), checkpoint).await
+ })
+ }
+ fn invalidate(
+ &self,
+ invalidation: ProjectionInvalidation,
+ ) -> BoxFuture<'_, Result<ProjectionStatus, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.invalidate", 1);
+ ProjectionStore::invalidate(self.storage(), invalidation).await
+ })
+ }
+ fn invalidation(
+ &self,
+ projection_id: ProjectionId,
+ replacement_generation: ProjectionGeneration,
+ ) -> BoxFuture<'_, Result<Option<ProjectionInvalidation>, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.invalidation", 1);
+ ProjectionStore::invalidation(self.storage(), projection_id, replacement_generation)
+ .await
+ })
+ }
+ fn request_rebuild(
+ &self,
+ ticket: RebuildTicket,
+ ) -> BoxFuture<'_, Result<RebuildTicket, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.request_rebuild", 1);
+ ProjectionStore::request_rebuild(self.storage(), ticket).await
+ })
+ }
+ fn rebuild(
+ &self,
+ ticket_id: RebuildTicketId,
+ ) -> BoxFuture<'_, Result<Option<RebuildTicket>, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.rebuild", 1);
+ ProjectionStore::rebuild(self.storage(), ticket_id).await
+ })
+ }
+ fn transition_rebuild(
+ &self,
+ transition: RebuildTransition,
+ ) -> BoxFuture<'_, Result<RebuildTicket, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.transition_rebuild", 1);
+ ProjectionStore::transition_rebuild(self.storage(), transition).await
+ })
+ }
+ fn event_index_manifest(
+ &self,
+ generation: ProjectionGeneration,
+ ) -> BoxFuture<'_, Result<Option<EventIndexManifest>, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.event_index_manifest", 1);
+ ProjectionStore::event_index_manifest(self.storage(), generation).await
+ })
+ }
+ fn put_event_index_manifest(
+ &self,
+ manifest: EventIndexManifest,
+ ) -> BoxFuture<'_, Result<(), Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.put_event_index_manifest", 1);
+ ProjectionStore::put_event_index_manifest(self.storage(), manifest).await
+ })
+ }
+ fn event_index_checkpoint(
+ &self,
+ generation: ProjectionGeneration,
+ ) -> BoxFuture<'_, Result<Option<EventIndexCheckpoint>, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.event_index_checkpoint", 1);
+ ProjectionStore::event_index_checkpoint(self.storage(), generation).await
+ })
+ }
+ fn put_event_index_checkpoint(
+ &self,
+ checkpoint: EventIndexCheckpoint,
+ ) -> BoxFuture<'_, Result<(), Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.put_event_index_checkpoint", 1);
+ ProjectionStore::put_event_index_checkpoint(self.storage(), checkpoint).await
+ })
+ }
+ fn put_projection_document(
+ &self,
+ projection_id: ProjectionId,
+ generation: ProjectionGeneration,
+ document: ProjectionDocument,
+ ) -> BoxFuture<'_, Result<(), Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.put_projection_document", 1);
+ ProjectionStore::put_projection_document(
+ self.storage(),
+ projection_id,
+ generation,
+ document,
+ )
+ .await
+ })
+ }
+ fn projection_document(
+ &self,
+ projection_id: ProjectionId,
+ generation: ProjectionGeneration,
+ key: String,
+ ) -> BoxFuture<'_, Result<Option<ProjectionDocument>, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.projection_document", 1);
+ ProjectionStore::projection_document(self.storage(), projection_id, generation, key)
+ .await
+ })
+ }
+ fn put_projection_snapshot(
+ &self,
+ snapshot: ProjectionSnapshot,
+ ) -> BoxFuture<'_, Result<(), Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.put_projection_snapshot", 1);
+ ProjectionStore::put_projection_snapshot(self.storage(), snapshot).await
+ })
+ }
+ fn projection_snapshot(
+ &self,
+ projection_id: ProjectionId,
+ snapshot_id: [u8; 32],
+ ) -> BoxFuture<'_, Result<Option<ProjectionSnapshot>, Error>> {
+ Box::pin(async move {
+ self.counts.add("projection.projection_snapshot", 1);
+ ProjectionStore::projection_snapshot(self.storage(), projection_id, snapshot_id).await
+ })
+ }
+}
+
+impl Journal for CountedStorage {
+ fn prepare(&self, operation: PrepareOperation) -> BoxFuture<'_, Result<PrepareReceipt, Error>> {
+ Journal::prepare(self.storage(), operation)
+ }
+ fn operation(
+ &self,
+ instance_id: OperationInstanceId,
+ ) -> BoxFuture<'_, Result<Option<OperationRecord>, Error>> {
+ Journal::operation(self.storage(), instance_id)
+ }
+ fn by_idempotency_key(
+ &self,
+ operation_id: OperationId,
+ idempotency_key: IdempotencyKey,
+ ) -> BoxFuture<'_, Result<Option<OperationRecord>, Error>> {
+ Journal::by_idempotency_key(self.storage(), operation_id, idempotency_key)
+ }
+ fn transition(
+ &self,
+ transition: JournalTransition,
+ ) -> BoxFuture<'_, Result<OperationRecord, Error>> {
+ Journal::transition(self.storage(), transition)
+ }
+ fn recoverable(&self, limit: u16) -> BoxFuture<'_, Result<Vec<OperationRecord>, Error>> {
+ Journal::recoverable(self.storage(), limit)
+ }
+}
+
+impl Outbox for CountedStorage {
+ fn enqueue(&self, item: EnqueueOutboxItem) -> BoxFuture<'_, Result<EnqueueReceipt, Error>> {
+ Outbox::enqueue(self.storage(), item)
+ }
+ fn item(&self, item_id: OutboxItemId) -> BoxFuture<'_, Result<Option<OutboxRecord>, Error>> {
+ Outbox::item(self.storage(), item_id)
+ }
+ fn claim(
+ &self,
+ request: ClaimOutboxItems,
+ ) -> BoxFuture<'_, Result<Vec<ClaimedOutboxItem>, Error>> {
+ Outbox::claim(self.storage(), request)
+ }
+ fn record_attempt(
+ &self,
+ evidence: DeliveryAttemptEvidence,
+ ) -> BoxFuture<'_, Result<OutboxRecord, Error>> {
+ Outbox::record_attempt(self.storage(), evidence)
+ }
+ fn release(
+ &self,
+ item_id: OutboxItemId,
+ lease_id: LeaseId,
+ expected_revision: OutboxRevision,
+ released_at_unix_ms: u64,
+ retry_not_before_unix_ms: Option<u64>,
+ ) -> BoxFuture<'_, Result<OutboxRecord, Error>> {
+ Outbox::release(
+ self.storage(),
+ item_id,
+ lease_id,
+ expected_revision,
+ released_at_unix_ms,
+ retry_not_before_unix_ms,
+ )
+ }
+ fn status(&self) -> BoxFuture<'_, Result<OutboxStatus, Error>> {
+ Outbox::status(self.storage())
+ }
+}
+
+impl PrivateArtifactStore for CountedStorage {
+ fn put_metadata(
+ &self,
+ metadata: PrivateArtifactMetadata,
+ ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> {
+ PrivateArtifactStore::put_metadata(self.storage(), metadata)
+ }
+ fn metadata(
+ &self,
+ artifact_id: PrivateArtifactId,
+ ) -> BoxFuture<'_, Result<Option<PrivateArtifactMetadata>, Error>> {
+ PrivateArtifactStore::metadata(self.storage(), artifact_id)
+ }
+ fn reseal_metadata(
+ &self,
+ request: PrivateArtifactResealRequest,
+ ) -> BoxFuture<'_, Result<PrivateArtifactResealReceipt, Error>> {
+ PrivateArtifactStore::reseal_metadata(self.storage(), request)
+ }
+ fn mark_expired(
+ &self,
+ artifact_id: PrivateArtifactId,
+ expected_revision: PrivateArtifactRevision,
+ at_unix_ms: u64,
+ ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> {
+ PrivateArtifactStore::mark_expired(
+ self.storage(),
+ artifact_id,
+ expected_revision,
+ at_unix_ms,
+ )
+ }
+ fn tombstone(
+ &self,
+ artifact_id: PrivateArtifactId,
+ expected_revision: PrivateArtifactRevision,
+ at_unix_ms: u64,
+ reason: DeletionReason,
+ ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> {
+ PrivateArtifactStore::tombstone(
+ self.storage(),
+ artifact_id,
+ expected_revision,
+ at_unix_ms,
+ reason,
+ )
+ }
+ fn expired(
+ &self,
+ at_unix_ms: u64,
+ limit: u16,
+ ) -> BoxFuture<'_, Result<Vec<PrivateArtifactMetadata>, Error>> {
+ PrivateArtifactStore::expired(self.storage(), at_unix_ms, limit)
+ }
+ fn status(&self) -> BoxFuture<'_, Result<PrivateArtifactStatus, Error>> {
+ PrivateArtifactStore::status(self.storage())
+ }
+}
+
+impl StorageReliability for CountedStorage {
+ fn begin_backup(&self, plan: BackupPlan) -> BoxFuture<'_, Result<BackupOperation, Error>> {
+ StorageReliability::begin_backup(self.storage(), plan)
+ }
+ fn transition_backup(
+ &self,
+ backup_id: BackupId,
+ expected_revision: ReliabilityRevision,
+ transition: BackupTransition,
+ at_unix_ms: u64,
+ ) -> BoxFuture<'_, Result<BackupOperation, Error>> {
+ StorageReliability::transition_backup(
+ self.storage(),
+ backup_id,
+ expected_revision,
+ transition,
+ at_unix_ms,
+ )
+ }
+ fn begin_restore(&self, plan: RestorePlan) -> BoxFuture<'_, Result<RestoreOperation, Error>> {
+ StorageReliability::begin_restore(self.storage(), plan)
+ }
+ fn transition_restore(
+ &self,
+ backup_id: BackupId,
+ expected_revision: ReliabilityRevision,
+ transition: RestoreTransition,
+ at_unix_ms: u64,
+ ) -> BoxFuture<'_, Result<RestoreOperation, Error>> {
+ StorageReliability::transition_restore(
+ self.storage(),
+ backup_id,
+ expected_revision,
+ transition,
+ at_unix_ms,
+ )
+ }
+ fn integrity(&self) -> BoxFuture<'_, Result<IntegrityStatus, Error>> {
+ StorageReliability::integrity(self.storage())
+ }
+ fn status(&self) -> BoxFuture<'_, Result<StorageStatus, Error>> {
+ StorageReliability::status(self.storage())
+ }
+ fn close(&self) -> BoxFuture<'_, Result<StorageStatus, Error>> {
+ StorageReliability::close(self.storage())
+ }
+}
+
+impl AtomicStorage for CountedStorage {
+ fn commit(&self, request: AtomicCommit) -> BoxFuture<'_, Result<AtomicCommitReceipt, Error>> {
+ AtomicStorage::commit(self.storage(), request)
+ }
+ fn receipt(
+ &self,
+ commit_id: AtomicCommitId,
+ ) -> BoxFuture<'_, Result<Option<AtomicCommitReceipt>, Error>> {
+ AtomicStorage::receipt(self.storage(), commit_id)
+ }
+}
+
+impl AuthoredAtomicStorage for CountedStorage {
+ fn execute_authored(
+ &self,
+ command: AuthoredAtomicCommand,
+ ) -> BoxFuture<'_, Result<AuthoredAtomicReceipt, Error>> {
+ AuthoredAtomicStorage::execute_authored(self.storage(), command)
+ }
+ fn authored_receipt(
+ &self,
+ commit_id: AtomicCommitId,
+ ) -> BoxFuture<'_, Result<Option<AuthoredAtomicReceipt>, Error>> {
+ AuthoredAtomicStorage::authored_receipt(self.storage(), commit_id)
+ }
+ fn authored_operation(
+ &self,
+ operation_id: OperationInstanceId,
+ ) -> BoxFuture<'_, Result<Option<AuthoredOperation>, Error>> {
+ AuthoredAtomicStorage::authored_operation(self.storage(), operation_id)
+ }
+ fn authored_artifact(
+ &self,
+ artifact_id: AuthoredArtifactId,
+ ) -> BoxFuture<'_, Result<Option<AuthoredArtifact>, Error>> {
+ AuthoredAtomicStorage::authored_artifact(self.storage(), artifact_id)
+ }
+ fn authored_delivery_plan(
+ &self,
+ plan_id: AuthoredDeliveryPlanId,
+ ) -> BoxFuture<'_, Result<Option<AuthoredDeliveryPlan>, Error>> {
+ AuthoredAtomicStorage::authored_delivery_plan(self.storage(), plan_id)
+ }
+}
+
+impl AuthoredDraftStore for CountedStorage {
+ fn append_authored_draft(
+ &self,
+ draft: AuthoredDraft,
+ expected_head: Option<AuthoredDraftRevision>,
+ ) -> BoxFuture<'_, Result<DraftAppendReceipt, Error>> {
+ AuthoredDraftStore::append_authored_draft(self.storage(), draft, expected_head)
+ }
+ fn authored_draft_head(
+ &self,
+ draft_id: AuthoredDraftId,
+ ) -> BoxFuture<'_, Result<Option<AuthoredDraft>, Error>> {
+ AuthoredDraftStore::authored_draft_head(self.storage(), draft_id)
+ }
+ fn authored_draft_revision(
+ &self,
+ draft_id: AuthoredDraftId,
+ revision: AuthoredDraftRevision,
+ ) -> BoxFuture<'_, Result<Option<AuthoredDraft>, Error>> {
+ AuthoredDraftStore::authored_draft_revision(self.storage(), draft_id, revision)
+ }
+ fn authored_draft_heads(
+ &self,
+ author: [u8; 32],
+ limit: u16,
+ ) -> BoxFuture<'_, Result<Vec<AuthoredDraft>, Error>> {
+ AuthoredDraftStore::authored_draft_heads(self.storage(), author, limit)
+ }
+}
diff --git a/core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs
@@ -0,0 +1,163 @@
+use super::tests::{context, keys, signed, visible_admission};
+use super::*;
+use crate::runtime::{
+ builder::RuntimeBuilder,
+ store::{MobileUserStoreConfig, ProtectedDataAvailability},
+};
+use std::{sync::Arc, time::Instant};
+#[path = "today_counted_storage_tests.rs"]
+mod counted_storage;
+use counted_storage::{CountedStorage, QueryCounts};
+const EVENT_COUNT: u64 = 10_000;
+const NOW: u64 = 2_000_000_000;
+
+#[tokio::test]
+async fn cached_sqlite_pages_do_not_decode_ten_thousand_source_events() {
+ let (_root, runtime, counts) = counted_sqlite().await;
+ let selected = context(None, 1);
+ for index in 0..EVENT_COUNT {
+ let event = signed(1, vec![], "Synthetic bounded query fixture", NOW - index);
+ EventStore::admit(
+ runtime.client.storage().unwrap(),
+ visible_admission(event, NOW * 1_000),
+ )
+ .await
+ .unwrap();
+ }
+ counts.take();
+ let refresh = Instant::now();
+ runtime
+ .phase1_refresh_today(&selected, NOW, TodayProjectionUpdate::Rebuild)
+ .await
+ .unwrap();
+ let rebuild_ms = refresh.elapsed().as_secs_f64() * 1_000.0;
+ let rebuild_calls = counts.take();
+ let samples = async {
+ let mut samples = Vec::new();
+ for _ in 0..if cfg!(debug_assertions) { 5 } else { 20 } {
+ counts.take();
+ let start = Instant::now();
+ let first = runtime
+ .phase1_today_page(&selected, TodayPageRequest::first(20, NOW))
+ .await
+ .unwrap();
+ assert_eq!(first.items.len(), 20);
+ let calls = counts.take();
+ samples.push(("first", start.elapsed().as_secs_f64() * 1_000.0, calls));
+ counts.take();
+ let start = Instant::now();
+ let next = runtime
+ .phase1_today_page(
+ &selected,
+ TodayPageRequest::after(20, first.next_cursor.unwrap()),
+ )
+ .await
+ .unwrap();
+ assert_eq!(next.items.len(), 20);
+ assert_ne!(first.items[0].card.card_id, next.items[0].card.card_id);
+ let calls = counts.take();
+ samples.push(("after", start.elapsed().as_secs_f64() * 1_000.0, calls));
+ }
+ samples
+ }
+ .await;
+ println!(
+ "{}",
+ serde_json::json!({
+ "scenario": "tera_10000_sqlite_cached_pages",
+ "events": EVENT_COUNT, "page_size": 20, "rebuild_ms": rebuild_ms,
+ "profile": if cfg!(debug_assertions) { "debug" } else { "release" },
+ "samples": samples, "rebuild_calls": rebuild_calls,
+ "count_boundary": "actual storage SPI calls and returned event rows; SQL logging remains disabled",
+ "physical_device_qualification": false
+ })
+ );
+ runtime.shutdown().await.unwrap();
+ assert!(samples.iter().all(|(_, _, calls)| {
+ calls
+ .get("projection.projection_document")
+ .copied()
+ .unwrap_or(0)
+ > 0
+ }));
+ assert!(
+ samples.iter().all(
+ |(_, _, calls)| calls.get("event.status").copied().unwrap_or(0) == 0
+ && calls.get("event.query_visible").copied().unwrap_or(0) == 0
+ && calls.get("event.rows").copied().unwrap_or(0) <= 1
+ ),
+ "Cached pages must not read full event history for generation validation"
+ );
+}
+
+async fn counted_sqlite() -> (tempfile::TempDir, TeraRuntime, Arc<QueryCounts>) {
+ let root = tempfile::tempdir().unwrap();
+ let store = MobileUserStoreConfig::from_encoded(
+ root.path(),
+ &keys().public_key().to_string(),
+ &"03".repeat(32),
+ 1_800_000_000_000,
+ ProtectedDataAvailability::Available,
+ )
+ .unwrap();
+ std::fs::create_dir_all(store.owner_directory()).unwrap();
+ let mut runtime = RuntimeBuilder::new(store).build().await.unwrap();
+ let counts = Arc::new(QueryCounts::default());
+ runtime.client = radroots_sdk::ClientBuilder::new()
+ .storage(Arc::new(CountedStorage {
+ client: runtime.client.clone(),
+ counts: counts.clone(),
+ }))
+ .build()
+ .unwrap();
+ (root, runtime, counts)
+}
+
+#[tokio::test]
+async fn invalid_page_boundaries_reject_before_any_storage_query() {
+ let (_root, runtime, counts) = counted_sqlite().await;
+ let selected = context(None, 1);
+ let mut wrong = selected.clone();
+ wrong.generation += 1;
+ let scope = CursorScope::new(
+ wrong.id.clone().into(),
+ wrong.generation,
+ NOW,
+ [3; 32],
+ 1,
+ paging_scope::query_scope(&wrong, runtime.store_public_key).unwrap(),
+ )
+ .unwrap();
+ // Malformed and oversized opaque inputs, invalid limit/as-of, and a valid
+ // cursor for a different authenticated context all precede storage work.
+ let position = TodayCursorPosition {
+ rank: TodayRank::derive(TodayRankInput {
+ card_id: CardId::parse(&"ab".repeat(32)).unwrap(),
+ context_rank: super::super::ContextRank::MissingLocalityFallback,
+ card_type: TodayCardType::Update,
+ time: TimeRelevance::Published,
+ effective_at: NOW,
+ as_of: NOW,
+ })
+ .unwrap(),
+ };
+ let wrong_cursor = TodayCursor::encode(&scope, position)
+ .unwrap()
+ .as_str()
+ .to_owned();
+ for request in [
+ TodayPageRequest::after(20, "bad".into()),
+ TodayPageRequest::after(20, "x".repeat(100_000)),
+ TodayPageRequest::first(0, NOW),
+ TodayPageRequest::first(20, 0),
+ TodayPageRequest::after(20, wrong_cursor),
+ ] {
+ counts.take();
+ assert!(runtime.phase1_today_page(&selected, request).await.is_err());
+ assert!(
+ counts.take().is_empty(),
+ "Invalid scope must not touch the store"
+ );
+ }
+ runtime.shutdown().await.unwrap();
+}
diff --git a/core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs b/core/crates/tera_core/src/runtime/product_surface/today_reconciliation.rs
@@ -41,7 +41,7 @@ impl TeraRuntime {
if state.query_scope != Some(query_scope)
|| state.visibility_digest.is_none()
|| expected_generation.is_some_and(|value| value != state.content_generation)
- || state.store_generation != *EventStore::status(storage).await?.generation().as_bytes()
+ || state.store_generation != current_store_generation(storage).await?
{
return Err(CursorError::Stale.into());
}
diff --git a/project.yml b/project.yml
@@ -84,6 +84,8 @@ targets:
- path: TeraTests/TeraStoreRecoveryTests.swift
- path: TeraTests/TeraAddObservationStartupTests.swift
- path: TeraTests/TeraMediaStoreTests.swift
+ - path: TeraTests/TeraMediaBudgetTests.swift
+ - path: TeraTests/TeraTodayRenderingTests.swift
- path: TeraTests/TeraMediaFileFixture.swift
- path: TeraTests/TeraMediaOwnershipFFITests.swift
- path: TeraTests/TeraBindingPortabilityTests.swift
diff --git a/release/provenance.json b/release/provenance.json
@@ -2,11 +2,11 @@
"artifacts": {
"app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b",
"ffi_api_sha256": "4cc03b03dd54db7b4ab22684ff819233444ae0a43a770ab875148892bed1e3f2",
- "ffi_provenance_sha256": "9061000a642f27b7ae4a4c8fb91e9d0be789ca4badedbecece5f18b0fdd3bd73",
+ "ffi_provenance_sha256": "f174e6196e6b8ffdadaabcf52c6d697ee7ba9e3acc55ff265cd928c1131d4010",
"info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef",
"privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12",
"sbom_sha256": "bea2d48c06e5f7e84cdad085d81290712674b74664456f2030064d9cf94c4b33",
- "xcode_project_sha256": "3e09b28fc0437d3bacdc5d62f6758f31f98fb6be34fcac8c182a8f1a3a0b4922"
+ "xcode_project_sha256": "edb5658749d389fee06d27a37451c2bff6851bb32b00572553e11b0fa340566d"
},
"disposition": "unsigned",
"platforms": [
@@ -22,7 +22,7 @@
"lib_revision": "13ac1f2135199702a07ba6aa14ab22101f17fcb0",
"source_date_epoch": 1787871027,
"swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0",
- "tera_ffi_source_tree": "3ff40c5420af5cfa8f2726bf65285c69a5a0386e",
+ "tera_ffi_source_tree": "703c94b59618f9ca1dd16c17df8fb07fb7b860bd",
"xcode_package_lock_sha256": "c7f41934ea25f7a287bdc4f3a6ecabbf09a3a0bdd0f5a3e58183f355ca814096"
},
"version": "0.1.0-alpha"
diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json
@@ -4337,6 +4337,14 @@
"count": 4
},
{
+ "path": "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs",
+ "count": 1
+ },
+ {
+ "path": "core/crates/tera_core/src/runtime/product_surface/today_performance_tests.rs",
+ "count": 1
+ },
+ {
"path": "core/crates/tera_core/src/runtime/product_surface/today_sync_tests.rs",
"count": 2
},
@@ -4461,6 +4469,10 @@
"count": 1
},
{
+ "path": "core/crates/tera_core/src/runtime/product_surface/today_counted_storage_tests.rs",
+ "count": 2
+ },
+ {
"path": "core/crates/tera_core/src/runtime/product_surface/today_sync.rs",
"count": 1
},
diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json
@@ -1 +1 @@
-[{"text":" init(","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":21,"file":"Tera\/App\/TeraAppModel.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":51,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines"}},{"text":"#endif","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":458,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 458"}},{"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":15,"line":111,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":1576,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 1576"}},{"text":"actor TeraRuntimeClient {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":3,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":379,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":835,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 835"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":1175,"file":"Tera\/Runtime\/TeraRuntimeModels.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 1175"}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":11,"file":"Tera\/State\/TeraAddStore.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines"}},{"text":" func retract(_ card: TeraTodayCard) async {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":459,"file":"Tera\/State\/TeraAddStore.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":796,"file":"Tera\/State\/TeraAddStore.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 796"}},{"text":"actor TeraConfigurationStore {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":78,"file":"Tera\/State\/TeraConfigurationStore.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":5,"line":148,"file":"Tera\/State\/TeraConfigurationStore.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":759,"file":"Tera\/State\/TeraConfigurationStore.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 759"}},{"text":"actor TeraSessionStore {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":39,"file":"Tera\/State\/TeraSessionStore.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"character":11,"line":125,"file":"Tera\/State\/TeraSessionStore.swift"},"ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","reason":"Function should have complexity 10 or less; currently complexity is 13"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":452,"file":"Tera\/State\/TeraSessionStore.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 452"}},{"text":"struct TeraAddView: View {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":4,"file":"Tera\/Views\/TeraAddView.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":560,"file":"Tera\/Views\/TeraAddView.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 560"}},{"text":"struct TeraSettingsView: View {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":1,"line":365,"file":"Tera\/Views\/TeraSupportingViews.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":629,"file":"Tera\/Views\/TeraSupportingViews.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 629"}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":6,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines"}},{"text":"private actor AddBackend: TeraRuntimeBackend {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":9,"line":844,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines"}},{"text":" private func makeStatus(","violation":{"ruleName":"Function Parameter Count","severity":"error","location":{"character":11,"line":1280,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"function_parameter_count","ruleDescription":"Number of function parameters should be low.","reason":"Function should have 5 parameters or less: it currently has 8"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":1431,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 1431"}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":7,"file":"TeraTests\/TeraRemoteQualificationTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":45,"file":"TeraTests\/TeraRemoteQualificationTests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":212,"file":"TeraTests\/TeraRemoteQualificationTests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":4,"file":"TeraTests\/TeraRuntimeClientTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":6,"file":"TeraTests\/TeraStateMigrationTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":5,"line":7,"file":"TeraTests\/TeraStateMigrationTests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":622,"file":"TeraTests\/TeraStateMigrationTests.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 622"}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":9,"line":207,"file":"TeraTests\/TeraSupportingStoreTests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":487,"file":"TeraTests\/TeraSupportingStoreTests.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 487"}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"ruleName":"Type Body Length","severity":"error","location":{"character":7,"line":4,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":62,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":174,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":3,"line":247,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"character":11,"line":573,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","reason":"Function should have complexity 10 or less; currently complexity is 18"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"character":11,"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","reason":"Function should have complexity 10 or less; currently complexity is 11"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":11,"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines"}},{"text":" static func environment(","violation":{"ruleName":"Function Body Length","severity":"error","location":{"character":10,"line":1736,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","location":{"character":1,"line":2212,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 2212"}}]
-\ No newline at end of file
+[{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/App\/TeraAppModel.swift","line":21,"character":3},"reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" init("},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":51,"character":1},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 330 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":458,"character":1},"reason":"File should contain 400 lines or less: currently contains 458","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"#endif"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":111,"character":15},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":1576,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1576","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":3,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 753 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraRuntimeClient {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":379,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":835,"character":1},"reason":"File should contain 400 lines or less: currently contains 835","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","line":1175,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1175","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":11,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 731 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":459,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraAddStore.swift","line":796,"character":1},"reason":"File should contain 400 lines or less: currently contains 796","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":78,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":148,"character":5},"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":759,"character":1},"reason":"File should contain 400 lines or less: currently contains 759","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":39,"character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraSessionStore {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":125,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 13","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":452,"character":1},"reason":"File should contain 400 lines or less: currently contains 452","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","line":4,"character":1},"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraAddView: View {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraAddView.swift","line":560,"character":1},"reason":"File should contain 400 lines or less: currently contains 560","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":365,"character":1},"reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":629,"character":1},"reason":"File should contain 400 lines or less: currently contains 629","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":6,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":844,"character":9},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 492 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor AddBackend: TeraRuntimeBackend {"},{"violation":{"ruleName":"Function Parameter Count","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1280,"character":11},"reason":"Function should have 5 parameters or less: it currently has 8","ruleIdentifier":"function_parameter_count","ruleDescription":"Number of function parameters should be low."},"text":" private func makeStatus("},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1431,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1431","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":7,"character":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":45,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":212,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","line":4,"character":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":6,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":7,"character":5},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":622,"character":1},"reason":"File should contain 400 lines or less: currently contains 622","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":207,"character":9},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":487,"character":1},"reason":"File should contain 400 lines or less: currently contains 487","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleName":"Type Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":4,"character":7},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":62,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":174,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":247,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":573,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 18","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"ruleName":"Cyclomatic Complexity","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"reason":"Function should have complexity 10 or less; currently complexity is 11","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleName":"Function Body Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":1736,"character":10},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"},"text":" static func environment("},{"violation":{"ruleName":"File Length","severity":"error","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":2212,"character":1},"reason":"File should contain 1000 lines or less: currently contains 2212","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines."},"text":"}"}]
+\ No newline at end of file