expo-modules-core 1.12.2 → 1.12.4

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
@@ -10,6 +10,19 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 1.12.4 — 2024-04-29
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [Android] Fixed gziped payload does not correctly shown in network inspector. ([#28486](https://github.com/expo/expo/pull/28486) by [@kudo](https://github.com/kudo))
18
+ - [Android] Fixed crashes during the deallocation of shared objects. ([#28491](https://github.com/expo/expo/pull/28491) by [@lukmccall](https://github.com/lukmccall))
19
+
20
+ ## 1.12.3 — 2024-04-26
21
+
22
+ ### 🐛 Bug fixes
23
+
24
+ - [Android] Fixes SEGFAULTs caused by `std::shared_ptr<JavaCalllback::CallbackContext>::__on_zero_shared`. ([#28483](https://github.com/expo/expo/pull/28483) by [@lukmccall](https://github.com/lukmccall))
25
+
13
26
  ## 1.12.2 — 2024-04-23
14
27
 
15
28
  _This version does not introduce any user-facing changes._
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'host.exp.exponent'
4
- version = '1.12.2'
4
+ version = '1.12.4'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -63,7 +63,7 @@ android {
63
63
  defaultConfig {
64
64
  consumerProguardFiles 'proguard-rules.pro'
65
65
  versionCode 1
66
- versionName "1.12.2"
66
+ versionName "1.12.4"
67
67
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString()
68
68
 
69
69
  testInstrumentationRunner "expo.modules.TestRunner"
@@ -29,6 +29,42 @@ namespace expo {
29
29
 
30
30
  #endif
31
31
 
32
+ /*
33
+ * A wrapper for a global reference that can be deallocated on any thread.
34
+ * It should be used with smart pointer. That structure can't be copied or moved.
35
+ */
36
+ template <typename T>
37
+ class ThreadSafeJNIGlobalRef {
38
+ public:
39
+ ThreadSafeJNIGlobalRef(jobject globalRef) : globalRef(globalRef) {}
40
+ ThreadSafeJNIGlobalRef(const ThreadSafeJNIGlobalRef &other) = delete;
41
+ ThreadSafeJNIGlobalRef(ThreadSafeJNIGlobalRef &&other) = delete;
42
+ ThreadSafeJNIGlobalRef &operator=(const ThreadSafeJNIGlobalRef &other) = delete;
43
+ ThreadSafeJNIGlobalRef &operator=(ThreadSafeJNIGlobalRef &&other) = delete;
44
+
45
+ void use(std::function<void(jni::alias_ref<T> globalRef)> &&action) {
46
+ if (globalRef == nullptr) {
47
+ throw std::runtime_error("ThreadSafeJNIGlobalRef: globalRef is null");
48
+ }
49
+
50
+ jni::ThreadScope::WithClassLoader([this, action = std::move(action)]() {
51
+ jni::alias_ref<jobject> aliasRef = jni::wrap_alias(globalRef);
52
+ jni::alias_ref<T> jsiContextRef = jni::static_ref_cast<T>(aliasRef);
53
+ action(jsiContextRef);
54
+ });
55
+ }
56
+
57
+ ~ThreadSafeJNIGlobalRef() {
58
+ if (globalRef != nullptr) {
59
+ jni::ThreadScope::WithClassLoader([this] {
60
+ jni::Environment::current()->DeleteGlobalRef(this->globalRef);
61
+ });
62
+ }
63
+ }
64
+
65
+ jobject globalRef;
66
+ };
67
+
32
68
  jni::local_ref<JSIContext::jhybriddata>
33
69
  JSIContext::initHybrid(jni::alias_ref<jhybridobject> jThis) {
34
70
  return makeCxxInstance(jThis);
@@ -137,13 +173,17 @@ void JSIContext::prepareRuntime() {
137
173
 
138
174
  EventEmitter::installClass(runtime);
139
175
 
176
+ auto threadSafeRef = std::make_shared<ThreadSafeJNIGlobalRef<JSIContext::javaobject>>(
177
+ jni::Environment::current()->NewGlobalRef(javaPart_.get())
178
+ );
179
+
140
180
  SharedObject::installBaseClass(
141
181
  runtime,
142
182
  // We can't predict the order of deallocation of the JSIContext and the SharedObject.
143
183
  // So we need to pass a new ref to retain the JSIContext to make sure it's not deallocated before the SharedObject.
144
- [javaObject = javaPart_](const SharedObject::ObjectId objectId) {
145
- jni::ThreadScope::WithClassLoader([objectId = objectId, javaObject = std::move(javaObject)] {
146
- JSIContext::deleteSharedObject(javaObject, objectId);
184
+ [threadSafeRef = std::move(threadSafeRef)](const SharedObject::ObjectId objectId) {
185
+ threadSafeRef->use([objectId](jni::alias_ref<JSIContext::javaobject> globalRef) {
186
+ JSIContext::deleteSharedObject(globalRef, objectId);
147
187
  });
148
188
  }
149
189
  );
@@ -169,7 +209,8 @@ void JSIContext::prepareRuntime() {
169
209
  jni::local_ref<JavaScriptModuleObject::javaobject>
170
210
  JSIContext::callGetJavaScriptModuleObjectMethod(const std::string &moduleName) const {
171
211
  if (javaPart_ == nullptr) {
172
- throw std::runtime_error("getJavaScriptModuleObject: JSIContext was prepared to be deallocated.");
212
+ throw std::runtime_error(
213
+ "getJavaScriptModuleObject: JSIContext was prepared to be deallocated.");
173
214
  }
174
215
 
175
216
  const static auto method = expo::JSIContext::javaClassLocal()
@@ -270,7 +311,7 @@ void JSIContext::registerSharedObject(
270
311
  }
271
312
 
272
313
  void JSIContext::deleteSharedObject(
273
- jni::global_ref<JSIContext::javaobject> javaObject,
314
+ jni::alias_ref<JSIContext::javaobject> javaObject,
274
315
  int objectId
275
316
  ) {
276
317
  if (javaObject == nullptr) {
@@ -324,13 +365,17 @@ void JSIContext::jniSetNativeStateForSharedObject(
324
365
  int id,
325
366
  jni::alias_ref<JavaScriptObject::javaobject> jsObject
326
367
  ) {
368
+ auto threadSafeRef = std::make_shared<ThreadSafeJNIGlobalRef<JSIContext::javaobject>>(
369
+ jni::Environment::current()->NewGlobalRef(javaPart_.get())
370
+ );
371
+
327
372
  auto nativeState = std::make_shared<expo::SharedObject::NativeState>(
328
373
  id,
329
374
  // We can't predict the order of deallocation of the JSIContext and the SharedObject.
330
375
  // So we need to pass a new ref to retain the JSIContext to make sure it's not deallocated before the SharedObject.
331
- [javaObject = javaPart_](const SharedObject::ObjectId objectId) {
332
- jni::ThreadScope::WithClassLoader([objectId = objectId, javaObject = std::move(javaObject)] {
333
- JSIContext::deleteSharedObject(javaObject, objectId);
376
+ [threadSafeRef = std::move(threadSafeRef)](const SharedObject::ObjectId objectId) {
377
+ threadSafeRef->use([objectId](jni::alias_ref<JSIContext::javaobject> globalRef) {
378
+ JSIContext::deleteSharedObject(globalRef, objectId);
334
379
  });
335
380
  }
336
381
  );
@@ -123,7 +123,7 @@ public:
123
123
  );
124
124
 
125
125
  static void deleteSharedObject(
126
- jni::global_ref<JSIContext::javaobject> javaObject,
126
+ jni::alias_ref<JSIContext::javaobject> javaObject,
127
127
  int objectId
128
128
  );
129
129
 
@@ -13,6 +13,39 @@
13
13
 
14
14
  namespace expo {
15
15
 
16
+ #if REACT_NATIVE_TARGET_VERSION >= 75
17
+
18
+ JavaCallback::CallbackContext::CallbackContext(
19
+ jsi::Runtime &rt,
20
+ std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
21
+ std::optional<jsi::Function> resolveHolder,
22
+ std::optional<jsi::Function> rejectHolder
23
+ ) : react::LongLivedObject(rt),
24
+ rt(rt),
25
+ jsCallInvokerHolder(std::move(jsCallInvokerHolder)),
26
+ resolveHolder(std::move(resolveHolder)),
27
+ rejectHolder(std::move(rejectHolder)) {}
28
+
29
+ #else
30
+
31
+ JavaCallback::CallbackContext::CallbackContext(
32
+ jsi::Runtime &rt,
33
+ std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
34
+ std::optional<jsi::Function> resolveHolder,
35
+ std::optional<jsi::Function> rejectHolder
36
+ ) : rt(rt),
37
+ jsCallInvokerHolder(std::move(jsCallInvokerHolder)),
38
+ resolveHolder(std::move(resolveHolder)),
39
+ rejectHolder(std::move(rejectHolder)) {}
40
+
41
+ #endif
42
+
43
+ void JavaCallback::CallbackContext::invalidate() {
44
+ resolveHolder.reset();
45
+ rejectHolder.reset();
46
+ allowRelease();
47
+ }
48
+
16
49
  JavaCallback::JavaCallback(std::shared_ptr<CallbackContext> callbackContext)
17
50
  : callbackContext(std::move(callbackContext)) {}
18
51
 
@@ -27,6 +60,7 @@ void JavaCallback::registerNatives() {
27
60
  makeNativeMethod("invokeNative", JavaCallback::invokeArray),
28
61
  makeNativeMethod("invokeNative", JavaCallback::invokeMap),
29
62
  makeNativeMethod("invokeNative", JavaCallback::invokeSharedRef),
63
+ makeNativeMethod("invokeNative", JavaCallback::invokeError),
30
64
  });
31
65
  }
32
66
 
@@ -45,24 +79,41 @@ void JavaCallback::invokeJSFunction(
45
79
  ArgsConverter<T> argsConverter,
46
80
  T arg
47
81
  ) {
48
- const auto jsInvoker = callbackContext->jsCallInvokerHolder;
82
+ const auto strongCallbackContext = this->callbackContext.lock();
83
+ // The context were deallocated before the callback was invoked.
84
+ if (strongCallbackContext == nullptr) {
85
+ return;
86
+ }
87
+
88
+ const auto jsInvoker = strongCallbackContext->jsCallInvokerHolder.lock();
89
+ // Call invoker is already released, so we cannot invoke the callback.
90
+ if (jsInvoker == nullptr) {
91
+ return;
92
+ }
93
+
49
94
  jsInvoker->invokeAsync(
50
95
  [
51
- context = std::move(callbackContext),
96
+ context = callbackContext,
52
97
  argsConverter = std::move(argsConverter),
53
98
  arg = std::move(arg)
54
99
  ]() -> void {
55
- if (!context->jsFunctionHolder.has_value()) {
100
+ auto strongContext = context.lock();
101
+ // The context were deallocated before the callback was invoked.
102
+ if (strongContext == nullptr) {
103
+ return;
104
+ }
105
+
106
+ if (!strongContext->resolveHolder.has_value()) {
56
107
  throw std::runtime_error(
57
108
  "JavaCallback was already settled. Cannot invoke it again"
58
109
  );
59
110
  }
60
111
 
61
- jsi::Function &jsFunction = context->jsFunctionHolder.value();
62
- jsi::Runtime &rt = context->rt;
112
+ jsi::Function &jsFunction = strongContext->resolveHolder.value();
113
+ jsi::Runtime &rt = strongContext->rt;
63
114
 
64
- argsConverter(rt, jsFunction, std::move(arg), context->isRejectCallback);
65
- context->jsFunctionHolder.reset();
115
+ argsConverter(rt, jsFunction, std::move(arg));
116
+ strongContext->invalidate();
66
117
  });
67
118
  }
68
119
 
@@ -72,8 +123,7 @@ void JavaCallback::invokeJSFunction(T arg) {
72
123
  [](
73
124
  jsi::Runtime &rt,
74
125
  jsi::Function &jsFunction,
75
- T arg,
76
- bool isRejectCallback
126
+ T arg
77
127
  ) {
78
128
  jsFunction.call(rt, {jsi::Value(rt, arg)});
79
129
  },
@@ -86,8 +136,7 @@ void JavaCallback::invoke() {
86
136
  [](
87
137
  jsi::Runtime &rt,
88
138
  jsi::Function &jsFunction,
89
- nullptr_t arg,
90
- bool isRejectCallback
139
+ nullptr_t arg
91
140
  ) {
92
141
  jsFunction.call(rt, {jsi::Value::null()});
93
142
  },
@@ -116,8 +165,7 @@ void JavaCallback::invokeString(jni::alias_ref<jstring> result) {
116
165
  [](
117
166
  jsi::Runtime &rt,
118
167
  jsi::Function &jsFunction,
119
- std::string arg,
120
- bool isRejectCallback
168
+ std::string arg
121
169
  ) {
122
170
  std::optional<jsi::Value> extendedString = convertStringToFollyDynamicIfNeeded(
123
171
  rt,
@@ -145,8 +193,7 @@ void JavaCallback::invokeArray(jni::alias_ref<react::WritableNativeArray::javaob
145
193
  [](
146
194
  jsi::Runtime &rt,
147
195
  jsi::Function &jsFunction,
148
- folly::dynamic arg,
149
- bool isRejectCallback
196
+ folly::dynamic arg
150
197
  ) {
151
198
  jsi::Value convertedArg = jsi::valueFromDynamic(rt, arg);
152
199
  auto enhancedArg = decorateValueForDynamicExtension(rt, convertedArg);
@@ -169,28 +216,8 @@ void JavaCallback::invokeMap(jni::alias_ref<react::WritableNativeMap::javaobject
169
216
  [](
170
217
  jsi::Runtime &rt,
171
218
  jsi::Function &jsFunction,
172
- folly::dynamic arg,
173
- bool isRejectCallback
219
+ folly::dynamic arg
174
220
  ) {
175
- if (isRejectCallback) {
176
- auto errorCode = arg.find("code")->second.asString();
177
- auto message = arg.find("message")->second.asString();
178
-
179
- auto codedError = makeCodedError(
180
- rt,
181
- jsi::String::createFromUtf8(rt, errorCode),
182
- jsi::String::createFromUtf8(rt, message)
183
- );
184
-
185
- jsFunction.call(
186
- rt,
187
- (const jsi::Value *) &codedError,
188
- (size_t) 1
189
- );
190
-
191
- return;
192
- }
193
-
194
221
  jsi::Value convertedArg = jsi::valueFromDynamic(rt, arg);
195
222
  auto enhancedArg = decorateValueForDynamicExtension(rt, convertedArg);
196
223
  if (enhancedArg) {
@@ -212,8 +239,7 @@ void JavaCallback::invokeSharedRef(jni::alias_ref<SharedRef::javaobject> result)
212
239
  [](
213
240
  jsi::Runtime &rt,
214
241
  jsi::Function &jsFunction,
215
- jni::global_ref<SharedRef::javaobject> arg,
216
- bool isRejectCallback
242
+ jni::global_ref<SharedRef::javaobject> arg
217
243
  ) {
218
244
  const auto jsiContext = getJSIContext(rt);
219
245
  auto native = jni::make_local(arg);
@@ -248,4 +274,54 @@ void JavaCallback::invokeSharedRef(jni::alias_ref<SharedRef::javaobject> result)
248
274
  jni::make_global(result)
249
275
  );
250
276
  }
277
+
278
+ void JavaCallback::invokeError(jni::alias_ref<jstring> code, jni::alias_ref<jstring> errorMessage) {
279
+ const auto strongCallbackContext = this->callbackContext.lock();
280
+ // The context were deallocated before the callback was invoked.
281
+ if (strongCallbackContext == nullptr) {
282
+ return;
283
+ }
284
+
285
+ const auto jsInvoker = strongCallbackContext->jsCallInvokerHolder.lock();
286
+ // Call invoker is already released, so we cannot invoke the callback.
287
+ if (jsInvoker == nullptr) {
288
+ return;
289
+ }
290
+
291
+ jsInvoker->invokeAsync(
292
+ [
293
+ context = callbackContext,
294
+ code = code->toStdString(),
295
+ errorMessage = errorMessage->toStdString()
296
+ ]() -> void {
297
+ auto strongContext = context.lock();
298
+ // The context were deallocated before the callback was invoked.
299
+ if (strongContext == nullptr) {
300
+ return;
301
+ }
302
+
303
+ if (!strongContext->rejectHolder.has_value()) {
304
+ throw std::runtime_error(
305
+ "JavaCallback was already settled. Cannot invoke it again"
306
+ );
307
+ }
308
+
309
+ jsi::Function &jsFunction = strongContext->rejectHolder.value();
310
+ jsi::Runtime &rt = strongContext->rt;
311
+
312
+ auto codedError = makeCodedError(
313
+ rt,
314
+ jsi::String::createFromUtf8(rt, code),
315
+ jsi::String::createFromUtf8(rt, errorMessage)
316
+ );
317
+
318
+ jsFunction.call(
319
+ rt,
320
+ (const jsi::Value *) &codedError,
321
+ (size_t) 1
322
+ );
323
+
324
+ strongContext->invalidate();
325
+ });
326
+ }
251
327
  } // namespace expo
@@ -13,6 +13,7 @@
13
13
  #include <react/jni/WritableNativeMap.h>
14
14
  #include <fbjni/detail/CoreClasses.h>
15
15
  #include <ReactCommon/CallInvoker.h>
16
+ #include <ReactCommon/LongLivedObject.h>
16
17
 
17
18
  namespace jni = facebook::jni;
18
19
  namespace react = facebook::react;
@@ -26,19 +27,27 @@ struct SharedRef : public jni::JavaClass<SharedRef> {
26
27
 
27
28
  class JSIContext;
28
29
 
29
- typedef std::variant<folly::dynamic, jni::global_ref<SharedRef::javaobject>> CallbackArg;
30
-
31
30
  class JavaCallback : public jni::HybridClass<JavaCallback, Destructible> {
32
31
  public:
33
32
  static auto constexpr
34
33
  kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaCallback;";
35
34
  static auto constexpr TAG = "JavaCallback";
36
35
 
37
- struct CallbackContext {
36
+ class CallbackContext : public react::LongLivedObject {
37
+ public:
38
+ CallbackContext(
39
+ jsi::Runtime &rt,
40
+ std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
41
+ std::optional<jsi::Function> resolveHolder,
42
+ std::optional<jsi::Function> rejectHolder
43
+ );
44
+
38
45
  jsi::Runtime &rt;
39
- std::shared_ptr<react::CallInvoker> jsCallInvokerHolder;
40
- std::optional<jsi::Function> jsFunctionHolder;
41
- bool isRejectCallback;
46
+ std::weak_ptr<react::CallInvoker> jsCallInvokerHolder;
47
+ std::optional<jsi::Function> resolveHolder;
48
+ std::optional<jsi::Function> rejectHolder;
49
+
50
+ void invalidate();
42
51
  };
43
52
 
44
53
  static void registerNatives();
@@ -49,7 +58,7 @@ public:
49
58
  );
50
59
 
51
60
  private:
52
- std::shared_ptr<CallbackContext> callbackContext;
61
+ std::weak_ptr<CallbackContext> callbackContext;
53
62
 
54
63
  friend HybridBase;
55
64
 
@@ -73,13 +82,10 @@ private:
73
82
 
74
83
  void invokeSharedRef(jni::alias_ref<SharedRef::javaobject> result);
75
84
 
85
+ void invokeError(jni::alias_ref<jstring> code, jni::alias_ref<jstring> errorMessage);
86
+
76
87
  template<class T>
77
- using ArgsConverter = std::function<void(
78
- jsi::Runtime &rt,
79
- jsi::Function &jsFunction,
80
- T arg,
81
- bool isRejectCallback
82
- )>;
88
+ using ArgsConverter = std::function<void(jsi::Runtime &rt, jsi::Function &jsFunction, T arg)>;
83
89
 
84
90
  template<class T>
85
91
  inline void invokeJSFunction(
@@ -36,7 +36,7 @@ void JavaReferencesCache::loadJClasses(JNIEnv *env) {
36
36
  });
37
37
 
38
38
  loadJClass(env, "expo/modules/kotlin/jni/PromiseImpl", {
39
- {"<init>", "(Lexpo/modules/kotlin/jni/JavaCallback;Lexpo/modules/kotlin/jni/JavaCallback;)V"}
39
+ {"<init>", "(Lexpo/modules/kotlin/jni/JavaCallback;)V"}
40
40
  });
41
41
 
42
42
  loadJClass(env, "java/lang/Object", {});
@@ -7,13 +7,14 @@
7
7
  #include "Exceptions.h"
8
8
  #include "JavaCallback.h"
9
9
  #include "types/JNIToJSIConverter.h"
10
+ #include "JSReferencesCache.h"
10
11
 
11
12
  #include <utility>
12
13
  #include <functional>
13
14
  #include <unistd.h>
14
15
  #include <optional>
15
16
 
16
- #include "JSReferencesCache.h"
17
+ #include <ReactCommon/LongLivedObject.h>
17
18
 
18
19
  namespace jni = facebook::jni;
19
20
  namespace jsi = facebook::jsi;
@@ -21,10 +22,10 @@ namespace react = facebook::react;
21
22
 
22
23
  namespace expo {
23
24
 
24
- jni::local_ref<JavaCallback::JavaPart> createJavaCallbackFromJSIFunction(
25
- jsi::Function &&function,
26
- jsi::Runtime &rt,
27
- bool isRejectCallback = false
25
+ jni::local_ref<JavaCallback::JavaPart> createJavaCallback(
26
+ jsi::Function &&resolveFunction,
27
+ jsi::Function &&rejectFunction,
28
+ jsi::Runtime &rt
28
29
  ) {
29
30
  JSIContext *jsiContext = getJSIContext(rt);
30
31
  std::shared_ptr<react::CallInvoker> jsInvoker = jsiContext->runtimeHolder->jsInvoker;
@@ -32,10 +33,16 @@ jni::local_ref<JavaCallback::JavaPart> createJavaCallbackFromJSIFunction(
32
33
  std::shared_ptr<JavaCallback::CallbackContext> callbackContext = std::make_shared<JavaCallback::CallbackContext>(
33
34
  rt,
34
35
  std::move(jsInvoker),
35
- std::move(function),
36
- isRejectCallback
36
+ std::move(resolveFunction),
37
+ std::move(rejectFunction)
37
38
  );
38
39
 
40
+ #if REACT_NATIVE_TARGET_VERSION >= 75
41
+ facebook::react::LongLivedObjectCollection::get(rt).add(callbackContext);
42
+ #else
43
+ facebook::react::LongLivedObjectCollection::get().add(callbackContext);
44
+ #endif
45
+
39
46
  return JavaCallback::newInstance(jsiContext, std::move(callbackContext));
40
47
  }
41
48
 
@@ -336,15 +343,10 @@ jsi::Function MethodMetadata::createPromiseBody(
336
343
  jsi::Function resolveJSIFn = promiseConstructorArgs[0].getObject(rt).getFunction(rt);
337
344
  jsi::Function rejectJSIFn = promiseConstructorArgs[1].getObject(rt).getFunction(rt);
338
345
 
339
- jobject resolve = createJavaCallbackFromJSIFunction(
346
+ jobject javaCallback = createJavaCallback(
340
347
  std::move(resolveJSIFn),
341
- rt
342
- ).release();
343
-
344
- jobject reject = createJavaCallbackFromJSIFunction(
345
348
  std::move(rejectJSIFn),
346
- rt,
347
- true
349
+ rt
348
350
  ).release();
349
351
 
350
352
  JNIEnv *env = jni::Environment::current();
@@ -353,15 +355,14 @@ jsi::Function MethodMetadata::createPromiseBody(
353
355
  "expo/modules/kotlin/jni/PromiseImpl");
354
356
  jmethodID jPromiseConstructor = jPromise.getMethod(
355
357
  "<init>",
356
- "(Lexpo/modules/kotlin/jni/JavaCallback;Lexpo/modules/kotlin/jni/JavaCallback;)V"
358
+ "(Lexpo/modules/kotlin/jni/JavaCallback;)V"
357
359
  );
358
360
 
359
361
  // Creates a promise object
360
362
  jobject promise = env->NewObject(
361
363
  jPromise.clazz,
362
364
  jPromiseConstructor,
363
- resolve,
364
- reject
365
+ javaCallback
365
366
  );
366
367
 
367
368
  // Cast in this place is safe, cause we know that this function expects promise.
@@ -33,7 +33,7 @@ interface Promise {
33
33
  fun Promise.toBridgePromise(): com.facebook.react.bridge.Promise {
34
34
  val expoPromise = this
35
35
  val resolveMethod: (value: Any?) -> Unit = if (expoPromise is PromiseImpl) {
36
- expoPromise.resolveBlock::invoke
36
+ expoPromise.callback::invoke
37
37
  } else {
38
38
  expoPromise::resolve
39
39
  }
@@ -6,6 +6,11 @@ import android.util.Log
6
6
  import okhttp3.Interceptor
7
7
  import okhttp3.Request
8
8
  import okhttp3.Response
9
+ import okhttp3.ResponseBody
10
+ import okhttp3.ResponseBody.Companion.asResponseBody
11
+ import okio.GzipSource
12
+ import okio.buffer
13
+ import java.io.IOException
9
14
 
10
15
  private const val TAG = "ExpoNetworkInspector"
11
16
 
@@ -31,7 +36,9 @@ class ExpoNetworkInspectOkHttpNetworkInterceptor : Interceptor {
31
36
  it.priorResponse = response
32
37
  }
33
38
  } else {
34
- delegate.didReceiveResponse(requestId, request, response)
39
+ val body = peekResponseBody(response)
40
+ delegate.didReceiveResponse(requestId, request, response, body)
41
+ body?.close()
35
42
  }
36
43
  } catch (e: Exception) {
37
44
  Log.e(TAG, "Failed to send network request CDP event", e)
@@ -62,9 +69,18 @@ class ExpoNetworkInspectOkHttpAppInterceptor : Interceptor {
62
69
  * The delegate to dispatch network request events
63
70
  */
64
71
  internal interface ExpoNetworkInspectOkHttpInterceptorsDelegate {
65
- fun willSendRequest(requestId: String, request: Request, redirectResponse: Response?)
72
+ fun willSendRequest(
73
+ requestId: String,
74
+ request: Request,
75
+ redirectResponse: Response?
76
+ )
66
77
 
67
- fun didReceiveResponse(requestId: String, request: Request, response: Response)
78
+ fun didReceiveResponse(
79
+ requestId: String,
80
+ request: Request,
81
+ response: Response,
82
+ body: ResponseBody?
83
+ )
68
84
  }
69
85
 
70
86
  /**
@@ -74,3 +90,36 @@ internal class RedirectResponse {
74
90
  var requestId: String? = null
75
91
  var priorResponse: Response? = null
76
92
  }
93
+
94
+ /**
95
+ * Peek response body that could send to CDP delegate.
96
+ * Also uncompress gzip payload if necessary since OkHttp [Interceptor] does not uncompress payload for you.
97
+ * @return null if the response body exceeds [byteCount]
98
+ */
99
+ internal fun peekResponseBody(
100
+ response: Response,
101
+ byteCount: Long = ExpoNetworkInspectOkHttpNetworkInterceptor.MAX_BODY_SIZE
102
+ ): ResponseBody? {
103
+ val body = response.body ?: return null
104
+ val peeked = body.source().peek()
105
+ try {
106
+ if (peeked.request(byteCount + 1)) {
107
+ // When the request() returns true,
108
+ // it means the source have more available bytes then [byteCount].
109
+ return null
110
+ }
111
+ } catch (_: IOException) {}
112
+
113
+ val encoding = response.header("Content-Encoding")
114
+ val source = when {
115
+ encoding.equals("gzip", ignoreCase = true) ->
116
+ GzipSource(peeked).buffer().apply {
117
+ request(byteCount)
118
+ }
119
+ else -> peeked
120
+ }
121
+
122
+ val buffer = okio.Buffer()
123
+ buffer.write(source, minOf(byteCount, source.buffer.size))
124
+ return buffer.asResponseBody(body.contentType(), buffer.size)
125
+ }
@@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
13
13
  import kotlinx.coroutines.launch
14
14
  import okhttp3.Request
15
15
  import okhttp3.Response
16
+ import okhttp3.ResponseBody
16
17
  import java.math.BigDecimal
17
18
  import java.math.RoundingMode
18
19
 
@@ -38,7 +39,11 @@ object ExpoRequestCdpInterceptor : ExpoNetworkInspectOkHttpInterceptorsDelegate
38
39
 
39
40
  //region ExpoNetworkInspectOkHttpInterceptorsDelegate implementations
40
41
 
41
- override fun willSendRequest(requestId: String, request: Request, redirectResponse: Response?) {
42
+ override fun willSendRequest(
43
+ requestId: String,
44
+ request: Request,
45
+ redirectResponse: Response?
46
+ ) {
42
47
  val now = BigDecimal(System.currentTimeMillis() / 1000.0).setScale(3, RoundingMode.CEILING)
43
48
 
44
49
  val params = RequestWillBeSentParams(now, requestId, request, redirectResponse)
@@ -48,14 +53,19 @@ object ExpoRequestCdpInterceptor : ExpoNetworkInspectOkHttpInterceptorsDelegate
48
53
  dispatchEvent(Event("Network.requestWillBeSentExtraInfo", params2))
49
54
  }
50
55
 
51
- override fun didReceiveResponse(requestId: String, request: Request, response: Response) {
56
+ override fun didReceiveResponse(
57
+ requestId: String,
58
+ request: Request,
59
+ response: Response,
60
+ body: ResponseBody?
61
+ ) {
52
62
  val now = BigDecimal(System.currentTimeMillis() / 1000.0).setScale(3, RoundingMode.CEILING)
53
63
 
54
64
  val params = ResponseReceivedParams(now, requestId, request, response)
55
65
  dispatchEvent(Event("Network.responseReceived", params))
56
66
 
57
- if (response.peekBody(ExpoNetworkInspectOkHttpNetworkInterceptor.MAX_BODY_SIZE + 1).contentLength() <= ExpoNetworkInspectOkHttpNetworkInterceptor.MAX_BODY_SIZE) {
58
- val params2 = ExpoReceivedResponseBodyParams(now, requestId, request, response)
67
+ if (body != null) {
68
+ val params2 = ExpoReceivedResponseBodyParams(now, requestId, request, response, body)
59
69
  dispatchEvent(Event("Expo(Network.receivedResponseBody)", params2))
60
70
  }
61
71
 
@@ -227,15 +227,20 @@ data class ExpoReceivedResponseBodyParams(
227
227
  var body: String,
228
228
  var base64Encoded: Boolean
229
229
  ) : JsonSerializable {
230
- constructor(now: BigDecimal, requestId: RequestId, request: okhttp3.Request, response: okhttp3.Response) : this(
230
+ constructor(
231
+ now: BigDecimal,
232
+ requestId: RequestId,
233
+ request: okhttp3.Request,
234
+ response: okhttp3.Response,
235
+ body: okhttp3.ResponseBody
236
+ ) : this(
231
237
  requestId = requestId,
232
238
  body = "",
233
239
  base64Encoded = false
234
240
  ) {
235
- val rawBody = response.peekBody(ExpoNetworkInspectOkHttpNetworkInterceptor.MAX_BODY_SIZE)
236
- val contentType = rawBody.contentType()
241
+ val contentType = body.contentType()
237
242
  val isText = contentType?.type == "text" || (contentType?.type == "application" && contentType.subtype == "json")
238
- val bodyString = if (isText) rawBody.string() else rawBody.source().readByteString().base64()
243
+ val bodyString = if (isText) body.string() else body.source().readByteString().base64()
239
244
 
240
245
  this.body = bodyString
241
246
  this.base64Encoded = !isText
@@ -49,6 +49,10 @@ class JavaCallback @DoNotStrip internal constructor(@DoNotStrip private val mHyb
49
49
  invokeNative(result)
50
50
  }
51
51
 
52
+ operator fun invoke(code: String, errorMessage: String) = checkIfValid {
53
+ invokeNative(code, errorMessage)
54
+ }
55
+
52
56
  private external fun invokeNative()
53
57
  private external fun invokeNative(result: Int)
54
58
  private external fun invokeNative(result: Boolean)
@@ -58,6 +62,7 @@ class JavaCallback @DoNotStrip internal constructor(@DoNotStrip private val mHyb
58
62
  private external fun invokeNative(result: WritableNativeArray)
59
63
  private external fun invokeNative(result: WritableNativeMap)
60
64
  private external fun invokeNative(result: SharedRef<*>)
65
+ private external fun invokeNative(code: String, errorMessage: String)
61
66
 
62
67
  private inline fun checkIfValid(body: () -> Unit) {
63
68
  try {
@@ -1,8 +1,5 @@
1
1
  package expo.modules.kotlin.jni
2
2
 
3
- import com.facebook.react.bridge.WritableMap
4
- import com.facebook.react.bridge.WritableNativeArray
5
- import com.facebook.react.bridge.WritableNativeMap
6
3
  import expo.modules.BuildConfig
7
4
  import expo.modules.core.interfaces.DoNotStrip
8
5
  import expo.modules.kotlin.AppContext
@@ -30,8 +27,7 @@ private const val STACK_FRAME_KEY_METHOD_NAME = "methodName"
30
27
 
31
28
  @DoNotStrip
32
29
  class PromiseImpl @DoNotStrip internal constructor(
33
- @DoNotStrip internal val resolveBlock: JavaCallback,
34
- @DoNotStrip internal val rejectBlock: JavaCallback
30
+ @DoNotStrip internal val callback: JavaCallback
35
31
  ) : Promise {
36
32
  internal var wasSettled = false
37
33
  private set
@@ -39,80 +35,35 @@ class PromiseImpl @DoNotStrip internal constructor(
39
35
  private var fullFunctionName: String? = null
40
36
 
41
37
  override fun resolve(value: Any?) = checkIfWasSettled {
42
- resolveBlock(
38
+ callback.invoke(
43
39
  JSTypeConverter.convertToJSValue(value)
44
40
  )
45
41
  }
46
42
 
47
43
  override fun resolve(result: Int) = checkIfWasSettled {
48
- resolveBlock.invoke(result)
44
+ callback.invoke(result)
49
45
  }
50
46
 
51
47
  override fun resolve(result: Boolean) = checkIfWasSettled {
52
- resolveBlock.invoke(result)
48
+ callback.invoke(result)
53
49
  }
54
50
 
55
51
  override fun resolve(result: Double) = checkIfWasSettled {
56
- resolveBlock.invoke(result)
52
+ callback.invoke(result)
57
53
  }
58
54
 
59
55
  override fun resolve(result: Float) = checkIfWasSettled {
60
- resolveBlock.invoke(result)
56
+ callback.invoke(result)
61
57
  }
62
58
 
63
59
  override fun resolve(result: String) = checkIfWasSettled {
64
- resolveBlock.invoke(result)
60
+ callback.invoke(result)
65
61
  }
66
62
 
67
63
  // Copy of the reject method from [com.facebook.react.bridge.PromiseImpl]
68
64
  override fun reject(code: String, message: String?, cause: Throwable?) = checkIfWasSettled {
69
- val errorInfo = WritableNativeMap()
70
-
71
- errorInfo.putString(ERROR_MAP_KEY_CODE, code)
72
-
73
- // Use the custom message if provided otherwise use the throwable message.
74
- if (message != null) {
75
- errorInfo.putString(ERROR_MAP_KEY_MESSAGE, message)
76
- } else if (cause != null) {
77
- errorInfo.putString(ERROR_MAP_KEY_MESSAGE, cause.message)
78
- } else {
79
- // The JavaScript side expects a map with at least an error message.
80
- // /Libraries/BatchedBridge/NativeModules.js -> createErrorFromErrorData
81
- // TYPE: (errorData: { message: string })
82
- errorInfo.putString(ERROR_MAP_KEY_MESSAGE, ERROR_DEFAULT_MESSAGE)
83
- }
84
-
85
- // For consistency with iOS ensure userInfo key exists, even if we null it.
86
- // iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError
87
- errorInfo.putNull(ERROR_MAP_KEY_USER_INFO)
88
-
89
- // Attach a nativeStackAndroid array if a throwable was passed
90
- // this matches iOS behavior - iOS adds a `nativeStackIOS` property
91
- // iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError
92
- if (cause != null) {
93
- val stackTrace: Array<StackTraceElement> = cause.stackTrace
94
- val nativeStackAndroid = WritableNativeArray()
95
-
96
- // Build an an Array of StackFrames to match JavaScript:
97
- // iOS: /Libraries/Core/Devtools/parseErrorStack.js -> StackFrame
98
- var i = 0
99
- while (i < stackTrace.size && i < ERROR_STACK_FRAME_LIMIT) {
100
- val frame = stackTrace[i]
101
- val frameMap: WritableMap = WritableNativeMap()
102
- // NOTE: no column number exists StackTraceElement
103
- frameMap.putString(STACK_FRAME_KEY_CLASS, frame.className)
104
- frameMap.putString(STACK_FRAME_KEY_FILE, frame.fileName)
105
- frameMap.putInt(STACK_FRAME_KEY_LINE_NUMBER, frame.lineNumber)
106
- frameMap.putString(STACK_FRAME_KEY_METHOD_NAME, frame.methodName)
107
- nativeStackAndroid.pushMap(frameMap)
108
- i++
109
- }
110
- errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, nativeStackAndroid)
111
- } else {
112
- errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, WritableNativeArray())
113
- }
114
-
115
- rejectBlock.invoke(errorInfo)
65
+ // TODO(@lukmccall): Add information about the stack trace to the error message
66
+ callback.invoke(code, message ?: cause?.message ?: "unknown")
116
67
  }
117
68
 
118
69
  private inline fun checkIfWasSettled(body: () -> Unit) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "1.12.2",
3
+ "version": "1.12.4",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -44,5 +44,5 @@
44
44
  "@testing-library/react-hooks": "^7.0.1",
45
45
  "expo-module-scripts": "^3.0.0"
46
46
  },
47
- "gitHead": "ee4f30ef3b5fa567ad1bf94794197f7683fdd481"
47
+ "gitHead": "4a7cf0d0baf6dfc595d93f604945d2142e705a36"
48
48
  }