@callstack/brownie 3.3.0 → 3.5.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,27 @@
1
1
  # @callstack/brownie
2
2
 
3
+ ## 3.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#257](https://github.com/callstack/react-native-brownfield/pull/257) [`d0e6203`](https://github.com/callstack/react-native-brownfield/commit/d0e62039c8a080c648abbbeace047e72fadce28b) Thanks [@hurali97](https://github.com/hurali97)! - add brownie android
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`d0e6203`](https://github.com/callstack/react-native-brownfield/commit/d0e62039c8a080c648abbbeace047e72fadce28b)]:
12
+ - @callstack/brownfield-cli@3.5.0
13
+
14
+ ## 3.4.0
15
+
16
+ ### Patch Changes
17
+
18
+ - [#246](https://github.com/callstack/react-native-brownfield/pull/246) [`5484065`](https://github.com/callstack/react-native-brownfield/commit/5484065da9dc86a420af2be692fcdefa32fbb2af) Thanks [@artus9033](https://github.com/artus9033)! - chore: upgrade dependencies
19
+
20
+ - [#246](https://github.com/callstack/react-native-brownfield/pull/246) [`5484065`](https://github.com/callstack/react-native-brownfield/commit/5484065da9dc86a420af2be692fcdefa32fbb2af) Thanks [@artus9033](https://github.com/artus9033)! - chore: upgrade dependencies
21
+
22
+ - Updated dependencies [[`5484065`](https://github.com/callstack/react-native-brownfield/commit/5484065da9dc86a420af2be692fcdefa32fbb2af), [`dd8b8a0`](https://github.com/callstack/react-native-brownfield/commit/dd8b8a0b532fe779c1f2ce018577ad748b887ee0), [`54ab7ab`](https://github.com/callstack/react-native-brownfield/commit/54ab7ab01bd6f95439cc8b702d4124552e22ad55), [`5484065`](https://github.com/callstack/react-native-brownfield/commit/5484065da9dc86a420af2be692fcdefa32fbb2af)]:
23
+ - @callstack/brownfield-cli@3.4.0
24
+
3
25
  ## 3.3.0
4
26
 
5
27
  ### Patch Changes
@@ -0,0 +1,91 @@
1
+ buildscript {
2
+ ext.brownie = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36,
7
+ cmakeVersion: "3.22.1"
8
+ ]
9
+
10
+ ext.getExtOrDefault = { prop ->
11
+ if (rootProject.ext.has(prop)) {
12
+ return rootProject.ext.get(prop)
13
+ }
14
+
15
+ return brownie[prop]
16
+ }
17
+
18
+ repositories {
19
+ google()
20
+ mavenCentral()
21
+ }
22
+
23
+ dependencies {
24
+ classpath "com.android.tools.build:gradle:8.7.2"
25
+ // noinspection DifferentKotlinGradleVersion
26
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
27
+ }
28
+ }
29
+
30
+
31
+ apply plugin: "com.android.library"
32
+ apply plugin: "kotlin-android"
33
+
34
+ apply plugin: "com.facebook.react"
35
+
36
+ android {
37
+ namespace "com.callstack.brownie"
38
+
39
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
40
+
41
+ defaultConfig {
42
+ minSdkVersion getExtOrDefault("minSdkVersion")
43
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
44
+ consumerProguardFiles "consumer-rules.pro"
45
+
46
+ externalNativeBuild {
47
+ cmake {
48
+ arguments "-DANDROID_STL=c++_shared"
49
+ cppFlags "-std=c++20 -fexceptions -frtti"
50
+ }
51
+ }
52
+ }
53
+
54
+ buildFeatures {
55
+ buildConfig true
56
+ prefab true
57
+ }
58
+
59
+ packaging {
60
+ jniLibs {
61
+ excludes += ["**/libc++_shared.so"]
62
+ }
63
+ }
64
+
65
+ buildTypes {
66
+ release {
67
+ minifyEnabled false
68
+ }
69
+ }
70
+
71
+ lint {
72
+ disable "GradleCompatible"
73
+ }
74
+
75
+ compileOptions {
76
+ sourceCompatibility JavaVersion.VERSION_1_8
77
+ targetCompatibility JavaVersion.VERSION_1_8
78
+ }
79
+
80
+ externalNativeBuild {
81
+ cmake {
82
+ path "src/main/cpp/CMakeLists.txt"
83
+ version getExtOrDefault("cmakeVersion")
84
+ }
85
+ }
86
+ }
87
+
88
+ dependencies {
89
+ implementation "com.facebook.react:react-android"
90
+ implementation "com.google.code.gson:gson:2.13.1"
91
+ }
@@ -0,0 +1,4 @@
1
+ -keep class com.callstack.brownie.BrownieStoreBridge {
2
+ void onStoreDidChange(java.lang.String);
3
+ native <methods>;
4
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.callstack.brownie" />
@@ -0,0 +1,68 @@
1
+ cmake_minimum_required(VERSION 3.13)
2
+
3
+ project(brownie)
4
+
5
+ set(CMAKE_CXX_STANDARD 20)
6
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
7
+
8
+ set(PACKAGE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../..")
9
+
10
+ SET(folly_FLAGS
11
+ -DFOLLY_NO_CONFIG=1
12
+ -DFOLLY_HAVE_CLOCK_GETTIME=1
13
+ -DFOLLY_USE_LIBCPP=1
14
+ -DFOLLY_CFG_NO_COROUTINES=1
15
+ -DFOLLY_MOBILE=1
16
+ -DFOLLY_HAVE_RECVMMSG=1
17
+ -DFOLLY_HAVE_PTHREAD=1
18
+ # Once we target android-23 above, we can comment
19
+ # the following line. NDK uses GNU style stderror_r() after API 23.
20
+ -DFOLLY_HAVE_XSI_STRERROR_R=1
21
+ )
22
+
23
+ add_compile_options(${folly_FLAGS})
24
+
25
+ add_library(
26
+ brownie
27
+ SHARED
28
+ JNIBrownieStoreBridge.cpp
29
+ "${PACKAGE_ROOT}/cpp/BrownieHostObject.cpp"
30
+ "${PACKAGE_ROOT}/cpp/BrownieInstaller.cpp"
31
+ "${PACKAGE_ROOT}/cpp/BrownieStore.cpp"
32
+ "${PACKAGE_ROOT}/cpp/BrownieStoreManager.cpp"
33
+ )
34
+
35
+ target_include_directories(
36
+ brownie
37
+ PRIVATE
38
+ "${PACKAGE_ROOT}/cpp"
39
+ )
40
+
41
+ find_library(log-lib log)
42
+ find_package(fbjni REQUIRED CONFIG)
43
+ find_package(ReactAndroid REQUIRED CONFIG)
44
+
45
+ target_link_libraries(
46
+ brownie
47
+ PRIVATE
48
+ ${log-lib}
49
+ fbjni::fbjni
50
+ )
51
+
52
+
53
+ if(TARGET ReactAndroid::jsi)
54
+ target_link_libraries(brownie PRIVATE ReactAndroid::jsi)
55
+ elseif(TARGET jsi)
56
+ target_link_libraries(brownie PRIVATE jsi)
57
+ endif()
58
+
59
+ if(TARGET ReactAndroid::reactnative)
60
+ target_link_libraries(brownie PRIVATE ReactAndroid::reactnative)
61
+ endif()
62
+
63
+ if(TARGET ReactAndroid::folly_runtime)
64
+ target_link_libraries(brownie PRIVATE ReactAndroid::folly_runtime)
65
+ elseif(TARGET folly_runtime)
66
+ target_link_libraries(brownie PRIVATE folly_runtime)
67
+ endif()
68
+
@@ -0,0 +1,233 @@
1
+ #include <jni.h>
2
+ #include <folly/dynamic.h>
3
+ #include <folly/json.h>
4
+ #include <jsi/jsi.h>
5
+ #include <memory>
6
+ #include <mutex>
7
+ #include <string>
8
+ #include "BrownieInstaller.h"
9
+ #include "BrownieStore.h"
10
+ #include "BrownieStoreManager.h"
11
+
12
+ namespace {
13
+
14
+ constexpr auto kBridgeClassName = "com/callstack/brownie/BrownieStoreBridge";
15
+ constexpr auto kOnStoreDidChangeMethod = "onStoreDidChange";
16
+ constexpr auto kOnStoreDidChangeSignature = "(Ljava/lang/String;)V";
17
+
18
+ JavaVM *g_vm = nullptr;
19
+ jclass g_bridgeClass = nullptr;
20
+ jmethodID g_onStoreDidChangeMethod = nullptr;
21
+ std::once_flag g_initMethodOnce;
22
+
23
+ std::string fromJString(JNIEnv *env, jstring value) {
24
+ if (value == nullptr) {
25
+ return "";
26
+ }
27
+
28
+ const char *chars = env->GetStringUTFChars(value, nullptr);
29
+ if (chars == nullptr) {
30
+ return "";
31
+ }
32
+
33
+ std::string result(chars);
34
+ env->ReleaseStringUTFChars(value, chars);
35
+ return result;
36
+ }
37
+
38
+ jstring toJString(JNIEnv *env, const std::string &value) {
39
+ return env->NewStringUTF(value.c_str());
40
+ }
41
+
42
+ bool initOnStoreDidChangeMethod(JNIEnv *env) {
43
+ bool success = true;
44
+ std::call_once(g_initMethodOnce, [env, &success]() {
45
+ auto localBridgeClass = env->FindClass(kBridgeClassName);
46
+ if (localBridgeClass == nullptr) {
47
+ success = false;
48
+ return;
49
+ }
50
+
51
+ g_bridgeClass = reinterpret_cast<jclass>(env->NewGlobalRef(localBridgeClass));
52
+ env->DeleteLocalRef(localBridgeClass);
53
+ if (g_bridgeClass == nullptr) {
54
+ success = false;
55
+ return;
56
+ }
57
+
58
+ g_onStoreDidChangeMethod = env->GetStaticMethodID(
59
+ g_bridgeClass, kOnStoreDidChangeMethod, kOnStoreDidChangeSignature);
60
+ if (g_onStoreDidChangeMethod == nullptr) {
61
+ success = false;
62
+ }
63
+ });
64
+
65
+ return success && g_bridgeClass != nullptr && g_onStoreDidChangeMethod != nullptr;
66
+ }
67
+
68
+ void emitStoreDidChange(const std::string &storeKey) {
69
+ if (g_vm == nullptr) {
70
+ return;
71
+ }
72
+
73
+ JNIEnv *env = nullptr;
74
+ bool didAttachCurrentThread = false;
75
+
76
+ if (g_vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
77
+ if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
78
+ return;
79
+ }
80
+ didAttachCurrentThread = true;
81
+ }
82
+
83
+ if (!initOnStoreDidChangeMethod(env)) {
84
+ if (didAttachCurrentThread) {
85
+ g_vm->DetachCurrentThread();
86
+ }
87
+ return;
88
+ }
89
+
90
+ auto jStoreKey = toJString(env, storeKey);
91
+ env->CallStaticVoidMethod(g_bridgeClass, g_onStoreDidChangeMethod, jStoreKey);
92
+ env->DeleteLocalRef(jStoreKey);
93
+
94
+ if (didAttachCurrentThread) {
95
+ g_vm->DetachCurrentThread();
96
+ }
97
+ }
98
+
99
+ std::shared_ptr<brownie::BrownieStore> getStoreOrNull(const std::string &storeKey) {
100
+ return brownie::BrownieStoreManager::shared().getStore(storeKey);
101
+ }
102
+
103
+ template <typename TCallback>
104
+ void withStore(JNIEnv *env, jstring storeKey, TCallback &&callback) {
105
+ auto store = getStoreOrNull(fromJString(env, storeKey));
106
+ if (!store) {
107
+ return;
108
+ }
109
+
110
+ callback(std::move(store));
111
+ }
112
+
113
+ template <typename TCallback>
114
+ jstring withStoreResult(JNIEnv *env, jstring storeKey, TCallback &&callback) {
115
+ auto store = getStoreOrNull(fromJString(env, storeKey));
116
+ if (!store) {
117
+ return nullptr;
118
+ }
119
+
120
+ return callback(std::move(store));
121
+ }
122
+
123
+ template <typename TCallback>
124
+ void withParsedJson(const std::string &json, TCallback &&callback) {
125
+ try {
126
+ callback(folly::parseJson(json));
127
+ } catch (const std::exception &) {
128
+ // Keep native bridge resilient to malformed payloads from Kotlin callers.
129
+ }
130
+ }
131
+
132
+ jstring toJsonJStringOrNull(JNIEnv *env, const folly::dynamic &value) {
133
+ try {
134
+ return toJString(env, folly::toJson(value));
135
+ } catch (const std::exception &) {
136
+ return nullptr;
137
+ }
138
+ }
139
+
140
+ } // namespace
141
+
142
+ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
143
+ g_vm = vm;
144
+ return JNI_VERSION_1_6;
145
+ }
146
+
147
+ extern "C" JNIEXPORT void JNICALL
148
+ Java_com_callstack_brownie_BrownieStoreBridge_nativeInstallJSIBindings(JNIEnv *,
149
+ jclass,
150
+ jlong runtimePointer) {
151
+ auto *runtime = reinterpret_cast<facebook::jsi::Runtime *>(runtimePointer);
152
+ if (runtime == nullptr) {
153
+ return;
154
+ }
155
+
156
+ brownie::BrownieInstaller::install(*runtime);
157
+ }
158
+
159
+ extern "C" JNIEXPORT void JNICALL
160
+ Java_com_callstack_brownie_BrownieStoreBridge_nativeRegisterStore(JNIEnv *env,
161
+ jclass,
162
+ jstring storeKey) {
163
+ auto store = std::make_shared<brownie::BrownieStore>();
164
+ auto key = fromJString(env, storeKey);
165
+ store->setChangeCallback([key]() { emitStoreDidChange(key); });
166
+ brownie::BrownieStoreManager::shared().registerStore(key, store);
167
+ }
168
+
169
+ extern "C" JNIEXPORT void JNICALL
170
+ Java_com_callstack_brownie_BrownieStoreBridge_nativeRemoveStore(JNIEnv *env,
171
+ jclass,
172
+ jstring storeKey) {
173
+ brownie::BrownieStoreManager::shared().removeStore(fromJString(env, storeKey));
174
+ }
175
+
176
+ extern "C" JNIEXPORT void JNICALL
177
+ Java_com_callstack_brownie_BrownieStoreBridge_nativeSetValue(JNIEnv *env,
178
+ jclass,
179
+ jstring valueJson,
180
+ jstring propKey,
181
+ jstring storeKey) {
182
+ withStore(env, storeKey, [env, propKey, valueJson](std::shared_ptr<brownie::BrownieStore> store) {
183
+ auto key = fromJString(env, propKey);
184
+ auto json = fromJString(env, valueJson);
185
+ withParsedJson(json, [&store, &key](folly::dynamic value) { store->set(key, std::move(value)); });
186
+ });
187
+ }
188
+
189
+ extern "C" JNIEXPORT jstring JNICALL
190
+ Java_com_callstack_brownie_BrownieStoreBridge_nativeGetValue(JNIEnv *env,
191
+ jclass,
192
+ jstring propKey,
193
+ jstring storeKey) {
194
+ return withStoreResult(env, storeKey, [env, propKey](std::shared_ptr<brownie::BrownieStore> store) {
195
+ auto key = fromJString(env, propKey);
196
+ return toJsonJStringOrNull(env, store->get(key));
197
+ });
198
+ }
199
+
200
+ extern "C" JNIEXPORT jstring JNICALL
201
+ Java_com_callstack_brownie_BrownieStoreBridge_nativeGetSnapshot(JNIEnv *env,
202
+ jclass,
203
+ jstring storeKey) {
204
+ return withStoreResult(env, storeKey, [env](std::shared_ptr<brownie::BrownieStore> store) {
205
+ return toJsonJStringOrNull(env, store->getSnapshot());
206
+ });
207
+ }
208
+
209
+ extern "C" JNIEXPORT void JNICALL
210
+ Java_com_callstack_brownie_BrownieStoreBridge_nativeSetState(JNIEnv *env,
211
+ jclass,
212
+ jstring stateJson,
213
+ jstring storeKey) {
214
+ withStore(env, storeKey, [env, stateJson](std::shared_ptr<brownie::BrownieStore> store) {
215
+ auto json = fromJString(env, stateJson);
216
+ withParsedJson(json, [&store](folly::dynamic state) { store->setState(std::move(state)); });
217
+ });
218
+ }
219
+
220
+ extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *, void *) {
221
+ if (g_vm == nullptr || g_bridgeClass == nullptr) {
222
+ return;
223
+ }
224
+
225
+ JNIEnv *env = nullptr;
226
+ if (g_vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
227
+ return;
228
+ }
229
+
230
+ env->DeleteGlobalRef(g_bridgeClass);
231
+ g_bridgeClass = nullptr;
232
+ g_onStoreDidChangeMethod = nullptr;
233
+ }
@@ -0,0 +1,84 @@
1
+ package com.callstack.brownie
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import java.util.concurrent.atomic.AtomicBoolean
8
+
9
+ /**
10
+ * TurboModule entrypoint for Brownie on Android.
11
+ *
12
+ * It installs JSI bindings and forwards native store change events to JavaScript.
13
+ */
14
+ class BrownieModule(reactContext: ReactApplicationContext) :
15
+ NativeBrownieModuleSpec(reactContext) {
16
+ private val mainHandler = Handler(Looper.getMainLooper())
17
+ private val didInstallJSI = AtomicBoolean(false)
18
+ private var storeDidChangeListenerId: String? = null
19
+
20
+ private val storeDidChangeListener: (String) -> Unit = { storeKey ->
21
+ val eventPayload =
22
+ Arguments.createMap().apply {
23
+ putString("storeKey", storeKey)
24
+ putString("key", "storeKey")
25
+ putString("value", storeKey)
26
+ }
27
+
28
+ if (Looper.myLooper() == Looper.getMainLooper()) {
29
+ emitNativeStoreDidChange(eventPayload)
30
+ } else {
31
+ mainHandler.post { emitNativeStoreDidChange(eventPayload) }
32
+ }
33
+ }
34
+
35
+ init {
36
+ storeDidChangeListenerId = BrownieStoreBridge.addStoreDidChangeListener(storeDidChangeListener)
37
+ }
38
+
39
+ /**
40
+ * Called by React Native when the module is initialized.
41
+ */
42
+ override fun initialize() {
43
+ super.initialize()
44
+ installJSIBindingsIfNeeded()
45
+ }
46
+
47
+ /**
48
+ * Called when the module is being torn down.
49
+ */
50
+ override fun invalidate() {
51
+ storeDidChangeListenerId?.let(BrownieStoreBridge::removeStoreDidChangeListener)
52
+ storeDidChangeListenerId = null
53
+ StoreManager.shared.clear()
54
+ super.invalidate()
55
+ }
56
+
57
+ /**
58
+ * Installs C++ JSI globals once a valid JS runtime pointer is available.
59
+ */
60
+ private fun installJSIBindingsIfNeeded() {
61
+ if (didInstallJSI.get()) {
62
+ return
63
+ }
64
+
65
+ val runtimePointer = reactApplicationContext.javaScriptContextHolder?.get() ?: 0L
66
+ if (runtimePointer == 0L) {
67
+ return
68
+ }
69
+
70
+ BrownieStoreBridge.installJSIBindings(runtimePointer)
71
+ didInstallJSI.set(true)
72
+ }
73
+
74
+ companion object {
75
+ const val NAME = "Brownie"
76
+ }
77
+
78
+ /**
79
+ * Exposes this module name to React Native.
80
+ */
81
+ override fun getName(): String {
82
+ return NAME
83
+ }
84
+ }
@@ -0,0 +1,33 @@
1
+ package com.callstack.brownie
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ /**
10
+ * React Native package that registers the Brownie TurboModule.
11
+ */
12
+ class BrowniePackage : BaseReactPackage() {
13
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
14
+ return if (name == BrownieModule.NAME) {
15
+ BrownieModule(reactContext)
16
+ } else {
17
+ null
18
+ }
19
+ }
20
+
21
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
22
+ mapOf(
23
+ BrownieModule.NAME to ReactModuleInfo(
24
+ name = BrownieModule.NAME,
25
+ className = BrownieModule.NAME,
26
+ canOverrideExistingModule = false,
27
+ needsEagerInit = false,
28
+ isCxxModule = false,
29
+ isTurboModule = true
30
+ )
31
+ )
32
+ }
33
+ }
@@ -0,0 +1,116 @@
1
+ package com.callstack.brownie
2
+
3
+ import java.util.UUID
4
+ import java.util.concurrent.ConcurrentHashMap
5
+
6
+ /**
7
+ * Kotlin/JNI bridge for interacting with the shared C++ Brownie store runtime.
8
+ */
9
+ object BrownieStoreBridge {
10
+ private val storeDidChangeListeners = ConcurrentHashMap<String, (String) -> Unit>()
11
+
12
+ init {
13
+ System.loadLibrary("brownie")
14
+ }
15
+
16
+ /**
17
+ * Registers a listener for store change notifications emitted from native.
18
+ *
19
+ * @return a listener id used to remove this listener.
20
+ */
21
+ fun addStoreDidChangeListener(listener: (String) -> Unit): String {
22
+ val listenerId = UUID.randomUUID().toString()
23
+ storeDidChangeListeners[listenerId] = listener
24
+ return listenerId
25
+ }
26
+
27
+ /**
28
+ * Removes a previously registered store change listener.
29
+ */
30
+ fun removeStoreDidChangeListener(listenerId: String) {
31
+ storeDidChangeListeners.remove(listenerId)
32
+ }
33
+
34
+ /**
35
+ * Creates and registers a C++ store for [storeKey].
36
+ */
37
+ fun registerStore(storeKey: String) {
38
+ nativeRegisterStore(storeKey)
39
+ }
40
+
41
+ /**
42
+ * Removes a C++ store for [storeKey].
43
+ */
44
+ fun removeStore(storeKey: String) {
45
+ nativeRemoveStore(storeKey)
46
+ }
47
+
48
+ /**
49
+ * Sets a single property on a store using JSON payload.
50
+ */
51
+ fun setValue(valueJson: String, propKey: String, storeKey: String) {
52
+ nativeSetValue(valueJson, propKey, storeKey)
53
+ }
54
+
55
+ /**
56
+ * Gets a single property from a store as JSON.
57
+ */
58
+ fun getValue(propKey: String, storeKey: String): String? {
59
+ return nativeGetValue(propKey, storeKey)
60
+ }
61
+
62
+ /**
63
+ * Gets the full store snapshot as JSON.
64
+ */
65
+ fun getSnapshot(storeKey: String): String? {
66
+ return nativeGetSnapshot(storeKey)
67
+ }
68
+
69
+ /**
70
+ * Replaces full store state with the provided JSON payload.
71
+ */
72
+ fun setState(stateJson: String, storeKey: String) {
73
+ nativeSetState(stateJson, storeKey)
74
+ }
75
+
76
+ /**
77
+ * Installs Brownie JSI bindings into the provided JS runtime.
78
+ */
79
+ fun installJSIBindings(runtimePointer: Long) {
80
+ nativeInstallJSIBindings(runtimePointer)
81
+ }
82
+
83
+ /**
84
+ * Entry point called from JNI when a store changes in the C++ layer.
85
+ */
86
+ @JvmStatic
87
+ private fun onStoreDidChange(storeKey: String) {
88
+ storeDidChangeListeners.values.forEach { listener ->
89
+ listener.invoke(storeKey)
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Native bindings implemented in `JNIBrownieStoreBridge.cpp`.
95
+ */
96
+ @JvmStatic
97
+ private external fun nativeInstallJSIBindings(runtimePointer: Long)
98
+
99
+ @JvmStatic
100
+ private external fun nativeRegisterStore(storeKey: String)
101
+
102
+ @JvmStatic
103
+ private external fun nativeRemoveStore(storeKey: String)
104
+
105
+ @JvmStatic
106
+ private external fun nativeSetValue(valueJson: String, propKey: String, storeKey: String)
107
+
108
+ @JvmStatic
109
+ private external fun nativeGetValue(propKey: String, storeKey: String): String?
110
+
111
+ @JvmStatic
112
+ private external fun nativeGetSnapshot(storeKey: String): String?
113
+
114
+ @JvmStatic
115
+ private external fun nativeSetState(stateJson: String, storeKey: String)
116
+ }
@@ -0,0 +1,83 @@
1
+ package com.callstack.brownie
2
+
3
+ import com.google.gson.Gson
4
+
5
+ /**
6
+ * Serializer used by [Store] to sync typed Kotlin state with the C++ store as JSON.
7
+ */
8
+ interface BrownieStoreSerializer<State> {
9
+ /**
10
+ * Encodes a typed state object into a JSON string.
11
+ */
12
+ fun encode(state: State): String
13
+
14
+ /**
15
+ * Decodes a JSON snapshot from native into a typed state object.
16
+ */
17
+ fun decode(snapshotJson: String): State
18
+ }
19
+
20
+ /**
21
+ * Immutable description of a store key and its serialization strategy.
22
+ */
23
+ interface BrownieStoreDefinition<State> {
24
+ val storeName: String
25
+ val serializer: BrownieStoreSerializer<State>
26
+ }
27
+
28
+ private val brownieJson = Gson()
29
+
30
+ private class BrownieStoreDefinitionImpl<State>(
31
+ override val storeName: String,
32
+ override val serializer: BrownieStoreSerializer<State>,
33
+ ) : BrownieStoreDefinition<State>
34
+
35
+ private class JsonBrownieStoreSerializer<State>(
36
+ private val clazz: Class<State>,
37
+ ) : BrownieStoreSerializer<State> {
38
+ override fun encode(state: State): String = brownieJson.toJson(state)
39
+
40
+ override fun decode(snapshotJson: String): State = brownieJson.fromJson(snapshotJson, clazz)
41
+ }
42
+
43
+ /**
44
+ * Creates a store definition backed by Gson using a runtime [Class].
45
+ */
46
+ fun <State : Any> brownieStoreDefinition(
47
+ storeName: String,
48
+ clazz: Class<State>,
49
+ ): BrownieStoreDefinition<State> = brownieStoreDefinition(storeName, JsonBrownieStoreSerializer(clazz))
50
+
51
+ /**
52
+ * Creates a store definition backed by Gson using a reified state type.
53
+ */
54
+ inline fun <reified State : Any> brownieStoreDefinition(
55
+ storeName: String,
56
+ ): BrownieStoreDefinition<State> = brownieStoreDefinition(storeName, State::class.java)
57
+
58
+ /**
59
+ * Creates a store definition with a custom serializer implementation.
60
+ */
61
+ fun <State> brownieStoreDefinition(
62
+ storeName: String,
63
+ serializer: BrownieStoreSerializer<State>,
64
+ ): BrownieStoreDefinition<State> = BrownieStoreDefinitionImpl(storeName, serializer)
65
+
66
+ /**
67
+ * Creates a store definition from encode/decode lambdas.
68
+ */
69
+ fun <State> brownieStoreDefinition(
70
+ storeName: String,
71
+ encode: (State) -> String,
72
+ decode: (String) -> State,
73
+ ): BrownieStoreDefinition<State> {
74
+ return brownieStoreDefinition(
75
+ storeName = storeName,
76
+ serializer =
77
+ object : BrownieStoreSerializer<State> {
78
+ override fun encode(state: State): String = encode(state)
79
+
80
+ override fun decode(snapshotJson: String): State = decode(snapshotJson)
81
+ },
82
+ )
83
+ }
@@ -0,0 +1,55 @@
1
+ package com.callstack.brownie
2
+
3
+ /**
4
+ * Registers a new [Store] instance from this definition and the provided initial state.
5
+ */
6
+ fun <State> BrownieStoreDefinition<State>.register(initialState: State): Store<State> {
7
+ return Store(initialState, storeName, serializer)
8
+ }
9
+
10
+ /**
11
+ * Convenience API that creates a definition with Gson and registers it.
12
+ */
13
+ fun <State : Any> registerStore(
14
+ storeName: String,
15
+ initialState: State,
16
+ clazz: Class<State>,
17
+ ): Store<State> = brownieStoreDefinition(storeName, clazz).register(initialState)
18
+
19
+ /**
20
+ * Reified overload of [registerStore].
21
+ */
22
+ inline fun <reified State : Any> registerStore(
23
+ storeName: String,
24
+ initialState: State,
25
+ ): Store<State> = registerStore(storeName, initialState, State::class.java)
26
+
27
+ /**
28
+ * Registers once per store name and returns null when the store was already registered.
29
+ *
30
+ * Idempotency is based on whether a store with this [storeName] currently exists
31
+ * in [StoreManager], so clearing or removing a store allows registration again
32
+ * within the same process.
33
+ */
34
+ fun <State> BrownieStoreDefinition<State>.registerIfNeeded(initialState: () -> State): Store<State>? {
35
+ return StoreManager.shared.registerIfAbsent(storeName) {
36
+ register(initialState())
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Convenience API that registers only once for the provided key.
42
+ */
43
+ fun <State : Any> registerStoreIfNeeded(
44
+ storeName: String,
45
+ initialState: () -> State,
46
+ clazz: Class<State>,
47
+ ): Store<State>? = brownieStoreDefinition(storeName, clazz).registerIfNeeded(initialState)
48
+
49
+ /**
50
+ * Reified overload of [registerStoreIfNeeded].
51
+ */
52
+ inline fun <reified State : Any> registerStoreIfNeeded(
53
+ storeName: String,
54
+ noinline initialState: () -> State,
55
+ ): Store<State>? = registerStoreIfNeeded(storeName, initialState, State::class.java)
@@ -0,0 +1,173 @@
1
+ package com.callstack.brownie
2
+
3
+ import java.util.concurrent.CopyOnWriteArraySet
4
+ import java.util.concurrent.atomic.AtomicBoolean
5
+ import java.util.concurrent.locks.ReentrantLock
6
+ import kotlin.concurrent.withLock
7
+
8
+ /**
9
+ * Typed Kotlin facade over a shared C++ Brownie store.
10
+ *
11
+ * It keeps local typed state in sync with native snapshots and notifies subscribers on changes.
12
+ */
13
+ class Store<State>(
14
+ initialState: State,
15
+ private val storeKey: String,
16
+ private val serializer: BrownieStoreSerializer<State>,
17
+ ) : AutoCloseable {
18
+ private val stateLock = ReentrantLock()
19
+ private val listeners = CopyOnWriteArraySet<(State) -> Unit>()
20
+ private val disposed = AtomicBoolean(false)
21
+
22
+ @Volatile
23
+ private var _state: State = initialState
24
+
25
+ /**
26
+ * Latest typed state snapshot known on the Kotlin side.
27
+ */
28
+ val state: State
29
+ get() = stateLock.withLock { _state }
30
+
31
+ private val bridgeListenerId: String
32
+
33
+ init {
34
+ BrownieStoreBridge.registerStore(storeKey)
35
+ pushStateToCxx()
36
+
37
+ bridgeListenerId =
38
+ BrownieStoreBridge.addStoreDidChangeListener { updatedStoreKey ->
39
+ if (updatedStoreKey == storeKey) {
40
+ rebuildState()
41
+ }
42
+ }
43
+
44
+ StoreManager.shared.register(this, storeKey)
45
+ }
46
+
47
+ /**
48
+ * Updates state with [updater] and pushes it to C++.
49
+ *
50
+ * Listener notification is triggered via the native storeDidChange callback and [rebuildState]
51
+ * to keep Kotlin updates consistent with cross-runtime updates.
52
+ */
53
+ fun set(updater: (State) -> State) {
54
+ val newState =
55
+ stateLock.withLock {
56
+ _state = updater(_state)
57
+ _state
58
+ }
59
+
60
+ pushStateToCxx(newState)
61
+ }
62
+
63
+ /**
64
+ * Replaces state with a concrete value.
65
+ */
66
+ fun set(value: State) {
67
+ set { value }
68
+ }
69
+
70
+ /**
71
+ * Sets a single property in the underlying C++ store using JSON payload.
72
+ */
73
+ fun setValue(property: String, valueJson: String) {
74
+ BrownieStoreBridge.setValue(valueJson, property, storeKey)
75
+ }
76
+
77
+ /**
78
+ * Reads a single property from the underlying C++ store as JSON.
79
+ */
80
+ fun getValue(property: String): String? = BrownieStoreBridge.getValue(property, storeKey)
81
+
82
+ /**
83
+ * Subscribes to full state updates.
84
+ *
85
+ * The listener is called immediately with current state and then on every update.
86
+ */
87
+ fun subscribe(onChange: (State) -> Unit): () -> Unit {
88
+ listeners.add(onChange)
89
+ onChange(state)
90
+ return { listeners.remove(onChange) }
91
+ }
92
+
93
+ /**
94
+ * Releases local listeners and bridge callbacks; called by [StoreManager].
95
+ */
96
+ internal fun dispose() {
97
+ if (!disposed.compareAndSet(false, true)) {
98
+ return
99
+ }
100
+
101
+ BrownieStoreBridge.removeStoreDidChangeListener(bridgeListenerId)
102
+ listeners.clear()
103
+ }
104
+
105
+ /**
106
+ * Closes this store and removes it from [StoreManager].
107
+ */
108
+ override fun close() {
109
+ StoreManager.shared.removeStore(storeKey)
110
+ }
111
+
112
+ /**
113
+ * Serializes typed state and pushes it to the C++ store.
114
+ */
115
+ private fun pushStateToCxx(state: State = this.state) {
116
+ BrownieStoreBridge.setState(serializer.encode(state), storeKey)
117
+ }
118
+
119
+ /**
120
+ * Pulls latest snapshot from C++, decodes it, and updates local state.
121
+ */
122
+ private fun rebuildState() {
123
+ val snapshot = BrownieStoreBridge.getSnapshot(storeKey) ?: return
124
+ val rebuiltState = runCatching { serializer.decode(snapshot) }.getOrNull() ?: return
125
+
126
+ stateLock.withLock {
127
+ _state = rebuiltState
128
+ }
129
+
130
+ notifyListeners(rebuiltState)
131
+ }
132
+
133
+ /**
134
+ * Notifies all active local subscribers with [state].
135
+ */
136
+ private fun notifyListeners(state: State) {
137
+ listeners.forEach { listener ->
138
+ listener(state)
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Subscribes to a selected slice of store state.
145
+ *
146
+ * The listener is invoked only when the selected value changes by `!=`.
147
+ */
148
+ fun <State, Selected> Store<State>.subscribe(
149
+ selector: (State) -> Selected,
150
+ onChange: (Selected) -> Unit,
151
+ ): () -> Unit {
152
+ val selectorLock = ReentrantLock()
153
+ var hasSelection = false
154
+ var previousSelection: Selected? = null
155
+
156
+ return subscribe { state ->
157
+ val newSelection = selector(state)
158
+ val shouldNotify =
159
+ selectorLock.withLock {
160
+ if (!hasSelection || previousSelection != newSelection) {
161
+ hasSelection = true
162
+ previousSelection = newSelection
163
+ true
164
+ } else {
165
+ false
166
+ }
167
+ }
168
+
169
+ if (shouldNotify) {
170
+ onChange(newSelection)
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,93 @@
1
+ package com.callstack.brownie
2
+
3
+ import java.util.concurrent.locks.ReentrantLock
4
+ import kotlin.concurrent.withLock
5
+
6
+ /**
7
+ * Process-wide registry of Kotlin [Store] instances keyed by store name.
8
+ */
9
+ class StoreManager private constructor() {
10
+ companion object {
11
+ /**
12
+ * Shared singleton used by Brownie runtime and app integrations.
13
+ */
14
+ val shared: StoreManager = StoreManager()
15
+ }
16
+
17
+ private val lock = ReentrantLock()
18
+ private val stores: MutableMap<String, Any> = mutableMapOf()
19
+
20
+ /**
21
+ * Registers a store instance under a key.
22
+ */
23
+ fun <State> register(store: Store<State>, key: String) {
24
+ lock.withLock {
25
+ check(!stores.containsKey(key)) {
26
+ "Store with key '$key' is already registered. Remove the previous store first using Store.close() or StoreManager.removeStore(key)"
27
+ }
28
+ stores[key] = store
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Registers a new store only when [key] is currently absent.
34
+ *
35
+ * Returns the created store when registration succeeds, otherwise `null`.
36
+ * The check and registration are atomic under [lock].
37
+ */
38
+ fun <State> registerIfAbsent(key: String, createStore: () -> Store<State>): Store<State>? {
39
+ return lock.withLock {
40
+ if (stores.containsKey(key)) {
41
+ return@withLock null
42
+ }
43
+
44
+ val store = createStore()
45
+ stores[key] = store
46
+ store
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Retrieves a typed store by key when the runtime state type matches [clazz].
52
+ */
53
+ fun <State> store(key: String, clazz: Class<State>): Store<State>? {
54
+ return lock.withLock {
55
+ val store = stores[key] as? Store<*> ?: return@withLock null
56
+ runCatching { clazz.cast(store.state) }.getOrNull() ?: return@withLock null
57
+ @Suppress("UNCHECKED_CAST")
58
+ store as Store<State>
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Removes a store from the registry and from the native bridge.
64
+ */
65
+ fun removeStore(key: String) {
66
+ val store = lock.withLock {
67
+ stores.remove(key) as? Store<*>
68
+ }
69
+
70
+ store?.dispose()
71
+ BrownieStoreBridge.removeStore(key)
72
+ }
73
+
74
+ /**
75
+ * Removes all registered stores.
76
+ */
77
+ fun clear() {
78
+ val keys = lock.withLock {
79
+ stores.keys.toList()
80
+ }
81
+
82
+ keys.forEach { key ->
83
+ removeStore(key)
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Reified convenience overload for [StoreManager.store].
90
+ */
91
+ inline fun <reified State : Any> StoreManager.store(key: String): Store<State>? {
92
+ return store(key, State::class.java)
93
+ }
@@ -16,16 +16,18 @@ folly::dynamic BrownieStore::get(const std::string &key) const {
16
16
  }
17
17
 
18
18
  void BrownieStore::set(const std::string &key, folly::dynamic value) {
19
+ ChangeCallback callback;
19
20
  {
20
21
  std::lock_guard<std::mutex> lock(mutex_);
21
22
  if (!state_.isObject()) {
22
23
  state_ = folly::dynamic::object();
23
24
  }
24
25
  state_[key] = std::move(value);
26
+ callback = changeCallback_;
25
27
  }
26
28
 
27
- if (changeCallback_) {
28
- changeCallback_();
29
+ if (callback) {
30
+ callback();
29
31
  }
30
32
  }
31
33
 
@@ -35,13 +37,15 @@ folly::dynamic BrownieStore::getSnapshot() const {
35
37
  }
36
38
 
37
39
  void BrownieStore::setState(folly::dynamic state) {
40
+ ChangeCallback callback;
38
41
  {
39
42
  std::lock_guard<std::mutex> lock(mutex_);
40
43
  state_ = std::move(state);
44
+ callback = changeCallback_;
41
45
  }
42
46
 
43
- if (changeCallback_) {
44
- changeCallback_();
47
+ if (callback) {
48
+ callback();
45
49
  }
46
50
  }
47
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/brownie",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "license": "MIT",
5
5
  "author": "Oskar Kwaśniewski <oskarkwasniewski@icloud.com>",
6
6
  "bin": {
@@ -50,10 +50,16 @@
50
50
  "files": [
51
51
  "src",
52
52
  "lib",
53
+ "android",
53
54
  "ios",
54
55
  "cpp",
55
56
  "*.podspec",
56
57
  "!ios/build",
58
+ "!android/build",
59
+ "!android/gradle",
60
+ "!android/gradlew",
61
+ "!android/gradlew.bat",
62
+ "!android/local.properties",
57
63
  "!**/__tests__",
58
64
  "!**/__fixtures__",
59
65
  "!**/__mocks__",
@@ -69,7 +75,7 @@
69
75
  "react-native": "*"
70
76
  },
71
77
  "dependencies": {
72
- "@callstack/brownfield-cli": "^3.3.0",
78
+ "@callstack/brownfield-cli": "^3.5.0",
73
79
  "ts-morph": "^27.0.2"
74
80
  },
75
81
  "devDependencies": {
@@ -79,14 +85,14 @@
79
85
  "@babel/runtime": "^7.25.0",
80
86
  "@react-native/babel-preset": "0.82.1",
81
87
  "@react-native/eslint-config": "0.82.1",
82
- "@types/node": "^25.0.8",
88
+ "@types/node": "^25.5.0",
83
89
  "@types/react": "^19.1.1",
84
- "eslint": "^9.28.0",
85
- "globals": "^16.2.0",
90
+ "eslint": "^9.39.3",
91
+ "globals": "^17.3.0",
86
92
  "import": "^0.0.6",
87
- "nodemon": "^3.1.11",
93
+ "nodemon": "^3.1.14",
88
94
  "react-native": "0.82.1",
89
- "react-native-builder-bob": "^0.40.17",
95
+ "react-native-builder-bob": "^0.40.18",
90
96
  "typescript": "5.9.3"
91
97
  },
92
98
  "codegenConfig": {