@callstack/react-native-brownfield 6.1.0 → 6.2.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @callstack/react-native-brownfield
2
2
 
3
+ ## 6.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`6517ec5`](https://github.com/callstack/react-native-brownfield/commit/6517ec5666269007f10c28efb7a748beba476998)]:
8
+ - @callstack/brownfield-cli@6.2.0
9
+
10
+ ## 6.1.1
11
+
12
+ ### Patch Changes
13
+
14
+ - [#516](https://github.com/callstack/react-native-brownfield/pull/516) [`66ac701`](https://github.com/callstack/react-native-brownfield/commit/66ac701d71cb3990732e701a0ab1abd40a7bd271) Thanks [@malopezr7](https://github.com/malopezr7)! - Android: `ReactNativeBrownfield.initialize` can now be called again after it throws. Before, a failure while loading native libraries or creating the `ReactHost` left it marked as initialized, so the next call returned without error and `ReactNativeBrownfield.shared` was never set.
15
+
16
+ - [#517](https://github.com/callstack/react-native-brownfield/pull/517) [`5884ce9`](https://github.com/callstack/react-native-brownfield/commit/5884ce96bcccf73de33835e7db2785aa3ab8c2a9) Thanks [@tlq5l](https://github.com/tlq5l)! - Fix an iOS crash (`std::bad_function_call`) when native code posts a message to JS while the Brownfield module's JS emitter is still being set up. If the Brownfield module exists but its JS emitter is not ready when delivery happens, the message is dropped and a log line is written. If the module does not exist yet, the message is dropped without a log line.
17
+ - Updated dependencies [[`a9726c1`](https://github.com/callstack/react-native-brownfield/commit/a9726c137c61fe2e59c039555a955632a5d86aec)]:
18
+ - @callstack/brownfield-cli@6.1.1
19
+
3
20
  ## 6.1.0
4
21
 
5
22
  ### Minor Changes
@@ -18,7 +18,6 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
18
18
  import com.facebook.react.soloader.OpenSourceMergedSoMapping
19
19
  import com.facebook.soloader.SoLoader
20
20
  import java.util.concurrent.CopyOnWriteArrayList
21
- import java.util.concurrent.atomic.AtomicBoolean
22
21
 
23
22
  fun interface OnJSBundleLoaded {
24
23
  operator fun invoke(timings: JSBundleTimings)
@@ -33,17 +32,15 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
33
32
 
34
33
  companion object {
35
34
  private lateinit var instance: ReactNativeBrownfield
36
- private val initialized = AtomicBoolean()
37
- private val nativeLibsLoaded = AtomicBoolean()
35
+ private val initialization = RetryableOnce()
36
+ private val nativeLibsLoading = RetryableOnce()
38
37
  private lateinit var applicationContext: Application
39
38
 
40
39
  @JvmStatic
41
40
  val shared: ReactNativeBrownfield get() = instance
42
41
 
43
42
  private fun loadNativeLibs(application: Application) {
44
- if (!nativeLibsLoaded.getAndSet(true)) {
45
- loadNativeLibsInternal(application)
46
- }
43
+ nativeLibsLoading.run { loadNativeLibsInternal(application) }
47
44
  }
48
45
 
49
46
  private fun loadNativeLibsInternal(application: Application) {
@@ -65,11 +62,12 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
65
62
  onJSBundleLoaded: OnJSBundleLoaded? = null
66
63
  ) {
67
64
  applicationContext = application
68
- if (!initialized.getAndSet(true)) {
65
+ val ranNow = initialization.run {
69
66
  AndroidJSBundleTimingObserver.install()
70
67
  loadNativeLibs(application)
71
68
  installAndPreload(reactHost, onJSBundleLoaded)
72
- } else {
69
+ }
70
+ if (!ranNow) {
73
71
  invokeWhenJSBundleLoaded(onJSBundleLoaded)
74
72
  }
75
73
  }
@@ -81,11 +79,12 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
81
79
  reactHostFactory: () -> ReactHost
82
80
  ) {
83
81
  applicationContext = application
84
- if (!initialized.getAndSet(true)) {
82
+ val ranNow = initialization.run {
85
83
  AndroidJSBundleTimingObserver.install()
86
84
  loadNativeLibs(application)
87
85
  installAndPreload(reactHostFactory(), onJSBundleLoaded)
88
- } else {
86
+ }
87
+ if (!ranNow) {
89
88
  invokeWhenJSBundleLoaded(onJSBundleLoaded)
90
89
  }
91
90
  }
@@ -98,7 +97,7 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
98
97
  onJSBundleLoaded: OnJSBundleLoaded? = null
99
98
  ) {
100
99
  applicationContext = application
101
- if (!initialized.getAndSet(true)) {
100
+ val ranNow = initialization.run {
102
101
  AndroidJSBundleTimingObserver.install()
103
102
  loadNativeLibs(application)
104
103
  val reactHost = getDefaultReactHost(
@@ -114,7 +113,8 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
114
113
  jsRuntimeFactory = null
115
114
  )
116
115
  installAndPreload(reactHost, onJSBundleLoaded)
117
- } else {
116
+ }
117
+ if (!ranNow) {
118
118
  invokeWhenJSBundleLoaded(onJSBundleLoaded)
119
119
  }
120
120
  }
@@ -0,0 +1,27 @@
1
+ package com.callstack.reactnativebrownfield
2
+
3
+ import java.util.concurrent.atomic.AtomicBoolean
4
+
5
+ /**
6
+ * Runs a block at most once, counting only a run that completes.
7
+ *
8
+ * If the block throws, the next [run] call runs it again instead of treating
9
+ * the work as done. Calls made while a run is in progress are skipped.
10
+ */
11
+ internal class RetryableOnce {
12
+ private val claimed = AtomicBoolean()
13
+
14
+ /** Returns `true` when [block] ran and completed during this call. */
15
+ fun run(block: () -> Unit): Boolean {
16
+ if (claimed.getAndSet(true)) {
17
+ return false
18
+ }
19
+ try {
20
+ block()
21
+ } catch (error: Throwable) {
22
+ claimed.set(false)
23
+ throw error
24
+ }
25
+ return true
26
+ }
27
+ }
@@ -0,0 +1,37 @@
1
+ package com.callstack.reactnativebrownfield
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertTrue
6
+ import org.junit.Assert.fail
7
+ import org.junit.Test
8
+
9
+ class RetryableOnceTest {
10
+ @Test fun `runs the block once after it completes`() {
11
+ val once = RetryableOnce()
12
+ var runs = 0
13
+
14
+ assertTrue(once.run { runs++ })
15
+ assertFalse(once.run { runs++ })
16
+ assertEquals(1, runs)
17
+ }
18
+
19
+ @Test fun `runs the block again after it throws`() {
20
+ val once = RetryableOnce()
21
+ var runs = 0
22
+
23
+ try {
24
+ once.run {
25
+ runs++
26
+ throw UnsatisfiedLinkError("dlopen failed")
27
+ }
28
+ fail("expected the error to propagate")
29
+ } catch (expected: UnsatisfiedLinkError) {
30
+ assertEquals("dlopen failed", expected.message)
31
+ }
32
+
33
+ assertTrue(once.run { runs++ })
34
+ assertFalse(once.run { runs++ })
35
+ assertEquals(2, runs)
36
+ }
37
+ }
@@ -307,7 +307,9 @@ internal import Expo
307
307
 
308
308
  /**
309
309
  * Send a serialized JSON message to the React Native JS application.
310
- * The message is delivered as a `brownfieldMessage` DeviceEventEmitter event.
310
+ * Messages can be received using `ReactNativeBrownfield.onMessage()`.
311
+ * If the Brownfield module exists but its JS emitter is not ready when delivery happens, the message is dropped and a log line is written.
312
+ * If the module does not exist yet, the message is dropped without a log line.
311
313
  *
312
314
  * @param message - The serialized JSON message to send to the React Native JS application.
313
315
  * @example
@@ -15,7 +15,9 @@ static ReactNativeBrownfieldModule *_sharedInstance = nil;
15
15
  - (instancetype)init {
16
16
  self = [super init];
17
17
  if (self) {
18
- _sharedInstance = self;
18
+ @synchronized (ReactNativeBrownfieldModule.class) {
19
+ _sharedInstance = self;
20
+ }
19
21
 
20
22
  [[NSNotificationCenter defaultCenter] addObserver:self
21
23
  selector:@selector(handleNativeToJSMessage:)
@@ -28,8 +30,18 @@ static ReactNativeBrownfieldModule *_sharedInstance = nil;
28
30
  - (void)dealloc {
29
31
  [[NSNotificationCenter defaultCenter] removeObserver:self];
30
32
 
31
- if (_sharedInstance == self) {
32
- _sharedInstance = nil;
33
+ @synchronized (ReactNativeBrownfieldModule.class) {
34
+ if (_sharedInstance == self) {
35
+ _sharedInstance = nil;
36
+ }
37
+ }
38
+ }
39
+
40
+ // React Native calls invalidate during teardown; clearing the callback sends
41
+ // later messages down the not-ready drop path.
42
+ - (void)invalidate {
43
+ @synchronized (ReactNativeBrownfieldModule.class) {
44
+ _eventEmitterCallback = nullptr;
33
45
  }
34
46
  }
35
47
 
@@ -41,10 +53,22 @@ static ReactNativeBrownfieldModule *_sharedInstance = nil;
41
53
  }
42
54
 
43
55
  + (void)emitMessageFromNative:(NSString *)message {
44
- if (_sharedInstance) {
56
+ // Keep singleton and callback access under one lock because getTurboModule:
57
+ // installs the callback when JS first requires the module.
58
+ @synchronized (ReactNativeBrownfieldModule.class) {
59
+ if (!_sharedInstance) {
60
+ NSLog(@"ReactNativeBrownfieldModule is not initialized, dropping message");
61
+ return;
62
+ }
63
+
64
+ // The codegen base class exposes this callback as a protected std::function;
65
+ // invoking it while empty would throw std::bad_function_call.
66
+ if (!_sharedInstance->_eventEmitterCallback) {
67
+ NSLog(@"ReactNativeBrownfieldModule is not ready for JS yet, dropping message");
68
+ return;
69
+ }
70
+
45
71
  [_sharedInstance emitOnBrownfieldMessage:@{ @"text": message }];
46
- } else {
47
- NSLog(@"ReactNativeBrownfieldModule is not initialized, dropping message");
48
72
  }
49
73
  }
50
74
 
@@ -65,7 +89,11 @@ RCT_EXPORT_METHOD(addListener:(NSString *)eventName) {}
65
89
  RCT_EXPORT_METHOD(removeListeners:(double)count) {}
66
90
 
67
91
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
68
- return std::make_shared<facebook::react::NativeReactNativeBrownfieldModuleSpecJSI>(params);
92
+ // Building the spec installs _eventEmitterCallback; use the same lock as
93
+ // emitMessageFromNative: to prevent the callback read and write from racing.
94
+ @synchronized (ReactNativeBrownfieldModule.class) {
95
+ return std::make_shared<facebook::react::NativeReactNativeBrownfieldModuleSpecJSI>(params);
96
+ }
69
97
  }
70
98
 
71
99
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/react-native-brownfield",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "license": "MIT",
5
5
  "author": "Michal Chudziak <mike.chudziak@callstack.com>",
6
6
  "bin": {
@@ -100,7 +100,7 @@
100
100
  "@expo/config-plugins": ">=54 <100"
101
101
  },
102
102
  "dependencies": {
103
- "@callstack/brownfield-cli": "^6.1.0",
103
+ "@callstack/brownfield-cli": "^6.2.0",
104
104
  "@react-native-community/cli-config": "^20.0.0",
105
105
  "@react-native-community/cli-types": "^20.0.0",
106
106
  "xcode": "^3.0.1"