@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 +22 -0
- package/android/build.gradle +91 -0
- package/android/consumer-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +68 -0
- package/android/src/main/cpp/JNIBrownieStoreBridge.cpp +233 -0
- package/android/src/main/java/com/callstack/brownie/BrownieModule.kt +84 -0
- package/android/src/main/java/com/callstack/brownie/BrowniePackage.kt +33 -0
- package/android/src/main/java/com/callstack/brownie/BrownieStoreBridge.kt +116 -0
- package/android/src/main/java/com/callstack/brownie/BrownieStoreDefinition.kt +83 -0
- package/android/src/main/java/com/callstack/brownie/BrownieStoreRegistration.kt +55 -0
- package/android/src/main/java/com/callstack/brownie/Store.kt +173 -0
- package/android/src/main/java/com/callstack/brownie/StoreManager.kt +93 -0
- package/cpp/BrownieStore.cpp +8 -4
- package/package.json +13 -7
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,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
|
+
}
|
package/cpp/BrownieStore.cpp
CHANGED
|
@@ -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 (
|
|
28
|
-
|
|
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 (
|
|
44
|
-
|
|
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
|
+
"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.
|
|
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
|
|
88
|
+
"@types/node": "^25.5.0",
|
|
83
89
|
"@types/react": "^19.1.1",
|
|
84
|
-
"eslint": "^9.
|
|
85
|
-
"globals": "^
|
|
90
|
+
"eslint": "^9.39.3",
|
|
91
|
+
"globals": "^17.3.0",
|
|
86
92
|
"import": "^0.0.6",
|
|
87
|
-
"nodemon": "^3.1.
|
|
93
|
+
"nodemon": "^3.1.14",
|
|
88
94
|
"react-native": "0.82.1",
|
|
89
|
-
"react-native-builder-bob": "^0.40.
|
|
95
|
+
"react-native-builder-bob": "^0.40.18",
|
|
90
96
|
"typescript": "5.9.3"
|
|
91
97
|
},
|
|
92
98
|
"codegenConfig": {
|