@@ -84,17 +84,19 @@ - (instancetype)initWithDaemonConnection:(MOLXPCConnection*)daemonConn {
8484 if (config.fcmEnabled ) {
8585 LOGD (@" Using FCM push notifications" );
8686 _pushNotifications = [[SNTPushClientFCM alloc ] initWithSyncDelegate: self ];
87- } else if (config.enablePushNotifications ) {
88- // Unless explicitly disabled, initialize the NATS push client. The client
89- // will wait for configuration during a valid V2 preflight.
90- LOGD (@" Using NATS push notifications" );
91- _pushNotifications = [[SNTPushClientNATS alloc ] initWithSyncDelegate: self ];
9287 }
88+ // NATS push client is created dynamically in preflightWithSyncState: after
89+ // the server provides a validated token chain (isSyncV2) and push config.
9390
9491 _fullSyncTimer = [self createSyncTimerWithBlock: ^{
95- [self rescheduleTimerQueue: self .fullSyncTimer
96- secondsFromNow: _pushNotifications ? _pushNotifications.fullSyncInterval
97- : kDefaultFullSyncInterval ];
92+ // Reschedule as a fallback before starting the sync. On success,
93+ // preflightWithSyncState: will override with the server-informed interval.
94+ // On failure, the failed-preflight path corrects it as well. This is just
95+ // a safety net for the case where the sync fails before reaching any
96+ // rescheduling logic.
97+ NSUInteger interval = self.pushNotifications ? self.pushNotifications .fullSyncInterval
98+ : kDefaultFullSyncInterval ;
99+ [self rescheduleTimerQueue: self .fullSyncTimer secondsFromNow: interval];
98100 [self syncType: SNTSyncTypeNormal withReply: NULL ];
99101 }];
100102 _ruleSyncTimer = [self createSyncTimerWithBlock: ^{
@@ -523,47 +525,76 @@ - (SNTSyncStatusType)preflightWithSyncState:(SNTSyncState*)syncState {
523525
524526 self.eventBatchSize = syncState.eventBatchSize ;
525527
526- // Start listening for push notifications with a full sync every
527- // pushNotificationsFullSyncInterval.
528+ // Dynamic NATS lifecycle: create when server validates sync v2,
529+ // tear down when sync v2 is no longer valid.
530+ if (syncState.isSyncV2 && !self.pushNotifications ) {
531+ // Check kill switch — respect explicit admin disable
532+ if ([[SNTConfigurator configurator ] enablePushNotifications ]) {
533+ LOGI (@" Creating NATS push client after successful sync v2 preflight" );
534+ self.pushNotifications = [[SNTPushClientNATS alloc ] initWithSyncDelegate: self ];
535+ }
536+ } else if (!syncState.isSyncV2 && self.pushNotifications &&
537+ [self .pushNotifications isKindOfClass: [SNTPushClientNATS class ]]) {
538+ // Token chain no longer valid — tear down NATS client.
539+ // The NATS C library holds unretained pointers to the client via __bridge
540+ // callbacks, so we must disconnect (which destroys the C-level connection)
541+ // before the object is deallocated. disconnectWithCompletion: dispatches
542+ // async onto connectionQueue and the block's implicit capture of self (the
543+ // NATS client) keeps it alive until cleanup completes.
544+ LOGI (@" Sync v2 no longer active, tearing down NATS push client" );
545+ [(SNTPushClientNATS*)self .pushNotifications disconnectWithCompletion: nil ];
546+ self.pushNotifications = nil ;
547+ }
548+
549+ // pushNotificationsFullSyncInterval is only meaningful when push notifications
550+ // are in use. Clear it otherwise so the postflight persists 0 to the daemon,
551+ // overriding any stale default (14400).
552+ if (!self.pushNotifications ) {
553+ syncState.pushNotificationsFullSyncInterval = @(0 );
554+ }
555+
556+ // Hand off push credentials to the push client (if present).
528557 if (self.pushNotifications ) {
529558 NSUInteger oldInterval = self.pushNotifications .fullSyncInterval ;
530559 [self .pushNotifications handlePreflightSyncState: syncState];
531560
532- // Clear all push credentials from syncState after handoff to push client
533- // These are no longer needed and should not be accessible to other sync stages
561+ // Clear all push credentials from syncState after handoff to push client.
562+ // These are no longer needed and should not be accessible to other sync stages.
534563 syncState.pushNKey = nil ;
535564 syncState.pushJWT = nil ;
536565 syncState.pushIssuerJWT = nil ;
537566 syncState.pushHMACKey = nil ;
538567
539- // If push interval changed, mark log the difference.
540568 if (oldInterval != self.pushNotifications .fullSyncInterval ) {
541569 LOGD (
542570 @" Push notification sync interval changed from %lu to %lu seconds. Rescheduling timer." ,
543571 oldInterval, self.pushNotifications .fullSyncInterval );
544572 }
573+ }
545574
546- // Always reschedule
575+ // Use the push notification interval when the server provided one and we
576+ // have an active push client. syncState.pushNotificationsFullSyncInterval
577+ // is nil when the server does not set push_notification_full_sync_interval_seconds
578+ // (e.g. sync v1). In that case, fall back to the server's regular full_sync_interval.
579+ if (self.pushNotifications && syncState.pushNotificationsFullSyncInterval ) {
547580 [self rescheduleTimerQueue: self .fullSyncTimer
548581 secondsFromNow: self .pushNotifications.fullSyncInterval];
549582 } else {
550583 NSUInteger interval = syncState.fullSyncInterval
551584 ? syncState.fullSyncInterval .unsignedIntegerValue
552585 : kDefaultFullSyncInterval ;
553- LOGD (@" Push notifications are not enabled. Sync every %lu min." , interval / 60 );
554-
555- // Always reschedule
586+ LOGD (@" Push notifications not configured by server. Sync every %lu min." , interval / 60 );
556587 [self rescheduleTimerQueue: self .fullSyncTimer secondsFromNow: interval];
557588 }
558589
559590 if (syncState.preflightOnly ) return SNTSyncStatusTypeSuccess;
560591 return [self eventUploadWithSyncState: syncState];
561- } else if (_pushNotifications ) {
592+ } else if (self. pushNotifications ) {
562593 // If preflight failed and push notifications are enabled, force a reschedule for
563594 // the smaller of the default sync interval (default 10 minutes) and whatever the
564595 // last push full sync interval was set to (default 4 hours).
565596 // If push notifications are not enabled, the default sync interval was already set (10m).
566- auto interval = std::min (_pushNotifications .fullSyncInterval , kDefaultFullSyncInterval );
597+ auto interval = std::min (self. pushNotifications .fullSyncInterval , kDefaultFullSyncInterval );
567598 [self rescheduleTimerQueue: self .fullSyncTimer secondsFromNow: interval];
568599 }
569600
@@ -621,6 +652,10 @@ - (dispatch_source_t)createSyncTimerWithBlock:(void (^)(void))block {
621652 block ();
622653 }
623654 });
655+ // Arm with DISPATCH_TIME_FOREVER so the timer does not fire until explicitly
656+ // scheduled via rescheduleTimerQueue:secondsFromNow: (e.g. syncSecondsFromNow:15).
657+ // Without this, the default fire time races with the initial schedule call.
658+ dispatch_source_set_timer (timerQueue, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 0 );
624659 dispatch_resume (timerQueue);
625660 return timerQueue;
626661}
0 commit comments