Skip to content

Commit d320e19

Browse files
committed
fix(ios): serialize VoipService bridge statics on a serial queue
Route lastVoipToken and initialEventsData through bridgeStateQueue so PushKit /main writers and React Native bridge readers cannot race. Document that all other static state remains main-thread-only per VoipService call-site review. PR 3b from voip-pr-6918-review split plan (H1 narrowed). Made-with: Cursor
1 parent abefd00 commit d320e19

1 file changed

Lines changed: 60 additions & 32 deletions

File tree

ios/Libraries/VoipService.swift

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import PushKit
66
* VoipModuleSwift - Swift implementation for VoIP push notifications and initial events data.
77
* This class provides static methods called by VoipModule.mm (the TurboModule bridge).
88
*
9+
* Threading:
10+
* - `lastVoipToken` and `initialEventsData` are synchronized on `bridgeStateQueue` because they are
11+
* written on the main thread (PushKit, native call flows) and read from the React Native bridge thread.
12+
* - **All other static state is main-thread-only** (PushKit registry on `.main`, `CXCallObserver` on
13+
* `.main`, and `trackIncomingCall` / `clearTrackedIncomingCall` already bounce work to main where needed).
14+
*
915
* This module:
1016
* - Manages PushKit VoIP registration
1117
* - Tracks VoIP push tokens
@@ -28,7 +34,9 @@ public final class VoipService: NSObject {
2834
private static let TAG = "RocketChat.VoipService"
2935
private static let voipTokenStorageKey = "RCVoipPushToken"
3036
private static let storage = MMKVBridge.build()
31-
37+
/// Serializes access to `lastVoipToken` and `initialEventsData` (main-thread writers vs RN bridge readers).
38+
private static let bridgeStateQueue = DispatchQueue(label: "chat.rocket.ios.voipService.bridgeState")
39+
3240
// MARK: - Static Properties
3341

3442
private static var initialEventsData: VoipPayload?
@@ -67,7 +75,8 @@ public final class VoipService: NSObject {
6775
public static func voipRegistration() {
6876
if isVoipRegistered {
6977
#if DEBUG
70-
print("[\(TAG)] voipRegistration already registered. Returning lastVoipToken: \(lastVoipToken)")
78+
let tokenSnapshot = bridgeStateQueue.sync { lastVoipToken }
79+
print("[\(TAG)] voipRegistration already registered. Returning lastVoipToken: \(tokenSnapshot)")
7180
#endif
7281
return
7382
}
@@ -100,14 +109,21 @@ public final class VoipService: NSObject {
100109
// Convert token data to hex string
101110
let token = credentials.token.map { String(format: "%02x", $0) }.joined()
102111

103-
if lastVoipToken == token {
112+
let tokenUnchanged = bridgeStateQueue.sync { () -> Bool in
113+
if lastVoipToken == token {
114+
return true
115+
}
116+
lastVoipToken = token
117+
return false
118+
}
119+
120+
if tokenUnchanged {
104121
#if DEBUG
105122
print("[\(TAG)] VoIP token unchanged")
106123
#endif
107124
return
108125
}
109126

110-
lastVoipToken = token
111127
persistVoipToken(token)
112128

113129
#if DEBUG
@@ -126,7 +142,9 @@ public final class VoipService: NSObject {
126142
// TODO: remove voip token from all logged in workspaces, since they share the same token
127143
@objc
128144
public static func invalidatePushToken() {
129-
lastVoipToken = ""
145+
bridgeStateQueue.sync {
146+
lastVoipToken = ""
147+
}
130148
storage.removeValue(forKey: voipTokenStorageKey)
131149

132150
#if DEBUG
@@ -161,39 +179,45 @@ public final class VoipService: NSObject {
161179
/// Stores initial events for JS to retrieve.
162180
@objc
163181
public static func storeInitialEvents(_ payload: VoipPayload) {
164-
initialEventsData = payload
165-
166-
#if DEBUG
167-
print("[\(TAG)] Stored initial events: \(payload.callId)")
168-
#endif
182+
bridgeStateQueue.sync {
183+
initialEventsData = payload
184+
185+
#if DEBUG
186+
print("[\(TAG)] Stored initial events: \(payload.callId)")
187+
#endif
188+
}
169189
}
170190

171191
/// Gets any initial events. Returns nil if no initial events.
172192
@objc
173193
public static func getInitialEvents() -> [String: Any]? {
174-
guard let data = initialEventsData else {
175-
return nil
176-
}
177-
178-
if data.isExpired() {
179-
clearInitialEventsInternal()
180-
return nil
194+
bridgeStateQueue.sync {
195+
guard let data = initialEventsData else {
196+
return nil
197+
}
198+
199+
if data.isExpired() {
200+
clearInitialEventsUnlocked()
201+
return nil
202+
}
203+
204+
let result = data.toDictionary()
205+
clearInitialEventsUnlocked()
206+
207+
return result
181208
}
182-
183-
let result = data.toDictionary()
184-
clearInitialEventsInternal()
185-
186-
return result
187209
}
188-
210+
189211
/// Clears any initial events
190212
@objc
191213
public static func clearInitialEvents() {
192-
clearInitialEventsInternal()
214+
bridgeStateQueue.sync {
215+
clearInitialEventsUnlocked()
216+
}
193217
}
194-
195-
/// Clears initial events (internal)
196-
private static func clearInitialEventsInternal() {
218+
219+
/// Clears initial events. Caller must already be running on `bridgeStateQueue`.
220+
private static func clearInitialEventsUnlocked() {
197221
initialEventsData = nil
198222
#if DEBUG
199223
print("[\(TAG)] Cleared initial events")
@@ -205,10 +229,12 @@ public final class VoipService: NSObject {
205229
/// Returns the last registered VoIP token
206230
@objc
207231
public static func getLastVoipToken() -> String {
208-
if lastVoipToken.isEmpty {
209-
lastVoipToken = loadPersistedVoipToken()
232+
bridgeStateQueue.sync {
233+
if lastVoipToken.isEmpty {
234+
lastVoipToken = loadPersistedVoipToken()
235+
}
236+
return lastVoipToken
210237
}
211-
return lastVoipToken
212238
}
213239

214240
private static func loadPersistedVoipToken() -> String {
@@ -502,8 +528,10 @@ public final class VoipService: NSObject {
502528
cancelIncomingCallTimeout(for: payload.callId)
503529
clearTrackedIncomingCall(for: payload.callUUID)
504530

505-
if initialEventsData?.callId == payload.callId {
506-
clearInitialEventsInternal()
531+
bridgeStateQueue.sync {
532+
if initialEventsData?.callId == payload.callId {
533+
clearInitialEventsUnlocked()
534+
}
507535
}
508536

509537
// End the just-reported CallKit call immediately (reason 2 = unanswered / declined).

0 commit comments

Comments
 (0)