@react-native-firebase/app 25.1.0 → 26.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/RNFBApp.podspec +7 -13
  3. package/android/build.gradle +25 -6
  4. package/android/src/reactnative/java/io/invertase/firebase/app/{ReactNativeFirebaseAppModule.java → NativeRNFBTurboApp.java} +81 -45
  5. package/android/src/reactnative/java/io/invertase/firebase/app/ReactNativeFirebaseAppPackage.java +3 -3
  6. package/android/src/reactnative/java/io/invertase/firebase/app/ReactNativeFirebaseVersion.java +2 -2
  7. package/android/src/reactnative/java/io/invertase/firebase/app/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAppSpec.java +136 -0
  8. package/android/src/reactnative/java/io/invertase/firebase/app/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboUtilsSpec.java +94 -0
  9. package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/CMakeLists.txt +36 -0
  10. package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/RNFBAppTurboModules-generated.cpp +170 -0
  11. package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/RNFBAppTurboModules.h +39 -0
  12. package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/react/renderer/components/RNFBAppTurboModules/RNFBAppTurboModulesJSI-generated.cpp +187 -0
  13. package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/react/renderer/components/RNFBAppTurboModules/RNFBAppTurboModulesJSI.h +606 -0
  14. package/android/src/reactnative/java/io/invertase/firebase/common/RCTConvertFirebase.java +3 -3
  15. package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebaseEventEmitter.java +475 -20
  16. package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebaseMeta.java +8 -0
  17. package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebasePreferences.java +8 -0
  18. package/android/src/reactnative/java/io/invertase/firebase/utils/{ReactNativeFirebaseUtilsModule.java → NativeRNFBTurboUtils.java} +57 -40
  19. package/android/src/test/java/io/invertase/firebase/app/NativeRNFBTurboAppTest.java +400 -0
  20. package/android/src/test/java/io/invertase/firebase/common/ReactNativeFirebaseEventEmitterTest.java +1823 -0
  21. package/app.plugin.js +1 -1
  22. package/dist/module/FirebaseApp.js +0 -9
  23. package/dist/module/FirebaseApp.js.map +1 -1
  24. package/dist/module/common/index.js +0 -645
  25. package/dist/module/common/index.js.map +1 -1
  26. package/dist/module/common/unitTestUtils.js +18 -54
  27. package/dist/module/common/unitTestUtils.js.map +1 -1
  28. package/dist/module/index.js +0 -1
  29. package/dist/module/index.js.map +1 -1
  30. package/dist/module/internal/RNFBNativeEventEmitter.js +12 -6
  31. package/dist/module/internal/RNFBNativeEventEmitter.js.map +1 -1
  32. package/dist/module/internal/constants.js +2 -2
  33. package/dist/module/internal/constants.js.map +1 -1
  34. package/dist/module/internal/index.js +1 -1
  35. package/dist/module/internal/index.js.map +1 -1
  36. package/dist/module/internal/nativeModuleAndroidIos.js +50 -10
  37. package/dist/module/internal/nativeModuleAndroidIos.js.map +1 -1
  38. package/dist/module/internal/nativeModuleWeb.js +12 -4
  39. package/dist/module/internal/nativeModuleWeb.js.map +1 -1
  40. package/dist/module/internal/registry/app.js +20 -12
  41. package/dist/module/internal/registry/app.js.map +1 -1
  42. package/dist/module/internal/registry/modular.js +73 -0
  43. package/dist/module/internal/registry/modular.js.map +1 -0
  44. package/dist/module/internal/registry/nativeModule.js +116 -95
  45. package/dist/module/internal/registry/nativeModule.js.map +1 -1
  46. package/dist/module/modular.js +44 -37
  47. package/dist/module/modular.js.map +1 -1
  48. package/dist/module/types/app.js +0 -3
  49. package/dist/module/types/app.js.map +1 -1
  50. package/dist/module/types/internal.js +0 -2
  51. package/dist/module/utils/UtilsStatics.js +2 -2
  52. package/dist/module/utils/UtilsStatics.js.map +1 -1
  53. package/dist/module/utils/index.js +21 -17
  54. package/dist/module/utils/index.js.map +1 -1
  55. package/dist/module/version.js +1 -1
  56. package/dist/typescript/lib/FirebaseApp.d.ts +1 -2
  57. package/dist/typescript/lib/FirebaseApp.d.ts.map +1 -1
  58. package/dist/typescript/lib/common/index.d.ts +0 -8
  59. package/dist/typescript/lib/common/index.d.ts.map +1 -1
  60. package/dist/typescript/lib/common/unitTestUtils.d.ts +1 -4
  61. package/dist/typescript/lib/common/unitTestUtils.d.ts.map +1 -1
  62. package/dist/typescript/lib/index.d.ts +0 -1
  63. package/dist/typescript/lib/index.d.ts.map +1 -1
  64. package/dist/typescript/lib/internal/NativeFirebaseError.d.ts +1 -1
  65. package/dist/typescript/lib/internal/NativeFirebaseError.d.ts.map +1 -1
  66. package/dist/typescript/lib/internal/RNFBNativeEventEmitter.d.ts.map +1 -1
  67. package/dist/typescript/lib/internal/constants.d.ts +2 -3
  68. package/dist/typescript/lib/internal/constants.d.ts.map +1 -1
  69. package/dist/typescript/lib/internal/index.d.ts +1 -1
  70. package/dist/typescript/lib/internal/index.d.ts.map +1 -1
  71. package/dist/typescript/lib/internal/nativeModuleAndroidIos.d.ts +2 -6
  72. package/dist/typescript/lib/internal/nativeModuleAndroidIos.d.ts.map +1 -1
  73. package/dist/typescript/lib/internal/nativeModuleWeb.d.ts.map +1 -1
  74. package/dist/typescript/lib/internal/registry/app.d.ts +2 -2
  75. package/dist/typescript/lib/internal/registry/app.d.ts.map +1 -1
  76. package/dist/typescript/lib/internal/registry/modular.d.ts +22 -0
  77. package/dist/typescript/lib/internal/registry/modular.d.ts.map +1 -0
  78. package/dist/typescript/lib/internal/registry/nativeModule.d.ts +3 -11
  79. package/dist/typescript/lib/internal/registry/nativeModule.d.ts.map +1 -1
  80. package/dist/typescript/lib/modular.d.ts +21 -7
  81. package/dist/typescript/lib/modular.d.ts.map +1 -1
  82. package/dist/typescript/lib/types/app.d.ts +4 -4
  83. package/dist/typescript/lib/types/app.d.ts.map +1 -1
  84. package/dist/typescript/lib/types/internal.d.ts +1 -33
  85. package/dist/typescript/lib/types/internal.d.ts.map +1 -1
  86. package/dist/typescript/lib/utils/index.d.ts +7 -3
  87. package/dist/typescript/lib/utils/index.d.ts.map +1 -1
  88. package/dist/typescript/lib/version.d.ts +1 -1
  89. package/firebase-schema.json +5 -1
  90. package/ios/RNFBApp/RNFBAppModule.h +1 -3
  91. package/ios/RNFBApp/{RNFBAppModule.m → RNFBAppModule.mm} +74 -77
  92. package/ios/RNFBApp/RNFBUtilsModule.h +1 -3
  93. package/ios/RNFBApp/{RNFBUtilsModule.m → RNFBUtilsModule.mm} +68 -25
  94. package/ios/RNFBApp/RNFBVersion.m +1 -1
  95. package/ios/RNFBApp.xcodeproj/project.pbxproj +12 -12
  96. package/ios/generated/RCTAppDependencyProvider.h +25 -0
  97. package/ios/generated/RCTAppDependencyProvider.mm +55 -0
  98. package/ios/generated/RCTModuleProviders.h +16 -0
  99. package/ios/generated/RCTModuleProviders.mm +52 -0
  100. package/ios/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
  101. package/ios/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
  102. package/ios/generated/RCTThirdPartyComponentsProvider.h +16 -0
  103. package/ios/generated/RCTThirdPartyComponentsProvider.mm +23 -0
  104. package/ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.h +14 -0
  105. package/ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.mm +19 -0
  106. package/ios/generated/RNFBAppTurboModules/RNFBAppTurboModules-generated.mm +210 -0
  107. package/ios/generated/RNFBAppTurboModules/RNFBAppTurboModules.h +399 -0
  108. package/ios/generated/RNFBAppTurboModulesJSI-generated.cpp +187 -0
  109. package/ios/generated/RNFBAppTurboModulesJSI.h +606 -0
  110. package/ios/generated/ReactAppDependencyProvider.podspec +34 -0
  111. package/ios/generated/ReactCodegen.podspec +111 -0
  112. package/lib/FirebaseApp.ts +1 -10
  113. package/lib/common/index.ts +0 -742
  114. package/lib/common/unitTestUtils.ts +17 -73
  115. package/lib/index.ts +0 -1
  116. package/lib/internal/NativeFirebaseError.ts +1 -1
  117. package/lib/internal/RNFBNativeEventEmitter.ts +12 -6
  118. package/lib/internal/constants.ts +3 -25
  119. package/lib/internal/global.d.ts +0 -4
  120. package/lib/internal/index.ts +1 -1
  121. package/lib/internal/nativeModuleAndroidIos.ts +69 -12
  122. package/lib/internal/nativeModuleWeb.ts +11 -5
  123. package/lib/internal/registry/app.ts +24 -22
  124. package/lib/internal/registry/modular.ts +97 -0
  125. package/lib/internal/registry/nativeModule.ts +136 -107
  126. package/lib/modular.ts +47 -72
  127. package/lib/types/app.ts +6 -11
  128. package/lib/types/internal.ts +1 -46
  129. package/lib/utils/UtilsStatics.ts +2 -2
  130. package/lib/utils/index.ts +24 -18
  131. package/lib/version.ts +1 -1
  132. package/package.json +41 -3
  133. package/plugin/build/app.plugin.d.ts +2 -0
  134. package/plugin/build/app.plugin.js +6 -0
  135. package/plugin/src/app.plugin.ts +3 -0
  136. package/plugin/tsconfig.tsbuildinfo +1 -1
  137. package/react-native.config.js +2 -0
  138. package/specs/NativeRNFBTurboApp.ts +57 -0
  139. package/specs/NativeRNFBTurboUtils.ts +34 -0
  140. package/typedoc.json +1 -2
  141. package/dist/module/internal/registry/namespace.js +0 -262
  142. package/dist/module/internal/registry/namespace.js.map +0 -1
  143. package/dist/module/namespaced.js +0 -25
  144. package/dist/module/namespaced.js.map +0 -1
  145. package/dist/typescript/lib/internal/registry/namespace.d.ts +0 -26
  146. package/dist/typescript/lib/internal/registry/namespace.d.ts.map +0 -1
  147. package/dist/typescript/lib/namespaced.d.ts +0 -5
  148. package/dist/typescript/lib/namespaced.d.ts.map +0 -1
  149. package/lib/internal/registry/namespace.ts +0 -338
  150. package/lib/namespaced.ts +0 -24
