SDK-392 Add defensive timeout to iOS initialize to prevent promise hang#833
SDK-392 Add defensive timeout to iOS initialize to prevent promise hang#833sumeruchat wants to merge 1 commit intomasterfrom
Conversation
…(SDK-392) On React Native New Architecture (bridgeless mode), the IterableAPI.initialize2 callback may never fire if auth token retry is exhausted in the native SDK, leaving the JS promise pending indefinitely. This adds a 10-second timeout that resolves the promise if the native callback is not invoked in time. The SDK still initializes and functions normally — the timeout only unblocks the JS promise so downstream app logic (e.g. iterableReady) is not stalled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Coverage Impact This PR will not change total coverage. 🚦 See full report on Qlty Cloud »🛟 Help
|
| // Architecture bridgeless mode, or network fetch hangs). | ||
| let timeoutWorkItem = DispatchWorkItem { | ||
| ITBInfo("initialize timeout reached – resolving promise to unblock JS") | ||
| resolveOnce(true) |
There was a problem hiding this comment.
This tells the JS layer initialization succeeded when it actually timed out. With the iOS fix now merged, this timeout should theoretically never fire. But if it does (some other unforeseen hang), masking the failure with true is incorrect.
If the timeout fires, tje initialization did not complete successfully and the app should know.
The point is to unblock the JS promise, bit the result should be accurate.
| resolveOnce(true) | |
| resolveOnce(false) |
There was a problem hiding this comment.
Good callout, but I think true is actually the more accurate result here. Here's why:
In IterableAPI.initialize2 (IterableAPI.swift:126-129), the SDK is initialized synchronously before start() is called:
implementation = InternalIterableAPI(apiKey: apiKey, ...) // ← this IS init
_ = implementation?.start().onSuccess { callback?(true) } // ← this is in-app syncThe callback is gated on implementation.start() → inAppManager.start() → fetcher.fetch() — that's the in-app message sync, not actual SDK initialization. The SDK is fully functional at that point.
Jena confirmed this from the debug session: "we did see the SDK respond to lifecycle events (background, foreground) so it was init despite the hang."
If this timeout fires, the most likely scenario is:
- The SDK did initialize successfully
- The callback just never propagated (the Fulfill bug we fixed in the Swift SDK, a network hang, etc.)
Resolving with false would tell the app "Iterable failed to initialize" — which could cause it to skip Iterable functionality entirely, even though the SDK is actually working. That's a worse outcome for the customer.

What
Adds a 10-second defensive timeout to
ReactIterableAPI.initialize()on iOS so the JS promise resolves even if the nativeIterableAPI.initialize2callback is never invoked.Reported by: CarGurus (SDK-392) —
Iterable.initialize()promise hangs indefinitely on React Native New Architecture.Root Cause
In New Architecture bridgeless mode,
RCTEventEmitterevents may not reach JS (bridge is nil). WhenauthHandleris configured with a saved user, the native SDK'sonAuthTokenRequestedsends an event to JS and blocks for 30s — but JS never receives it. The nil auth token causes a 401 JWT error on the in-app fetch, which enters a retry loop in the Swift SDK'sRequestProcessorUtil. When retries are exhausted, a bug inAuthManager.requestNewAuthToken()causes thePending/Fulfillto never resolve — so theinitialize2callback never fires, and the JS promise hangs forever.Companion fix: The underlying Swift SDK bug is addressed in Iterable/iterable-swift-sdk#1023. This timeout is a defensive workaround that:
maxRetry × retryInterval+ 30s auth timeoutChanges
ios/RNIterableAPI/ReactIterableAPI.swift: Wrap theresolverin aresolveOnceguard and schedule a 10-second timeout viaDispatchWorkItem. If the native callback fires normally, the timeout is cancelled. If not, the promise resolves withtrueafter 10s. The SDK still initializes and functions normally regardless — only the JS promise is unblocked.Regression Risk Analysis
Low risk
resolveOnceensures resolver is called exactly once with the real result. No behavior change.initializeresolves synchronously.Medium risk — review carefully
truebefore the actual result is known. The SDK is still initializing in the background. Callers that depend on the promise result to know if init truly succeeded would get a false positive. In practice, CarGurus confirmed the SDK does initialize and respond to lifecycle events even when the promise hangs — so resolving withtruematches actual behavior.Not affected
Testing
How to test:
IterableConfigwithauthHandlersetIterable.initialize()and verify the promise resolves within ~10s