Skip to content

[iOS] fix events lost before module initialized - #877

Merged
sboily merged 2 commits into
react-native-webrtc:masterfrom
minhuwv:minhu/fix_ios_lost_event_before_module_initialized
Oct 17, 2026
Merged

sboily merged 2 commits into
react-native-webrtc:masterfrom
minhuwv:minhu/fix_ios_lost_event_before_module_initialized

Conversation

@minhuwv

@minhuwv minhuwv commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

_delayedEvents is normally initialized in init. However, when the app is launched from a terminated state, reportNewIncomingCall can be invoked before init.
In this case, _delayedEvents is nil, so the incoming call event is not stored. When getInitialEvents is called early during app startup, it therefore returns an empty list.

Reproduce Log:

c99177_image

@sboily sboily self-assigned this Oct 16, 2026
@sboily sboily added the bug Something isn't working label Oct 16, 2026
@sboily

sboily commented Oct 16, 2026

Copy link
Copy Markdown
Member

Thanks for this, the diagnosis is right. I reproduced it before reviewing.

Reproduction. I compiled the real RNCallKeep.m (master and this PR) for Mac Catalyst against the real CallKit, with minimal React stubs. The test follows the AppDelegate path: +reportNewIncomingCall: runs before the module exists, then the bridge creates the module with -init on the main thread, then JS calls getInitialEvents. CallKit won't show calls for an unsigned process, so only the provider's completion is simulated, the way CXProvider.h documents it: "Completion block will be called on delegate queue, if specified, otherwise on a private serial queue". Before -init, no delegate is set.

  • master: getInitialEvents returns 0 events
  • this PR: getInitialEvents returns 1 event (RNCallKeepDidDisplayIncomingCall)

So the fix works, and -init keeps the buffered event thanks to its existing if (_delayedEvents == nil) check.

One problem: the lazy init races with -init. That completion runs on CallKit's private queue while the bridge runs -init on the main thread. Both run if (_delayedEvents == nil) _delayedEvents = [NSMutableArray array]; on the same ivar with no synchronization. If they overlap, -init replaces the array that just received the incoming-call event, and the event is lost again. I ran both concurrently on the singleton, shifting the start of -init from 0 to 40 µs across iterations. The event was lost in 102, 97 and 128 of 40,000 iterations (three runs). That's rare, but it's exactly the event this PR is meant to keep.

Suggestion: create the array once, where the singleton is allocated, and drop the lazy init:

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

dispatch_once makes it thread-safe, and it also covers the other path that buffers an event before -init (+application:continueUserActivity:). With this change, the same harness gives 1 event on the cold-start test and 0 lost in 120,000 iterations on the race test.

A separate, pre-existing issue: getInitialEvents resolves the live NSMutableArray, which React Native converts on the JS thread while other threads may still be appending to it. I've raised it on #878, where it's easier to trigger.

@sboily

sboily commented Oct 16, 2026

Copy link
Copy Markdown
Member

Update: I ran the same tests on a real iPhone 15 Pro Max (iOS 26.6.1) with the real CallKit. Nothing is simulated this time, including the incoming call.

  • Before -init, the reportNewIncomingCall completion runs on com.apple.callkit.callprovider.completion, not on the main thread.
  • Cold start, getInitialEvents: master 0 events, this PR 1 (RNCallKeepDidDisplayIncomingCall), the allocWithZone suggestion 1.
  • Race with -init: the event was lost 90 and 83 times in 40,000 iterations with this PR, and 0 times with the allocWithZone suggestion.

@minhuwv

minhuwv commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Hi @sboily thanks for your review and suggestion , I have pushed the changes.

@sboily sboily left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @minhuwv! This is byte-for-byte the version I tested: the incoming call reported before -init is kept, and no event was lost in the race test (40,000 iterations on an iPhone 15 Pro Max, 120,000 on Mac Catalyst).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants