commit 0f8bffd1cd4ac1d237d5d44fd9c68da70bf74a7a
parent d3b1aabb8347a6cf5033c0816fde6ab187e69f8c
Author: triesap <tyson@radroots.org>
Date: Wed, 17 Jun 2026 20:41:49 -0700
field-ios: bridge background session lifecycle
Forward iOS background URLSession lifecycle events through the shared background execution service and normalize the background execution UI test probe shape across success and failure paths.
Diffstat:
7 files changed, 106 insertions(+), 0 deletions(-)
diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj
@@ -39,6 +39,7 @@
9121BD4A3E7C6EF2B21F540F /* CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7DE4207398DE242519F9C /* CopyButton.swift */; };
9346DA48630668A65D37E14A /* FieldTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */; };
A1B921027DA7ACD7343BE250 /* SectionWideButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA8F5611075F60F214A00 /* SectionWideButton.swift */; };
+ A54E244A554EC6B46DF8DE48 /* RadrootsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D317BC9F759709098490DD /* RadrootsAppDelegate.swift */; };
ABBA5CC10933CA087E14A0E8 /* FieldBackgroundExecution.swift in Sources */ = {isa = PBXBuildFile; fileRef = C614F2A8813E63C85261F492 /* FieldBackgroundExecution.swift */; };
B8A3BBDE3A1FC0248512BF76 /* LoggingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */; };
B971351ABE8E79A472B4DC7D /* View+Nav.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */; };
@@ -47,6 +48,7 @@
CC5561169A29B5B2B6423959 /* FieldTelemetryUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */; };
D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */; };
D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A4289F43625DD65E6C4B25 /* CopyRow.swift */; };
+ D3E08BD0EB07C4E687BDAEF0 /* FieldBackgroundURLSessionEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA5C6DE4731C46D53B757E3 /* FieldBackgroundURLSessionEvents.swift */; };
D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */; };
D5C58A98C950D45AD027962A /* TradeListing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D69D200DB1F5FA7AA561CD7 /* TradeListing.swift */; };
D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227028B4EBDC6703999FB9DA /* ToastModifier.swift */; };
@@ -68,6 +70,7 @@
138AA7BAA021EE13E829390B /* Bundle+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Build.swift"; sourceTree = "<group>"; };
15DBA726450712D6DE88E951 /* RadrootsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsProvider.swift; sourceTree = "<group>"; };
16A7641E5C643B4B36CFEDA8 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
+ 19D317BC9F759709098490DD /* RadrootsAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsAppDelegate.swift; sourceTree = "<group>"; };
227028B4EBDC6703999FB9DA /* ToastModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastModifier.swift; sourceTree = "<group>"; };
26BAE32CD1D46033DDA1A5BB /* TradeListingDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListingDetailView.swift; sourceTree = "<group>"; };
2818363B157125491FB84A1E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
@@ -89,6 +92,7 @@
7CC1D11E2E296B4B54E7E8A9 /* FieldCaptureIntake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCaptureIntake.swift; sourceTree = "<group>"; };
7D69D200DB1F5FA7AA561CD7 /* TradeListing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListing.swift; sourceTree = "<group>"; };
8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldSecureIdentityStore.swift; sourceTree = "<group>"; };
+ 8DA5C6DE4731C46D53B757E3 /* FieldBackgroundURLSessionEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldBackgroundURLSessionEvents.swift; sourceTree = "<group>"; };
8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldTelemetryUITestProbe.swift; sourceTree = "<group>"; };
8F0F21496E7A8490EB14AC5B /* Radroots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Radroots.swift; sourceTree = "<group>"; };
93AA285819DD1269C3EAD80A /* Radroots.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Radroots.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -143,6 +147,7 @@
2818363B157125491FB84A1E /* App.swift */,
D448C9655B708CA3FA8712B9 /* AppEntry.swift */,
CBE1472FFD63A33F3AEA6C6C /* AppState.swift */,
+ 19D317BC9F759709098490DD /* RadrootsAppDelegate.swift */,
15DBA726450712D6DE88E951 /* RadrootsProvider.swift */,
);
path = App;
@@ -198,6 +203,7 @@
A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */,
C614F2A8813E63C85261F492 /* FieldBackgroundExecution.swift */,
59AC0543EF8335D691D56BD3 /* FieldBackgroundExecutionUITestProbe.swift */,
+ 8DA5C6DE4731C46D53B757E3 /* FieldBackgroundURLSessionEvents.swift */,
7CC1D11E2E296B4B54E7E8A9 /* FieldCaptureIntake.swift */,
E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */,
EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */,
@@ -436,6 +442,7 @@
D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */,
ABBA5CC10933CA087E14A0E8 /* FieldBackgroundExecution.swift in Sources */,
2FAE0FC43EB547F2CE7A567D /* FieldBackgroundExecutionUITestProbe.swift in Sources */,
+ D3E08BD0EB07C4E687BDAEF0 /* FieldBackgroundURLSessionEvents.swift in Sources */,
299A6111507F657670856F36 /* FieldCaptureIntake.swift in Sources */,
3FC570AC038C3DC575E5A3E7 /* FieldDocumentInterchange.swift in Sources */,
E432FD39ECC8F03764EEED81 /* FieldDocumentInterchangeUITestProbe.swift in Sources */,
@@ -459,6 +466,7 @@
5AECD474FB2F91855BDD79C0 /* PostFeedViewModel.swift in Sources */,
275D4D574BF3B3C1DD746CE7 /* ProfileView.swift in Sources */,
8F6D0970610DF68816DE1A98 /* Radroots.swift in Sources */,
+ A54E244A554EC6B46DF8DE48 /* RadrootsAppDelegate.swift in Sources */,
7C8DD424F3E3E0AB1B133863 /* RadrootsKitBindings.swift in Sources */,
E1EDAEE6B182025ACAF754A6 /* RadrootsProvider.swift in Sources */,
3A7FA9E5BCC7590B2EAC5349 /* RelaySettings.swift in Sources */,
diff --git a/Radroots/App/App.swift b/Radroots/App/App.swift
@@ -2,6 +2,8 @@ import SwiftUI
@main
struct RadrootsApp: App {
+ @UIApplicationDelegateAdaptor(RadrootsAppDelegate.self) private var appDelegate
+
var body: some Scene {
WindowGroup {
RadrootsProvider {
diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift
@@ -121,6 +121,7 @@ public final class AppState: ObservableObject {
telemetry: telemetry
)
self.backgroundExecution = backgroundExecution
+ await FieldBackgroundURLSessionEvents.shared.attach(backgroundExecution)
try FieldFileAccessUITestProbe.seedDestructiveResetSentinelIfRequested(
bundleIdentifier: appBundleIdentifier,
resetLocalStateRequested: resetLocalStateRequested
diff --git a/Radroots/App/RadrootsAppDelegate.swift b/Radroots/App/RadrootsAppDelegate.swift
@@ -0,0 +1,28 @@
+import UIKit
+
+final class RadrootsAppDelegate: NSObject, UIApplicationDelegate {
+ func application(
+ _ application: UIApplication,
+ handleEventsForBackgroundURLSession identifier: String,
+ completionHandler: @escaping () -> Void
+ ) {
+ let completion = FieldBackgroundURLSessionCompletion(completionHandler)
+ Task {
+ await FieldBackgroundURLSessionEvents.shared.handleEvents(identifier: identifier) {
+ completion.complete()
+ }
+ }
+ }
+}
+
+private final class FieldBackgroundURLSessionCompletion: @unchecked Sendable {
+ private let completionHandler: () -> Void
+
+ init(_ completionHandler: @escaping () -> Void) {
+ self.completionHandler = completionHandler
+ }
+
+ func complete() {
+ completionHandler()
+ }
+}
diff --git a/Radroots/Runtime/FieldBackgroundExecution.swift b/Radroots/Runtime/FieldBackgroundExecution.swift
@@ -196,6 +196,17 @@ actor FieldBackgroundExecution {
}
}
+ func handleEventsForBackgroundURLSession(
+ identifier: String,
+ completionHandler: @escaping @Sendable () -> Void
+ ) async {
+ await transfer.handleEventsForBackgroundURLSession(
+ identifier: identifier,
+ completionHandler: completionHandler
+ )
+ telemetry.backgroundExecution(operation: "background_url_session_events", outcome: "success")
+ }
+
func uiTestProbeValue() async -> String? {
guard FieldBackgroundExecutionUITestProbe.isRequested else {
return nil
diff --git a/Radroots/Runtime/FieldBackgroundExecutionUITestProbe.swift b/Radroots/Runtime/FieldBackgroundExecutionUITestProbe.swift
@@ -51,12 +51,20 @@ enum FieldBackgroundExecutionUITestProbe {
[
"registered=false",
"scheduled_task_count=0",
+ "schedule_observed=false",
"pending_before_maintenance=0",
"pending_before_cancel=0",
"pending_after_cancel=0",
"cancellation_observed=false",
"staged_blob_removed=false",
"transfer_snapshot_count=0",
+ "background_register_seen=false",
+ "background_schedule_seen=false",
+ "background_cancel_all_seen=false",
+ "background_transfer_inspect_seen=false",
+ "background_staged_blob_sweep_seen=false",
+ "background_relay_refresh_seen=false",
+ "background_maintenance_seen=false",
"probe_failure_outcome=\(outcome)",
"unsafe_values_present=false",
"relay_url_values_present=false",
diff --git a/Radroots/Runtime/FieldBackgroundURLSessionEvents.swift b/Radroots/Runtime/FieldBackgroundURLSessionEvents.swift
@@ -0,0 +1,48 @@
+import Foundation
+
+actor FieldBackgroundURLSessionEvents {
+ static let shared = FieldBackgroundURLSessionEvents()
+
+ private var backgroundExecution: FieldBackgroundExecution?
+ private var pendingEvents: [FieldPendingBackgroundURLSessionEvent]
+
+ private init() {
+ self.pendingEvents = []
+ }
+
+ func attach(_ backgroundExecution: FieldBackgroundExecution) async {
+ self.backgroundExecution = backgroundExecution
+ let events = pendingEvents
+ pendingEvents = []
+ for event in events {
+ await backgroundExecution.handleEventsForBackgroundURLSession(
+ identifier: event.identifier,
+ completionHandler: event.completionHandler
+ )
+ }
+ }
+
+ func handleEvents(
+ identifier: String,
+ completionHandler: @escaping @Sendable () -> Void
+ ) async {
+ guard let backgroundExecution else {
+ pendingEvents.append(
+ FieldPendingBackgroundURLSessionEvent(
+ identifier: identifier,
+ completionHandler: completionHandler
+ )
+ )
+ return
+ }
+ await backgroundExecution.handleEventsForBackgroundURLSession(
+ identifier: identifier,
+ completionHandler: completionHandler
+ )
+ }
+}
+
+private struct FieldPendingBackgroundURLSessionEvent: Sendable {
+ let identifier: String
+ let completionHandler: @Sendable () -> Void
+}