@react-native-firebase/app 25.1.0 → 26.1.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.
- package/CHANGELOG.md +54 -0
- package/README.md +21 -0
- package/RNFBApp.podspec +20 -15
- package/android/build.gradle +25 -6
- package/android/src/reactnative/java/io/invertase/firebase/app/{ReactNativeFirebaseAppModule.java → NativeRNFBTurboApp.java} +81 -45
- package/android/src/reactnative/java/io/invertase/firebase/app/ReactNativeFirebaseAppPackage.java +3 -3
- package/android/src/reactnative/java/io/invertase/firebase/app/ReactNativeFirebaseVersion.java +2 -2
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAppSpec.java +136 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboUtilsSpec.java +94 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/CMakeLists.txt +36 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/RNFBAppTurboModules-generated.cpp +170 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/RNFBAppTurboModules.h +39 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/react/renderer/components/RNFBAppTurboModules/RNFBAppTurboModulesJSI-generated.cpp +187 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/react/renderer/components/RNFBAppTurboModules/RNFBAppTurboModulesJSI.h +606 -0
- package/android/src/reactnative/java/io/invertase/firebase/common/RCTConvertFirebase.java +3 -3
- package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebaseEventEmitter.java +475 -20
- package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebaseMeta.java +8 -0
- package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebasePreferences.java +8 -0
- package/android/src/reactnative/java/io/invertase/firebase/utils/{ReactNativeFirebaseUtilsModule.java → NativeRNFBTurboUtils.java} +57 -40
- package/android/src/test/java/io/invertase/firebase/app/NativeRNFBTurboAppTest.java +400 -0
- package/android/src/test/java/io/invertase/firebase/common/ReactNativeFirebaseEventEmitterTest.java +1823 -0
- package/app.plugin.js +1 -1
- package/dist/module/FirebaseApp.js +0 -9
- package/dist/module/FirebaseApp.js.map +1 -1
- package/dist/module/common/index.js +0 -645
- package/dist/module/common/index.js.map +1 -1
- package/dist/module/common/unitTestUtils.js +18 -54
- package/dist/module/common/unitTestUtils.js.map +1 -1
- package/dist/module/index.js +0 -1
- package/dist/module/index.js.map +1 -1
- package/dist/module/internal/RNFBNativeEventEmitter.js +12 -6
- package/dist/module/internal/RNFBNativeEventEmitter.js.map +1 -1
- package/dist/module/internal/constants.js +2 -2
- package/dist/module/internal/constants.js.map +1 -1
- package/dist/module/internal/index.js +1 -1
- package/dist/module/internal/index.js.map +1 -1
- package/dist/module/internal/nativeModuleAndroidIos.js +50 -10
- package/dist/module/internal/nativeModuleAndroidIos.js.map +1 -1
- package/dist/module/internal/nativeModuleWeb.js +12 -4
- package/dist/module/internal/nativeModuleWeb.js.map +1 -1
- package/dist/module/internal/registry/app.js +20 -12
- package/dist/module/internal/registry/app.js.map +1 -1
- package/dist/module/internal/registry/modular.js +73 -0
- package/dist/module/internal/registry/modular.js.map +1 -0
- package/dist/module/internal/registry/nativeModule.js +116 -95
- package/dist/module/internal/registry/nativeModule.js.map +1 -1
- package/dist/module/internal/web/RNFBAppModule.js +16 -3
- package/dist/module/internal/web/RNFBAppModule.js.map +1 -1
- package/dist/module/internal/web/utils.js +6 -1
- package/dist/module/internal/web/utils.js.map +1 -1
- package/dist/module/modular.js +44 -37
- package/dist/module/modular.js.map +1 -1
- package/dist/module/types/app.js +0 -3
- package/dist/module/types/app.js.map +1 -1
- package/dist/module/types/internal.js +0 -2
- package/dist/module/utils/UtilsStatics.js +2 -2
- package/dist/module/utils/UtilsStatics.js.map +1 -1
- package/dist/module/utils/index.js +21 -17
- package/dist/module/utils/index.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/typescript/lib/FirebaseApp.d.ts +1 -2
- package/dist/typescript/lib/FirebaseApp.d.ts.map +1 -1
- package/dist/typescript/lib/common/index.d.ts +0 -8
- package/dist/typescript/lib/common/index.d.ts.map +1 -1
- package/dist/typescript/lib/common/unitTestUtils.d.ts +1 -4
- package/dist/typescript/lib/common/unitTestUtils.d.ts.map +1 -1
- package/dist/typescript/lib/index.d.ts +0 -1
- package/dist/typescript/lib/index.d.ts.map +1 -1
- package/dist/typescript/lib/internal/NativeFirebaseError.d.ts +1 -1
- package/dist/typescript/lib/internal/NativeFirebaseError.d.ts.map +1 -1
- package/dist/typescript/lib/internal/RNFBNativeEventEmitter.d.ts.map +1 -1
- package/dist/typescript/lib/internal/constants.d.ts +2 -3
- package/dist/typescript/lib/internal/constants.d.ts.map +1 -1
- package/dist/typescript/lib/internal/index.d.ts +1 -1
- package/dist/typescript/lib/internal/index.d.ts.map +1 -1
- package/dist/typescript/lib/internal/nativeModuleAndroidIos.d.ts +2 -6
- package/dist/typescript/lib/internal/nativeModuleAndroidIos.d.ts.map +1 -1
- package/dist/typescript/lib/internal/nativeModuleWeb.d.ts.map +1 -1
- package/dist/typescript/lib/internal/registry/app.d.ts +2 -2
- package/dist/typescript/lib/internal/registry/app.d.ts.map +1 -1
- package/dist/typescript/lib/internal/registry/modular.d.ts +22 -0
- package/dist/typescript/lib/internal/registry/modular.d.ts.map +1 -0
- package/dist/typescript/lib/internal/registry/nativeModule.d.ts +3 -11
- package/dist/typescript/lib/internal/registry/nativeModule.d.ts.map +1 -1
- package/dist/typescript/lib/internal/web/RNFBAppModule.d.ts.map +1 -1
- package/dist/typescript/lib/internal/web/utils.d.ts.map +1 -1
- package/dist/typescript/lib/modular.d.ts +21 -7
- package/dist/typescript/lib/modular.d.ts.map +1 -1
- package/dist/typescript/lib/types/app.d.ts +4 -4
- package/dist/typescript/lib/types/app.d.ts.map +1 -1
- package/dist/typescript/lib/types/internal.d.ts +1 -33
- package/dist/typescript/lib/types/internal.d.ts.map +1 -1
- package/dist/typescript/lib/utils/index.d.ts +7 -3
- package/dist/typescript/lib/utils/index.d.ts.map +1 -1
- package/dist/typescript/lib/version.d.ts +1 -1
- package/firebase-schema.json +5 -1
- package/firebase_spm.rb +1016 -0
- package/ios/RNFBApp/RCTConvert+FIRApp.h +4 -0
- package/ios/RNFBApp/RCTConvert+FIROptions.h +4 -0
- package/ios/RNFBApp/RNFBAppModule.h +1 -3
- package/ios/RNFBApp/{RNFBAppModule.m → RNFBAppModule.mm} +80 -77
- package/ios/RNFBApp/RNFBSharedUtils.h +4 -0
- package/ios/RNFBApp/RNFBUtilsModule.h +1 -3
- package/ios/RNFBApp/{RNFBUtilsModule.m → RNFBUtilsModule.mm} +68 -25
- package/ios/RNFBApp/RNFBVersion.m +1 -1
- package/ios/RNFBApp.xcodeproj/project.pbxproj +12 -12
- package/ios/generated/RCTAppDependencyProvider.h +25 -0
- package/ios/generated/RCTAppDependencyProvider.mm +55 -0
- package/ios/generated/RCTModuleProviders.h +16 -0
- package/ios/generated/RCTModuleProviders.mm +52 -0
- package/ios/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
- package/ios/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
- package/ios/generated/RCTThirdPartyComponentsProvider.h +16 -0
- package/ios/generated/RCTThirdPartyComponentsProvider.mm +23 -0
- package/ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.h +14 -0
- package/ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.mm +19 -0
- package/ios/generated/RNFBAppTurboModules/RNFBAppTurboModules-generated.mm +210 -0
- package/ios/generated/RNFBAppTurboModules/RNFBAppTurboModules.h +399 -0
- package/ios/generated/RNFBAppTurboModulesJSI-generated.cpp +187 -0
- package/ios/generated/RNFBAppTurboModulesJSI.h +606 -0
- package/ios/generated/ReactAppDependencyProvider.podspec +34 -0
- package/ios/generated/ReactCodegen.podspec +111 -0
- package/lib/FirebaseApp.ts +1 -10
- package/lib/common/index.ts +0 -742
- package/lib/common/unitTestUtils.ts +17 -73
- package/lib/index.ts +0 -1
- package/lib/internal/NativeFirebaseError.ts +1 -1
- package/lib/internal/RNFBNativeEventEmitter.ts +12 -6
- package/lib/internal/constants.ts +3 -25
- package/lib/internal/global.d.ts +0 -4
- package/lib/internal/index.ts +1 -1
- package/lib/internal/nativeModuleAndroidIos.ts +69 -12
- package/lib/internal/nativeModuleWeb.ts +11 -5
- package/lib/internal/registry/app.ts +24 -22
- package/lib/internal/registry/modular.ts +97 -0
- package/lib/internal/registry/nativeModule.ts +136 -107
- package/lib/internal/web/RNFBAppModule.ts +16 -3
- package/lib/internal/web/utils.ts +6 -1
- package/lib/modular.ts +47 -72
- package/lib/types/app.ts +6 -11
- package/lib/types/internal.ts +1 -46
- package/lib/utils/UtilsStatics.ts +2 -2
- package/lib/utils/index.ts +24 -18
- package/lib/version.ts +1 -1
- package/package.json +45 -6
- package/plugin/build/app.plugin.d.ts +2 -0
- package/plugin/build/app.plugin.js +6 -0
- package/plugin/src/app.plugin.ts +3 -0
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/react-native.config.js +2 -0
- package/specs/NativeRNFBTurboApp.ts +57 -0
- package/specs/NativeRNFBTurboUtils.ts +34 -0
- package/typedoc.json +1 -2
- package/dist/module/internal/registry/namespace.js +0 -262
- package/dist/module/internal/registry/namespace.js.map +0 -1
- package/dist/module/namespaced.js +0 -25
- package/dist/module/namespaced.js.map +0 -1
- package/dist/typescript/lib/internal/registry/namespace.d.ts +0 -26
- package/dist/typescript/lib/internal/registry/namespace.d.ts.map +0 -1
- package/dist/typescript/lib/namespaced.d.ts +0 -5
- package/dist/typescript/lib/namespaced.d.ts.map +0 -1
- package/lib/internal/registry/namespace.ts +0 -338
- 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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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())
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
+
emitContext
|
|
142
400
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
143
401
|
.emit("rnfb_" + event.getEventName(), event.getEventBody());
|
|
144
402
|
} catch (Exception e) {
|
|
145
|
-
Log.wtf(
|
|
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
|
}
|
package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebaseMeta.java
CHANGED
|
@@ -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
|
}
|