@@ -200,6 +200,8 @@ final class NodeAppModel {
200200 private( set) var activeGatewayConnectConfig : GatewayConnectConfig ?
201201
202202 private static let watchExecApprovalBridgeStateKey = " watch.execApproval.bridge.state.v1 "
203+ private static let backgroundAliveLastSuccessAtMsKey = " gateway.backgroundAlive.lastSuccessAtMs "
204+ private static let backgroundAliveLastTriggerKey = " gateway.backgroundAlive.lastTrigger "
203205
204206 var cameraHUDText : String ?
205207 var cameraHUDKind : CameraHUDKind ?
@@ -3142,32 +3144,39 @@ extension NodeAppModel {
31423144 return handled
31433145 }
31443146
3145- let result = await self . reconnectGatewaySessionsForSilentPushIfNeeded ( wakeId: wakeId)
3147+ let result = await self . performBackgroundAliveBeaconIfNeeded (
3148+ wakeId: wakeId,
3149+ trigger: . silentPush)
31463150 let outcomeMessage =
31473151 " Silent push outcome wakeId= \( wakeId) "
31483152 + " applied= \( result. applied) "
3153+ + " handled= \( result. handled) "
31493154 + " reason= \( result. reason) "
31503155 + " durationMs= \( result. durationMs) "
31513156 self . pushWakeLogger. info ( " \( outcomeMessage, privacy: . public) " )
3152- return result. applied
3157+ return result. handled
31533158 }
31543159
31553160 func handleBackgroundRefreshWake( trigger: String = " bg_app_refresh " ) async -> Bool {
31563161 let wakeId = Self . makePushWakeAttemptID ( )
3162+ let normalizedTrigger = BackgroundAliveBeacon . normalizeTrigger ( trigger)
31573163 let receivedMessage =
31583164 " Background refresh wake received wakeId= \( wakeId) "
3159- + " trigger= \( trigger ) "
3165+ + " trigger= \( normalizedTrigger . rawValue ) "
31603166 + " backgrounded= \( self . isBackgrounded) "
31613167 + " autoReconnect= \( self . gatewayAutoReconnectEnabled) "
31623168 self . pushWakeLogger. info ( " \( receivedMessage, privacy: . public) " )
3163- let result = await self . reconnectGatewaySessionsForSilentPushIfNeeded ( wakeId: wakeId)
3169+ let result = await self . performBackgroundAliveBeaconIfNeeded (
3170+ wakeId: wakeId,
3171+ trigger: normalizedTrigger)
31643172 let outcomeMessage =
31653173 " Background refresh wake outcome wakeId= \( wakeId) "
31663174 + " applied= \( result. applied) "
3175+ + " handled= \( result. handled) "
31673176 + " reason= \( result. reason) "
31683177 + " durationMs= \( result. durationMs) "
31693178 self . pushWakeLogger. info ( " \( outcomeMessage, privacy: . public) " )
3170- return result. applied
3179+ return result. handled
31713180 }
31723181
31733182 func handleSignificantLocationWakeIfNeeded( ) async {
@@ -3196,10 +3205,13 @@ extension NodeAppModel {
31963205 + " backgrounded= \( self . isBackgrounded) "
31973206 + " autoReconnect= \( self . gatewayAutoReconnectEnabled) "
31983207 self . locationWakeLogger. info ( " \( beginMessage, privacy: . public) " )
3199- let result = await self . reconnectGatewaySessionsForSilentPushIfNeeded ( wakeId: wakeId)
3208+ let result = await self . performBackgroundAliveBeaconIfNeeded (
3209+ wakeId: wakeId,
3210+ trigger: . significantLocation)
32003211 let triggerMessage =
32013212 " Location wake trigger wakeId= \( wakeId) "
32023213 + " applied= \( result. applied) "
3214+ + " handled= \( result. handled) "
32033215 + " reason= \( result. reason) "
32043216 + " durationMs= \( result. durationMs) "
32053217 self . locationWakeLogger. info ( " \( triggerMessage, privacy: . public) " )
@@ -3621,8 +3633,9 @@ extension NodeAppModel {
36213633 return gatewayError. message. lowercased ( ) . contains ( " allow-always is unavailable " )
36223634 }
36233635
3624- private struct SilentPushWakeAttemptResult {
3636+ private struct BackgroundAliveWakeAttemptResult {
36253637 var applied : Bool
3638+ var handled : Bool
36263639 var reason : String
36273640 var durationMs : Int
36283641 }
@@ -3797,43 +3810,100 @@ extension NodeAppModel {
37973810 return await self . waitForOperatorConnection ( timeoutMs: timeoutMs, pollMs: 250 )
37983811 }
37993812
3800- private func reconnectGatewaySessionsForSilentPushIfNeeded(
3801- wakeId: String ) async -> SilentPushWakeAttemptResult
3813+ private func performBackgroundAliveBeaconIfNeeded(
3814+ wakeId: String ,
3815+ trigger: BackgroundAliveBeacon . Trigger ) async -> BackgroundAliveWakeAttemptResult
38023816 {
38033817 let startedAt = Date ( )
3804- let makeResult : ( Bool , String ) -> SilentPushWakeAttemptResult = { applied, reason in
3818+ let makeResult : ( Bool , Bool , String ) -> BackgroundAliveWakeAttemptResult = { applied, handled , reason in
38053819 let durationMs = Int ( Date ( ) . timeIntervalSince ( startedAt) * 1000 )
3806- return SilentPushWakeAttemptResult (
3820+ return BackgroundAliveWakeAttemptResult (
38073821 applied: applied,
3822+ handled: handled,
38083823 reason: reason,
38093824 durationMs: max ( 0 , durationMs) )
38103825 }
38113826
38123827 guard self . isBackgrounded else {
38133828 self . pushWakeLogger. info ( " Wake no-op wakeId= \( wakeId, privacy: . public) : app not backgrounded " )
3814- return makeResult ( false , " not_backgrounded " )
3829+ return makeResult ( false , false , " not_backgrounded " )
38153830 }
38163831 guard self . gatewayAutoReconnectEnabled else {
38173832 self . pushWakeLogger. info ( " Wake no-op wakeId= \( wakeId, privacy: . public) : auto reconnect disabled " )
3818- return makeResult ( false , " auto_reconnect_disabled " )
3833+ return makeResult ( false , false , " auto_reconnect_disabled " )
38193834 }
3820- guard let cfg = self . activeGatewayConnectConfig else {
3821- self . pushWakeLogger. info ( " Wake no-op wakeId= \( wakeId, privacy: . public) : no active gateway config " )
3822- return makeResult ( false , " no_active_gateway_config " )
3835+ let now = Date ( )
3836+ let gatewayConnected = await self . isGatewayConnected ( )
3837+
3838+ var appliedReconnect = false
3839+ if !gatewayConnected {
3840+ guard let cfg = self . activeGatewayConnectConfig else {
3841+ self . pushWakeLogger. info ( " Wake no-op wakeId= \( wakeId, privacy: . public) : no active gateway config " )
3842+ return makeResult ( false , false , " no_active_gateway_config " )
3843+ }
3844+ self . pushWakeLogger. info (
3845+ " Wake reconnect begin wakeId= \( wakeId, privacy: . public) stableID= \( cfg. stableID, privacy: . public) " )
3846+ self . grantBackgroundReconnectLease ( seconds: 30 , reason: " wake_ \( wakeId) " )
3847+ await self . operatorGateway. disconnect ( )
3848+ await self . nodeGateway. disconnect ( )
3849+ self . operatorConnected = false
3850+ self . gatewayConnected = false
3851+ self . gatewayStatusText = " Reconnecting… "
3852+ self . talkMode. updateGatewayConnected ( false )
3853+ self . applyGatewayConnectConfig ( cfg)
3854+ appliedReconnect = true
3855+ self . pushWakeLogger. info ( " Wake reconnect trigger applied wakeId= \( wakeId, privacy: . public) " )
3856+
3857+ let connected = await self . waitForGatewayConnection ( timeoutMs: 12000 , pollMs: 250 )
3858+ guard connected else {
3859+ return makeResult ( appliedReconnect, false , " connect_timeout " )
3860+ }
3861+ } else if BackgroundAliveBeacon . shouldSkipRecentSuccess (
3862+ isGatewayConnected: true ,
3863+ now: now,
3864+ lastSuccessAtMs: UserDefaults . standard. object ( forKey: Self . backgroundAliveLastSuccessAtMsKey) as? Double )
3865+ {
3866+ return makeResult ( false , true , " recent_success " )
38233867 }
38243868
3825- self . pushWakeLogger. info (
3826- " Wake reconnect begin wakeId= \( wakeId, privacy: . public) stableID= \( cfg. stableID, privacy: . public) " )
3827- self . grantBackgroundReconnectLease ( seconds: 30 , reason: " wake_ \( wakeId) " )
3828- await self . operatorGateway. disconnect ( )
3829- await self . nodeGateway. disconnect ( )
3830- self . operatorConnected = false
3831- self . gatewayConnected = false
3832- self . gatewayStatusText = " Reconnecting… "
3833- self . talkMode. updateGatewayConnected ( false )
3834- self . applyGatewayConnectConfig ( cfg)
3835- self . pushWakeLogger. info ( " Wake reconnect trigger applied wakeId= \( wakeId, privacy: . public) " )
3836- return makeResult ( true , " reconnect_triggered " )
3869+ let beacon = await self . publishBackgroundAliveBeacon ( trigger: trigger)
3870+ if beacon. handled {
3871+ let successAtMs = Date ( ) . timeIntervalSince1970 * 1000
3872+ UserDefaults . standard. set ( successAtMs, forKey: Self . backgroundAliveLastSuccessAtMsKey)
3873+ UserDefaults . standard. set ( trigger. rawValue, forKey: Self . backgroundAliveLastTriggerKey)
3874+ return makeResult ( appliedReconnect, true , beacon. reason)
3875+ }
3876+ return makeResult ( appliedReconnect, false , beacon. reason)
3877+ }
3878+
3879+ private func publishBackgroundAliveBeacon(
3880+ trigger: BackgroundAliveBeacon . Trigger ) async -> ( handled: Bool , reason: String )
3881+ {
3882+ do {
3883+ let pushTransport = await self . pushRegistrationManager. usesRelayTransport ? " relay " : " direct "
3884+ let displayName = NodeDisplayName . resolve (
3885+ existing: UserDefaults . standard. string ( forKey: " node.displayName " ) ,
3886+ deviceName: UIDevice . current. name,
3887+ interfaceIdiom: UIDevice . current. userInterfaceIdiom)
3888+ let payload = BackgroundAliveBeacon . makePayload (
3889+ trigger: trigger,
3890+ displayName: displayName,
3891+ pushTransport: pushTransport)
3892+ let paramsJSON = try BackgroundAliveBeacon . makeNodeEventRequestPayloadJSON ( payload: payload)
3893+ let response = try await self . nodeGateway. request (
3894+ method: " node.event " ,
3895+ paramsJSON: paramsJSON,
3896+ timeoutSeconds: 8 )
3897+ guard let decoded = BackgroundAliveBeacon . decodeResponse ( response) else {
3898+ return ( false , " invalid_response " )
3899+ }
3900+ if decoded. handled == true {
3901+ return ( true , decoded. reason ?? " beacon_persisted " )
3902+ }
3903+ return ( false , decoded. reason ?? " unsupported " )
3904+ } catch {
3905+ return ( false , " beacon_failed " )
3906+ }
38373907 }
38383908}
38393909
0 commit comments