react-native-callkeep: Bridge Invalidation breaks RNCallKeep RTCEventEmitter on recent RN versions

Bug report

  • I’ve checked the example to reproduce the issue.

  • Reproduced on:

  • Android

  • iOS

Description

Issue specific til RN version 0.64 (and probably some versions prior).

This issue is created mainly because I’d like to discuss how to resolve this issue before writing code that does it. I’m not an expert on writing modules for react native on IOS - I’m more experienced with android in that regard. But I do have a solution in mind.

This issue stems from changes to react native made in this commit.

Because of the way RNCallKeep.m is put together, we’re running into an issue where an invalidate call across the RN bridge, causes stopObserving to be called on RCTEventEmitter, but it never calls startObserving again because of the way RCTEventEmitter.m is now put together internally.

Allow me to explain: When RNCallKeep.m is initialized, it’s done so as a Singleton:

+ (id)allocWithZone:(NSZone *)zone {
    static RNCallKeep *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [super allocWithZone:zone];
    });
    return sharedInstance;
}

This has a particularly unfortunate side effect - namely that dealloc is never called. This is on it’s own an issue, but I’m not sure how big of an issue. Before the commit I’ve linked further up, react native would use dealloc to decide if it wanted to call stopObserving. This method would never be called prior to that change, because a singleton cannot be deallocated by react native during a bridge invalidation.

However, because facebook now uses invalidate to do this check, they DO call stopObserving, causing us to set

- (void)stopObserving
{
    _hasListeners = FALSE;
}

BUT, because RNCallKeep is never deallocated, the _listenerCount variable is never reinitialized to zero (see this)

Because React Native internally performs this check, and that variable will be left at whatever value it had before the reload, they NEVER call startObserving again if the class that implements RTCEventEmitter is initialized as a singleton.

Proposed solution

I propose that we remove the RTCEventEmitter from RNCallKeep.m, and instead create a new class (perhaps RNCallKeepRTCEventEmitter.m?) that implements this interface, which is NOT initialized as a singleton. Instead RNCallKeep.m will get a new static function, registerRTCEventEmitter, which we call from RNCallKeepRTCEventEmitter.m with a reference to self on startObserving, and with nil on stopObserving

This way we can keep the singleton and have RN initialize and deinitialize the RTCEventEmitter as it needs.

Thoughts, concerns, ideas?

Steps to Reproduce

Reload app in Development Mode, and all handlers stops firing.

Versions

- Callkeep: All
- React Native: 0.64
- iOS: All

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 9
  • Comments: 26 (8 by maintainers)

Commits related to this issue

Most upvoted comments

@Romick2005 . This issue is relevant in two cases: DEV mode & over the air (OTA) updates in production.

In our case we don’t really care about DEV mode issue, since it does not impact users.

We do care about OTA scenario. We were using codepush (now switched to expo updates). When you are updating the JS code (https://docs.expo.dev/versions/latest/sdk/updates/#updatesreloadasync) it does not kill the app, it only invalidates the JS brigde and starts a new one. So in that case the workaround I added above helps to solve the issue.

@gormfrederiksen I fixed it with this patch on v4.3.8

diff --git a/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m b/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m
index e67c3cd..1da65f8 100644
--- a/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m
+++ b/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m
@@ -131,6 +131,17 @@ RCT_EXPORT_MODULE()
 - (void)stopObserving
 {
     _hasListeners = FALSE;
+
+    // Fix for https://github.com/react-native-webrtc/react-native-callkeep/issues/406
+    // We use Objective-C Key Value Coding(KVC) to sync _RTCEventEmitter_ `_listenerCount`.
+    @try {
+        [self setValue:@0 forKey:@"_listenerCount"];
+    } 
+    @catch ( NSException *e ){
+        NSLog(@"[RNCallKeep][stopObserving] exception: %@",e);
+        NSLog(@"[RNCallKeep][stopObserving] RNCallKeep parent class RTCEventEmitter might have a broken state.");
+        NSLog(@"[RNCallKeep][stopObserving] Please verify that the parent RTCEventEmitter.m has iVar `_listenerCount`.");
+    }
 }
 
 - (void)onAudioRouteChange:(NSNotification *)notification

@shawarmaz - we are not using codepush anymore. But the idea remains the same. I think you could listen for the status update callback to do the workaround.