@@ -17,27 +17,64 @@ package io.invertase.firebase.common;
17
17
  *
18
18
  */
19
19
 
20
+ import android.content.Context;
20
21
  import android.os.Handler;
21
22
  import android.os.Looper;
22
23
  import android.util.Log;
23
24
  import androidx.annotation.MainThread;
25
+ import androidx.annotation.Nullable;
26
+ import com.facebook.react.ReactApplication;
27
+ import com.facebook.react.ReactHost;
28
+ import com.facebook.react.ReactNativeHost;
24
29
  import com.facebook.react.bridge.Arguments;
25
30
  import com.facebook.react.bridge.ReactContext;
26
31
  import com.facebook.react.bridge.WritableMap;
27
32
  import com.facebook.react.modules.core.DeviceEventManagerModule;
33
+ import io.invertase.firebase.app.ReactNativeFirebaseApp;
28
34
  import io.invertase.firebase.interfaces.NativeEvent;
35
+ import java.lang.ref.WeakReference;
29
36
  import java.util.ArrayList;
30
37
  import java.util.HashMap;
31
38
  import java.util.List;
32
39
 
33
40
  public class ReactNativeFirebaseEventEmitter {
41
+ private static final String TAG = "RNFB_EMITTER";
34
42
  private static ReactNativeFirebaseEventEmitter sharedInstance =
35
43
  new ReactNativeFirebaseEventEmitter();
36
- private final List<NativeEvent> queuedEvents = new ArrayList<>();
37
44
  private final Handler handler = new Handler(Looper.getMainLooper());
45
+
46
+ // All mutable state below is guarded by the jsListeners monitor. Attach/detach/ready
47
+ // transitions happen synchronously at cause time under the same monitor as listener
48
+ // registration: with more than one ReactContext generation alive during an instance
49
+ // reload, deferring any of these transitions (e.g. to a congested main looper) reorders
50
+ // them against a replacement runtime's synchronous registrations and can wipe or wedge
51
+ // that runtime's state. Only queue flushing is posted to the main thread.
52
+ private final List<NativeEvent> queuedEvents = new ArrayList<>();
38
53
  private final HashMap<String, Integer> jsListeners = new HashMap<>();
39
- private ReactContext reactContext;
40
- private Boolean jsReady = false;
54
+ // Weak so the emitter never pins a dead ReactContext (and its module graph) for the
55
+ // lifetime of the process if a runtime is torn down without a paired detach.
56
+ private WeakReference<ReactContext> attachedReactContext = new WeakReference<>(null);
57
+ // When the host still names a dying generation, a live module's attach is remembered here
58
+ // and promoted once that dying context detaches or the host catches up — without waiting
59
+ // solely on a later Activity resume that may never arrive.
60
+ private WeakReference<ReactContext> pendingReactContext = new WeakReference<>(null);
61
+ // Host identity we moved past while converging onto pending/attached; late attaches of this
62
+ // identity must not displace the live generation while the host ref lags.
63
+ private WeakReference<ReactContext> hostLagReactContext = new WeakReference<>(null);
64
+ // When a new pending candidate enters while the attached generation still lives, listener
65
+ // accounting is reset so events queue until the pending JS re-registers. Keep a snapshot of
66
+ // the attached generation's accounting so a pending-only cancel can restore it — otherwise
67
+ // the surviving attached runtime stays deaf for the rest of the session (#8374-class).
68
+ @Nullable private HashMap<String, Integer> attachedListenersSnapshot;
69
+ private boolean attachedJsReadySnapshot;
70
+ private int attachedJsListenerCountSnapshot;
71
+ // After pending-only cancel restores that snapshot, emit must keep targeting the restored
72
+ // attached generation even if the host has already advanced to an unrelated unattached
73
+ // context — otherwise resolveEmitContext prefers raw hostCurrent and silently loses events
74
+ // into a runtime with no RNFB JS subscribers (R1 class after cancel). Cleared when a new
75
+ // generation attaches through acceptAttachedContext / convergence / full detach.
76
+ private boolean emitPrefersRestoredAttached;
77
+ private boolean jsReady = false;
41
78
  private int jsListenerCount;
42
79
 
43
80
  public static ReactNativeFirebaseEventEmitter getSharedInstance() {
@@ -45,26 +82,187 @@ public class ReactNativeFirebaseEventEmitter {
45
82
  }
46
83
 
47
84
  public void attachReactContext(final ReactContext reactContext) {
48
- handler.post(
49
- () -> {
50
- ReactNativeFirebaseEventEmitter.this.reactContext = reactContext;
51
- sendQueuedEvents();
52
- });
85
+ // Resolve host current outside jsListeners so ReactApplication getters are not held
86
+ // under the listener monitor (they are RN-owned and not expected to re-enter, but the
87
+ // coupling is unnecessary for attach arbitration).
88
+ ReactContext hostCurrent = getCurrentReactContextFromHost(reactContext);
89
+
90
+ synchronized (jsListeners) {
91
+ // Under the new architecture more than one ReactContext generation can exist while an
92
+ // instance is destroyed and recreated: a module belonging to the dying generation may
93
+ // run initialize() after the replacement generation has already attached (for example
94
+ // TurboModuleManager creates requested-but-never-built modules during invalidate).
95
+ // Attach is fail-closed when something is already attached: accept the candidate only
96
+ // when the host currently names it. A null host current keeps the previous attachment
97
+ // rather than accepting a late stale context. When the host still names the previous
98
+ // (dying) context, remember the candidate as pending so detach/host catch-up can
99
+ // converge without depending only on a later Activity resume.
100
+ ReactContext previousContext = attachedReactContext.get();
101
+ if (previousContext != null && previousContext != reactContext) {
102
+ if (hostCurrent == null) {
103
+ Log.w(
104
+ TAG,
105
+ "Ignoring attach of ReactContext@"
106
+ + System.identityHashCode(reactContext)
107
+ + " while host current is null; keeping @"
108
+ + System.identityHashCode(previousContext));
109
+ return;
110
+ }
111
+
112
+ ReactContext hostLag = hostLagReactContext.get();
113
+ if (hostCurrent == reactContext) {
114
+ if (hostLag != null && hostLag == reactContext) {
115
+ Log.w(
116
+ TAG,
117
+ "Ignoring attach of host-lag ReactContext@"
118
+ + System.identityHashCode(reactContext)
119
+ + "; keeping live @"
120
+ + System.identityHashCode(previousContext));
121
+ return;
122
+ }
123
+ acceptAttachedContext(
124
+ reactContext, /* resetListenerState= */ pendingReactContext.get() != reactContext);
125
+ clearConvergenceState();
126
+ discardAttachedListenerSnapshot();
127
+ } else if (hostCurrent == previousContext) {
128
+ // Host still names the dying attached generation. Remember the candidate as pending
129
+ // so detach/host catch-up can converge. Idempotent re-attach of the same pending
130
+ // identity (e.g. async onHostResume while the host still lags) must not wipe
131
+ // listeners the pending JS already registered. Reset only when entering pending for
132
+ // a new candidate: first pending, or last-writer-wins replace of a different one.
133
+ ReactContext existingPending = pendingReactContext.get();
134
+ boolean newPendingCandidate = existingPending != reactContext;
135
+ Log.i(
136
+ TAG,
137
+ "Host still on @"
138
+ + System.identityHashCode(previousContext)
139
+ + "; pending attach of @"
140
+ + System.identityHashCode(reactContext)
141
+ + (newPendingCandidate ? "" : " (idempotent)"));
142
+ pendingReactContext = new WeakReference<>(reactContext);
143
+ hostLagReactContext = new WeakReference<>(previousContext);
144
+ if (newPendingCandidate) {
145
+ // Drop previous-generation listener accounting; replacement JS will re-register.
146
+ // Snapshot once per pending window so pending-only cancel can restore the
147
+ // surviving attached generation (last-writer-wins replace keeps the first
148
+ // snapshot — that is still the attached runtime's pre-pending state).
149
+ snapshotAttachedListenerStateIfNeeded();
150
+ resetListenerState();
151
+ }
152
+ handler.post(this::tryConvergePendingReactContext);
153
+ return;
154
+ } else {
155
+ Log.w(
156
+ TAG,
157
+ "Ignoring attach of non-current ReactContext@"
158
+ + System.identityHashCode(reactContext)
159
+ + ", current is @"
160
+ + System.identityHashCode(hostCurrent));
161
+ return;
162
+ }
163
+ } else if (previousContext != reactContext) {
164
+ acceptAttachedContext(reactContext, /* resetListenerState= */ false);
165
+ clearConvergenceState();
166
+ } else {
167
+ // Same context re-attach (e.g. resume). Clear lag only when this identity is the
168
+ // live pending/attached generation the host has caught up to — not when we are
169
+ // still the dying hostLag while a different pending replacement is waiting.
170
+ if (hostCurrent == reactContext) {
171
+ ReactContext pending = pendingReactContext.get();
172
+ if (pending == null || pending == reactContext) {
173
+ clearConvergenceState();
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ handler.post(this::sendQueuedEvents);
180
+ }
181
+
182
+ public void detachReactContext(final ReactContext reactContext) {
183
+ synchronized (jsListeners) {
184
+ // Pending-only invalidate (failed replacement startup / overlapping teardown): the
185
+ // candidate never became attached, so cancel it before the attached-identity check.
186
+ // Restore the surviving attached generation's listener / jsReady snapshot taken when
187
+ // pending first entered — pending-window registrations belong to the cancelled
188
+ // generation and must not leave the attached runtime deaf. Superseded identities that
189
+ // are not the current pending fall through and no-op below — they must not wipe a
190
+ // newer live pending's registrations.
191
+ ReactContext pending = pendingReactContext.get();
192
+ if (pending == reactContext && attachedReactContext.get() != reactContext) {
193
+ Log.i(
194
+ TAG,
195
+ "Cancelled pending ReactContext@"
196
+ + System.identityHashCode(reactContext)
197
+ + "; attached remains @"
198
+ + System.identityHashCode(attachedReactContext.get()));
199
+ clearConvergenceState();
200
+ restoreAttachedListenerSnapshot();
201
+ // Events queued while the pending window wiped listeners must drain now — attached
202
+ // JS already registered before pending entered and will not re-addListener.
203
+ handler.post(this::sendQueuedEvents);
204
+ return;
205
+ }
206
+
207
+ // Only clear state installed by the runtime that is going away; a dying generation
208
+ // must not wipe listener registrations a replacement runtime has already made. A
209
+ // replacement runtime always attaches (synchronously, at module creation) before its
210
+ // JS can register listeners, so an identity match here means the dying state is ours.
211
+ if (attachedReactContext.get() != reactContext) {
212
+ return;
213
+ }
214
+
215
+ if (pending != null && pending != reactContext) {
216
+ Log.i(
217
+ TAG,
218
+ "Detached ReactContext@"
219
+ + System.identityHashCode(reactContext)
220
+ + "; promoting pending @"
221
+ + System.identityHashCode(pending));
222
+ attachedReactContext = new WeakReference<>(pending);
223
+ pendingReactContext = new WeakReference<>(null);
224
+ // Keep jsReady / jsListeners: they belong to the promoted generation's JS.
225
+ // hostLag stays set so emit and late attaches do not regress to the dying identity
226
+ // while the host ref still names it.
227
+ discardAttachedListenerSnapshot();
228
+ emitPrefersRestoredAttached = false;
229
+ handler.post(this::sendQueuedEvents);
230
+ return;
231
+ }
232
+
233
+ Log.i(TAG, "Detached ReactContext@" + System.identityHashCode(reactContext));
234
+ attachedReactContext = new WeakReference<>(null);
235
+ resetListenerState();
236
+ clearConvergenceState();
237
+ discardAttachedListenerSnapshot();
238
+ emitPrefersRestoredAttached = false;
239
+ // queuedEvents are intentionally kept: they drain once the next runtime attaches and
240
+ // registers listeners, which is how events raised while no runtime is alive (for
241
+ // example messaging events for a killed app) reach JS.
242
+ }
53
243
  }
54
244
 
55
245
  public void notifyJsReady(Boolean ready) {
56
- handler.post(
57
- () -> {
58
- jsReady = ready;
59
- sendQueuedEvents();
60
- });
246
+ synchronized (jsListeners) {
247
+ jsReady = ready;
248
+ }
249
+
250
+ handler.post(this::sendQueuedEvents);
61
251
  }
62
252
 
63
253
  public void sendEvent(final NativeEvent event) {
64
254
  handler.post(
65
255
  () -> {
66
256
  synchronized (jsListeners) {
67
- if (!jsListeners.containsKey(event.getEventName()) || !emit(event)) {
257
+ if (!jsListeners.containsKey(event.getEventName())) {
258
+ queuedEvents.add(event);
259
+ return;
260
+ }
261
+ }
262
+ // emit() resolves ReactApplication host current outside jsListeners; do not hold
263
+ // the monitor across that call.
264
+ if (!emit(event)) {
265
+ synchronized (jsListeners) {
68
266
  queuedEvents.add(event);
69
267
  }
70
268
  }
@@ -105,15 +303,36 @@ public class ReactNativeFirebaseEventEmitter {
105
303
  WritableMap writableMap = Arguments.createMap();
106
304
  WritableMap events = Arguments.createMap();
107
305
 
108
- writableMap.putInt("listeners", jsListenerCount);
109
- writableMap.putInt("queued", queuedEvents.size());
306
+ // Resolve host current outside jsListeners; converge/snapshot under the monitor.
307
+ ReactContext pendingHint;
308
+ ReactContext attachedHint;
309
+ synchronized (jsListeners) {
310
+ pendingHint = pendingReactContext.get();
311
+ attachedHint = attachedReactContext.get();
312
+ }
313
+ ReactContext hostForConverge =
314
+ getCurrentReactContextFromHost(pendingHint != null ? pendingHint : attachedHint);
110
315
 
316
+ ReactContext attachedContext;
111
317
  synchronized (jsListeners) {
318
+ tryConvergePendingReactContextLocked(hostForConverge);
319
+ attachedContext = attachedReactContext.get();
320
+
321
+ writableMap.putInt("listeners", jsListenerCount);
322
+ writableMap.putInt("queued", queuedEvents.size());
323
+ writableMap.putBoolean("jsReady", jsReady);
324
+ writableMap.putInt(
325
+ "attachedContextHash",
326
+ attachedContext == null ? 0 : System.identityHashCode(attachedContext));
327
+
112
328
  for (HashMap.Entry<String, Integer> entry : jsListeners.entrySet()) {
113
329
  events.putInt(entry.getKey(), entry.getValue());
114
330
  }
115
331
  }
116
332
 
333
+ ReactContext currentContext = getCurrentReactContextFromHost(attachedContext);
334
+ writableMap.putInt(
335
+ "currentContextHash", currentContext == null ? 0 : System.identityHashCode(currentContext));
117
336
  writableMap.putMap("events", events);
118
337
 
119
338
  return writableMap;
@@ -121,31 +340,267 @@ public class ReactNativeFirebaseEventEmitter {
121
340
 
122
341
  @MainThread
123
342
  private void sendQueuedEvents() {
343
+ List<NativeEvent> toSend;
124
344
  synchronized (jsListeners) {
345
+ toSend = new ArrayList<>();
125
346
  for (NativeEvent event : new ArrayList<>(queuedEvents)) {
126
347
  if (jsListeners.containsKey(event.getEventName())) {
127
348
  queuedEvents.remove(event);
128
- sendEvent(event);
349
+ toSend.add(event);
129
350
  }
130
351
  }
131
352
  }
353
+ for (NativeEvent event : toSend) {
354
+ sendEvent(event);
355
+ }
132
356
  }
133
357
 
134
358
  @MainThread
135
359
  private boolean emit(final NativeEvent event) {
136
- if (!jsReady || reactContext == null || !reactContext.hasActiveCatalystInstance()) {
360
+ // Snapshot readiness / pointers under the monitor, resolve host current outside (avoid
361
+ // holding jsListeners across ReactApplication getters), then re-enter to converge/resolve.
362
+ ReactContext attachedHint;
363
+ boolean ready;
364
+ synchronized (jsListeners) {
365
+ ready = jsReady;
366
+ if (!ready) {
367
+ return false;
368
+ }
369
+ attachedHint = attachedReactContext.get();
370
+ if (attachedHint == null) {
371
+ attachedHint = pendingReactContext.get();
372
+ }
373
+ }
374
+ ReactContext hostCurrent = getCurrentReactContextFromHost(attachedHint);
375
+
376
+ ReactContext emitContext;
377
+ synchronized (jsListeners) {
378
+ if (!jsReady) {
379
+ return false;
380
+ }
381
+ tryConvergePendingReactContextLocked(hostCurrent);
382
+
383
+ // Resolve the context hosting the live JS runtime at emit time rather than trusting the
384
+ // attached one alone: a context captured at module-creation time can belong to a previous
385
+ // generation whose runtime no longer contains the JS listeners, and on bridgeless a stale
386
+ // context still reports an active instance so it cannot be validated - only replaced
387
+ // (#8374, #8900). When attached has already converged ahead of a lagging host ref, prefer
388
+ // attached over the dying host current so events are not delivered into a runtime with no
389
+ // listeners.
390
+ ReactContext attached = attachedReactContext.get();
391
+ emitContext = resolveEmitContext(attached, hostCurrent);
392
+ }
393
+
394
+ if (emitContext == null || !emitContext.hasActiveReactInstance()) {
137
395
  return false;
138
396
  }
139
397
 
140
398
  try {
141
- reactContext
399
+ emitContext
142
400
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
143
401
  .emit("rnfb_" + event.getEventName(), event.getEventBody());
144
402
  } catch (Exception e) {
145
- Log.wtf("RNFB_EMITTER", "Error sending Event " + event.getEventName(), e);
403
+ Log.wtf(TAG, "Error sending Event " + event.getEventName(), e);
146
404
  return false;
147
405
  }
148
406
 
149
407
  return true;
150
408
  }
409
+
410
+ @Nullable
411
+ private ReactContext resolveEmitContext(
412
+ @Nullable ReactContext attached, @Nullable ReactContext hostCurrent) {
413
+ ReactContext pending = pendingReactContext.get();
414
+ ReactContext hostLag = hostLagReactContext.get();
415
+
416
+ // A pending replacement owns (or will own) the listener map. Prefer it over host/attached
417
+ // for the whole pending window — including when the host has already advanced to an
418
+ // unrelated third generation that has not yet attached via this emitter. Emitting into
419
+ // that third context would return true with no RNFB JS subscribers and no re-queue.
420
+ if (pending != null) {
421
+ return pending;
422
+ }
423
+
424
+ if (hostCurrent == null) {
425
+ return attached;
426
+ }
427
+ if (attached == null || attached == hostCurrent) {
428
+ if (attached == hostCurrent) {
429
+ clearConvergenceState();
430
+ emitPrefersRestoredAttached = false;
431
+ }
432
+ return hostCurrent;
433
+ }
434
+
435
+ if (hostLag != null && hostLag == hostCurrent) {
436
+ return attached;
437
+ }
438
+
439
+ // Pending-only cancel restored listeners onto attached; an unrelated hostCurrent that has
440
+ // not yet attached through this emitter must not steal delivery (silent loss, no re-queue).
441
+ // Flag is only set while an attached generation still owns the restored map.
442
+ if (emitPrefersRestoredAttached) {
443
+ return attached;
444
+ }
445
+
446
+ // Attached is stale relative to a host that has moved on — prefer the host.
447
+ return hostCurrent;
448
+ }
449
+
450
+ private void tryConvergePendingReactContext() {
451
+ ReactContext pendingHint;
452
+ synchronized (jsListeners) {
453
+ pendingHint = pendingReactContext.get();
454
+ }
455
+ if (pendingHint == null) {
456
+ return;
457
+ }
458
+ ReactContext hostCurrent = getCurrentReactContextFromHost(pendingHint);
459
+ synchronized (jsListeners) {
460
+ if (tryConvergePendingReactContextLocked(hostCurrent)) {
461
+ handler.post(this::sendQueuedEvents);
462
+ }
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Prefer resolving {@code hostCurrent} outside {@code jsListeners} and passing it in so
468
+ * ReactApplication getters are not held under the listener monitor.
469
+ *
470
+ * @return true when attachment changed
471
+ */
472
+ private boolean tryConvergePendingReactContextLocked(@Nullable ReactContext hostCurrent) {
473
+ ReactContext pending = pendingReactContext.get();
474
+ if (pending == null) {
475
+ return false;
476
+ }
477
+ if (hostCurrent != pending) {
478
+ return false;
479
+ }
480
+ ReactContext previous = attachedReactContext.get();
481
+ if (previous == pending) {
482
+ clearConvergenceState();
483
+ return false;
484
+ }
485
+ Log.i(
486
+ TAG, "Host caught up; attaching pending ReactContext@" + System.identityHashCode(pending));
487
+ acceptAttachedContext(pending, /* resetListenerState= */ false);
488
+ clearConvergenceState();
489
+ discardAttachedListenerSnapshot();
490
+ return true;
491
+ }
492
+
493
+ private void acceptAttachedContext(ReactContext reactContext, boolean resetListenerState) {
494
+ ReactContext previousContext = attachedReactContext.get();
495
+ if (previousContext != reactContext) {
496
+ Log.i(
497
+ TAG,
498
+ "ReactContext changed: @"
499
+ + System.identityHashCode(previousContext)
500
+ + " -> @"
501
+ + System.identityHashCode(reactContext));
502
+ attachedReactContext = new WeakReference<>(reactContext);
503
+ // A real attach of a new generation supersedes restored-attached emit affinity.
504
+ emitPrefersRestoredAttached = false;
505
+ if (resetListenerState) {
506
+ discardAttachedListenerSnapshot();
507
+ resetListenerState();
508
+ }
509
+ }
510
+ }
511
+
512
+ private void resetListenerState() {
513
+ jsReady = false;
514
+ jsListeners.clear();
515
+ jsListenerCount = 0;
516
+ }
517
+
518
+ private void snapshotAttachedListenerStateIfNeeded() {
519
+ if (attachedListenersSnapshot != null) {
520
+ return;
521
+ }
522
+ attachedListenersSnapshot = new HashMap<>(jsListeners);
523
+ attachedJsReadySnapshot = jsReady;
524
+ attachedJsListenerCountSnapshot = jsListenerCount;
525
+ }
526
+
527
+ private void restoreAttachedListenerSnapshot() {
528
+ if (attachedListenersSnapshot == null) {
529
+ resetListenerState();
530
+ emitPrefersRestoredAttached = false;
531
+ return;
532
+ }
533
+ jsListeners.clear();
534
+ jsListeners.putAll(attachedListenersSnapshot);
535
+ jsReady = attachedJsReadySnapshot;
536
+ jsListenerCount = attachedJsListenerCountSnapshot;
537
+ discardAttachedListenerSnapshot();
538
+ emitPrefersRestoredAttached = true;
539
+ }
540
+
541
+ private void discardAttachedListenerSnapshot() {
542
+ attachedListenersSnapshot = null;
543
+ attachedJsReadySnapshot = false;
544
+ attachedJsListenerCountSnapshot = 0;
545
+ }
546
+
547
+ private void clearConvergenceState() {
548
+ pendingReactContext = new WeakReference<>(null);
549
+ hostLagReactContext = new WeakReference<>(null);
550
+ }
551
+
552
+ @Nullable
553
+ private static ReactContext getCurrentReactContextFromHost(@Nullable ReactContext contextHint) {
554
+ Context applicationContext =
555
+ contextHint != null
556
+ ? contextHint.getApplicationContext()
557
+ : ReactNativeFirebaseApp.getApplicationContext();
558
+ if (applicationContext != null) {
559
+ // Normalize in case a non-application context (for example a ContentProvider context)
560
+ // was stored as the fallback.
561
+ applicationContext = applicationContext.getApplicationContext();
562
+ }
563
+ if (!(applicationContext instanceof ReactApplication)) {
564
+ // Brownfield hosts that do not implement ReactApplication fall back to the attached
565
+ // context, preserving pre-existing behaviour.
566
+ return null;
567
+ }
568
+
569
+ ReactApplication reactApplication = (ReactApplication) applicationContext;
570
+
571
+ // Note: these getters may lazily construct their host on first call; that is acceptable
572
+ // here because a live react-native-firebase module implies the app's host already exists.
573
+ ReactContext fromReactHost = null;
574
+ try {
575
+ ReactHost reactHost = reactApplication.getReactHost();
576
+ if (reactHost != null) {
577
+ fromReactHost = reactHost.getCurrentReactContext();
578
+ if (fromReactHost != null) {
579
+ return fromReactHost;
580
+ }
581
+ // Hybrid / transitional hosts may expose a non-null ReactHost whose current context
582
+ // is briefly null while the bridge ReactNativeHost still carries the live context.
583
+ // Fall through rather than treating null as a permanent answer.
584
+ }
585
+ } catch (RuntimeException | LinkageError e) {
586
+ // App-authored hosts may throw, and react-native 0.83+ throws from the deprecated
587
+ // reactNativeHost getter on bridgeless-only apps.
588
+ Log.d(TAG, "Failed to resolve current ReactContext via ReactHost", e);
589
+ }
590
+
591
+ try {
592
+ ReactNativeHost reactNativeHost = reactApplication.getReactNativeHost();
593
+ // hasInstance() guards against getReactInstanceManager() eagerly creating an instance
594
+ // manager purely as a side effect of emitting an event.
595
+ if (reactNativeHost != null && reactNativeHost.hasInstance()) {
596
+ return reactNativeHost.getReactInstanceManager().getCurrentReactContext();
597
+ }
598
+ } catch (RuntimeException | LinkageError e) {
599
+ // Bridgeless-only apps throw from getReactNativeHost(); if ReactHost was present but
600
+ // returned null current, that null stands — still log so the failure is visible.
601
+ Log.d(TAG, "Failed to resolve current ReactContext via ReactNativeHost", e);
602
+ }
603
+
604
+ return fromReactHost;
605
+ }
151
606
  }
@@ -70,6 +70,12 @@ public class ReactNativeFirebaseMeta {
70
70
  return metaData.getString(META_PREFIX + key, defaultValue);
71
71
  }
72
72
 
73
+ public int getIntValue(String key, int defaultValue) {
74
+ Bundle metaData = getMetaData();
75
+ if (metaData == null) return defaultValue;
76
+ return metaData.getInt(META_PREFIX + key, defaultValue);
77
+ }
78
+
73
79
  public WritableMap getAll() {
74
80
  Bundle metaData = getMetaData();
75
81
  WritableMap map = Arguments.createMap();
@@ -84,6 +90,8 @@ public class ReactNativeFirebaseMeta {
84
90
  map.putString(key, (String) value);
85
91
  } else if (value instanceof Boolean) {
86
92
  map.putBoolean(key, (Boolean) value);
93
+ } else if (value instanceof Integer) {
94
+ map.putInt(key, (Integer) value);
87
95
  }
88
96
  }
89
97
  }
@@ -46,6 +46,14 @@ public class ReactNativeFirebasePreferences {
46
46
  return getPreferences().getBoolean(key, defaultValue);
47
47
  }
48
48
 
49
+ public void setIntValue(String key, int value) {
50
+ getPreferences().edit().putInt(key, value).apply();
51
+ }
52
+
53
+ public int getIntValue(String key, int defaultValue) {
54
+ return getPreferences().getInt(key, defaultValue);
55
+ }
56
+
49
57
  public void setLongValue(String key, long value) {
50
58
  getPreferences().edit().putLong(key, value).apply();
51
59
  }