expo-modules-core 57.0.19 → 57.0.20

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,13 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.20 — 2026-09-29
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Fixed `Exception` reporting `undefined reason` to JavaScript instead of the description it was created with, which also affected every `promise.reject(code, description)` call. ([#50215](https://github.com/expo/expo/pull/50215) by [@Maher-Reven](https://github.com/Maher-Reven))
18
+ - [Android] Fixed async functions rejecting with `Cannot use shared object that was already released`, and `sharedObjectDidRelease` being called in the middle of a pending call, when the garbage collector took the JS object of a shared object receiver or argument before the call finished. ([#50513](https://github.com/expo/expo/pull/50513) by [@lukmccall](https://github.com/lukmccall))
19
+
13
20
  ## 57.0.19 — 2026-09-24
14
21
 
15
22
  ### 🐛 Bug fixes
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.19'
30
+ version = '57.0.20'
31
31
 
32
32
  def isExpoModulesCoreTests = {
33
33
  Gradle gradle = getGradle()
@@ -94,7 +94,7 @@ android {
94
94
  defaultConfig {
95
95
  consumerProguardFiles 'proguard-rules.pro'
96
96
  versionCode 1
97
- versionName "57.0.19"
97
+ versionName "57.0.20"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -12,16 +12,19 @@ JavaCallback::CallbackContext::CallbackContext(
12
12
  jsi::Runtime &rt,
13
13
  std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
14
14
  std::optional<jsi::Function> resolveHolder,
15
- std::optional<jsi::Function> rejectHolder
15
+ std::optional<jsi::Function> rejectHolder,
16
+ std::vector<jsi::Value> retainedValues
16
17
  ) : react::LongLivedObject(rt),
17
18
  rt(rt),
18
19
  jsCallInvokerHolder(std::move(jsCallInvokerHolder)),
19
20
  resolveHolder(std::move(resolveHolder)),
20
- rejectHolder(std::move(rejectHolder)) {}
21
+ rejectHolder(std::move(rejectHolder)),
22
+ retainedValues(std::move(retainedValues)) {}
21
23
 
22
24
  void JavaCallback::CallbackContext::invalidate() {
23
25
  resolveHolder.reset();
24
26
  rejectHolder.reset();
27
+ retainedValues.clear();
25
28
  allowRelease();
26
29
  }
27
30
 
@@ -32,13 +32,18 @@ public:
32
32
  jsi::Runtime &rt,
33
33
  std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
34
34
  std::optional<jsi::Function> resolveHolder,
35
- std::optional<jsi::Function> rejectHolder
35
+ std::optional<jsi::Function> rejectHolder,
36
+ std::vector<jsi::Value> retainedValues = {}
36
37
  );
37
38
 
38
39
  jsi::Runtime &rt;
39
40
  std::weak_ptr<react::CallInvoker> jsCallInvokerHolder;
40
41
  std::optional<jsi::Function> resolveHolder;
41
42
  std::optional<jsi::Function> rejectHolder;
43
+ /**
44
+ * JS values that have to stay alive until the promise is settled.
45
+ */
46
+ std::vector<jsi::Value> retainedValues;
42
47
 
43
48
  void invalidate();
44
49
  };
@@ -20,7 +20,8 @@ namespace expo {
20
20
  jni::local_ref<JavaCallback::JavaPart> createJavaCallback(
21
21
  jsi::Function &&resolveFunction,
22
22
  jsi::Function &&rejectFunction,
23
- jsi::Runtime &rt
23
+ jsi::Runtime &rt,
24
+ std::vector<jsi::Value> &&retainedValues
24
25
  ) {
25
26
  JSIContext *jsiContext = getJSIContext(rt);
26
27
  std::shared_ptr<react::CallInvoker> jsInvoker = jsiContext->runtimeHolder->jsInvoker;
@@ -29,7 +30,8 @@ jni::local_ref<JavaCallback::JavaPart> createJavaCallback(
29
30
  rt,
30
31
  std::move(jsInvoker),
31
32
  std::move(resolveFunction),
32
- std::move(rejectFunction)
33
+ std::move(rejectFunction),
34
+ std::move(retainedValues)
33
35
  );
34
36
 
35
37
  facebook::react::LongLivedObjectCollection::get(rt).add(callbackContext);
@@ -105,6 +107,11 @@ MethodMetadata::MethodMetadata(
105
107
  jni::global_ref<jobject> &&jBodyReference
106
108
  ) : info(std::move(info)),
107
109
  jBodyReference(std::move(jBodyReference)) {
110
+ for (size_t index = 0; index < this->info.argTypes.size(); index++) {
111
+ if (this->info.argTypes[index]->converter->retainsJSValue()) {
112
+ retainedArgIndices.push_back(index);
113
+ }
114
+ }
108
115
  }
109
116
 
110
117
  std::shared_ptr<jsi::Function> MethodMetadata::toJSFunction(
@@ -230,12 +237,18 @@ jsi::Function MethodMetadata::toAsyncFunction(
230
237
  auto globalConvertedArgs = (jobjectArray) env->NewGlobalRef(convertedArgs);
231
238
  env->DeleteLocalRef(convertedArgs);
232
239
 
240
+ std::shared_ptr<std::vector<jsi::Value>> retainedValues = nullptr;
241
+ if (!thisPtr->retainedArgIndices.empty()) {
242
+ retainedValues = std::make_shared<std::vector<jsi::Value>>(
243
+ thisPtr->retainArguments(rt, thisValue, args, count)
244
+ );
245
+ }
246
+
233
247
  // Creates a JSI promise
234
- jsi::Value promise = Promise.callAsConstructor(
248
+ return Promise.callAsConstructor(
235
249
  rt,
236
- thisPtr->createPromiseBody(rt, globalConvertedArgs)
250
+ thisPtr->createPromiseBody(rt, globalConvertedArgs, std::move(retainedValues))
237
251
  );
238
- return promise;
239
252
  } catch (jni::JniException &jniException) {
240
253
  jni::local_ref<jni::JThrowable> unboxedThrowable = jniException.getThrowable();
241
254
  if (!unboxedThrowable->isInstanceOf(CodedException::javaClassLocal())) {
@@ -246,7 +259,7 @@ jsi::Function MethodMetadata::toAsyncFunction(
246
259
  auto code = codedException->getCode();
247
260
  auto message = codedException->getLocalizedMessage().value_or("");
248
261
 
249
- jsi::Value promise = Promise.callAsConstructor(
262
+ return Promise.callAsConstructor(
250
263
  rt,
251
264
  jsi::Function::createFromHostFunction(
252
265
  rt,
@@ -275,22 +288,49 @@ jsi::Function MethodMetadata::toAsyncFunction(
275
288
  }
276
289
  )
277
290
  );
278
-
279
- return promise;
280
291
  }
281
292
  }
282
293
  );
283
294
  }
284
295
 
296
+ std::vector<jsi::Value> MethodMetadata::retainArguments(
297
+ jsi::Runtime &rt,
298
+ const jsi::Value &thisValue,
299
+ const jsi::Value *args,
300
+ size_t count
301
+ ) const {
302
+ std::vector<jsi::Value> retained;
303
+ retained.reserve(retainedArgIndices.size());
304
+
305
+ // With an owner, `info.argTypes[0]` describes the receiver and the JS arguments start at index 1.
306
+ const size_t argsOffset = info.takesOwner ? 1 : 0;
307
+ for (size_t index : retainedArgIndices) {
308
+ if (info.takesOwner && index == 0) {
309
+ if (thisValue.isObject()) {
310
+ retained.emplace_back(rt, thisValue);
311
+ }
312
+ continue;
313
+ }
314
+
315
+ const size_t argIndex = index - argsOffset;
316
+ if (argIndex < count && args[argIndex].isObject()) {
317
+ retained.emplace_back(rt, args[argIndex]);
318
+ }
319
+ }
320
+
321
+ return retained;
322
+ }
323
+
285
324
  jsi::Function MethodMetadata::createPromiseBody(
286
325
  jsi::Runtime &runtime,
287
- jobjectArray globalArgs
326
+ jobjectArray globalArgs,
327
+ std::shared_ptr<std::vector<jsi::Value>> retainedValues
288
328
  ) {
289
329
  return jsi::Function::createFromHostFunction(
290
330
  runtime,
291
331
  getJSIContext(runtime)->jsRegistry->getPropNameID(runtime, "promiseFn"),
292
332
  2,
293
- [this, globalArgs](
333
+ [this, globalArgs, retainedValues = std::move(retainedValues)](
294
334
  jsi::Runtime &rt,
295
335
  const jsi::Value &thisVal,
296
336
  const jsi::Value *promiseConstructorArgs,
@@ -303,10 +343,12 @@ jsi::Function MethodMetadata::createPromiseBody(
303
343
  jsi::Function resolveJSIFn = promiseConstructorArgs[0].getObject(rt).getFunction(rt);
304
344
  jsi::Function rejectJSIFn = promiseConstructorArgs[1].getObject(rt).getFunction(rt);
305
345
 
346
+ // The promise constructor calls this body synchronously, so the retained values are moved out once.
306
347
  jobject javaCallback = createJavaCallback(
307
348
  std::move(resolveJSIFn),
308
349
  std::move(rejectJSIFn),
309
- rt
350
+ rt,
351
+ retainedValues != nullptr ? std::move(*retainedValues) : std::vector<jsi::Value>()
310
352
  ).release();
311
353
 
312
354
  JNIEnv *env = jni::Environment::current();
@@ -107,9 +107,27 @@ private:
107
107
 
108
108
  jsi::Function createPromiseBody(
109
109
  jsi::Runtime &runtime,
110
- jobjectArray globalArgs
110
+ jobjectArray globalArgs,
111
+ std::shared_ptr<std::vector<jsi::Value>> retainedValues
111
112
  );
112
113
 
114
+ /**
115
+ * Indexes into `info.argTypes` of the arguments whose JS values have to stay alive until an async call
116
+ * is settled, computed once from the converters. When the receiver is taken, index 0 is the receiver.
117
+ */
118
+ std::vector<size_t> retainedArgIndices;
119
+
120
+ /**
121
+ * Copies the JS values of the arguments listed in `retainedArgIndices`.
122
+ * They are kept alive until the returned promise is settled.
123
+ */
124
+ std::vector<jsi::Value> retainArguments(
125
+ jsi::Runtime &rt,
126
+ const jsi::Value &thisValue,
127
+ const jsi::Value *args,
128
+ size_t count
129
+ ) const;
130
+
113
131
  jobjectArray convertJSIArgsToJNI(
114
132
  JNIEnv *env,
115
133
  jsi::Runtime &rt,
@@ -9,6 +9,7 @@
9
9
  #if USE_HERMES
10
10
 
11
11
  #include <hermes/hermes.h>
12
+ #include <jsi/instrumentation.h>
12
13
 
13
14
  #include <utility>
14
15
 
@@ -69,6 +70,25 @@ jlong RuntimeHolder::createRuntime() {
69
70
  runtime = facebook::jsc::makeJSCRuntime();
70
71
  #endif
71
72
 
73
+ // Exposes a `gc()` function, so tests can force a full garbage collection.
74
+ auto gcPropName = jsi::PropNameID::forUtf8(*runtime, "gc");
75
+ runtime->global().setProperty(
76
+ *runtime,
77
+ gcPropName,
78
+ jsi::Function::createFromHostFunction(
79
+ *runtime,
80
+ gcPropName,
81
+ 0,
82
+ [](jsi::Runtime &rt,
83
+ const jsi::Value &thisVal,
84
+ const jsi::Value *args,
85
+ size_t count) {
86
+ rt.instrumentation().collectGarbage("test");
87
+ return jsi::Value::undefined();
88
+ }
89
+ )
90
+ );
91
+
72
92
  // By default "global" property isn't set.
73
93
  runtime->global().setProperty(
74
94
  *runtime,
@@ -2,6 +2,8 @@
2
2
 
3
3
  #pragma once
4
4
 
5
+ #include <algorithm>
6
+
5
7
  #include "../ExpoHeader.pch"
6
8
  #include "CppType.h"
7
9
 
@@ -41,6 +43,16 @@ public:
41
43
  JNIEnv *env,
42
44
  const jsi::Value &value
43
45
  ) const = 0;
46
+
47
+ /**
48
+ * Whether the JS value has to stay alive until an async call that received it is settled.
49
+ * It's true for converters that pass only a reference to Kotlin, like the shared object id.
50
+ * The native body works with the object behind that reference, so the JS value must not be
51
+ * garbage collected before the call ends. Composite converters ask their element converters.
52
+ */
53
+ virtual bool retainsJSValue() const {
54
+ return false;
55
+ }
44
56
  };
45
57
 
46
58
  /**
@@ -276,6 +288,10 @@ public:
276
288
  ) const override;
277
289
 
278
290
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
291
+
292
+ bool retainsJSValue() const override {
293
+ return true;
294
+ }
279
295
  };
280
296
 
281
297
  /**
@@ -312,6 +328,12 @@ public:
312
328
  const jsi::Value &value
313
329
  ) const override;
314
330
 
331
+ bool retainsJSValue() const override {
332
+ return std::any_of(converters.begin(), converters.end(), [](const auto &converter) {
333
+ return converter->retainsJSValue();
334
+ });
335
+ }
336
+
315
337
  private:
316
338
  std::vector<std::shared_ptr<FrontendConverter>> converters;
317
339
  };
@@ -365,6 +387,10 @@ public:
365
387
 
366
388
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
367
389
 
390
+ bool retainsJSValue() const override {
391
+ return parameterConverter->retainsJSValue();
392
+ }
393
+
368
394
  private:
369
395
  /**
370
396
  * A string representation of desired Java type.
@@ -396,6 +422,10 @@ public:
396
422
  ) const override;
397
423
 
398
424
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
425
+
426
+ bool retainsJSValue() const override {
427
+ return parameterConverter->retainsJSValue();
428
+ }
399
429
  private:
400
430
  /**
401
431
  * Converter used to convert array elements.
@@ -425,6 +455,10 @@ public:
425
455
  ) const override;
426
456
 
427
457
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
458
+
459
+ bool retainsJSValue() const override {
460
+ return valueConverter->retainsJSValue();
461
+ }
428
462
  private:
429
463
  /**
430
464
  * Converter used to convert values.
@@ -464,6 +498,10 @@ public:
464
498
  ) const override;
465
499
 
466
500
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
501
+
502
+ bool retainsJSValue() const override {
503
+ return parameterConverter->retainsJSValue();
504
+ }
467
505
  private:
468
506
  std::shared_ptr<FrontendConverter> parameterConverter;
469
507
  };
@@ -481,6 +519,10 @@ public:
481
519
  ) const override;
482
520
 
483
521
  bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
522
+
523
+ bool retainsJSValue() const override {
524
+ return parameterConverter->retainsJSValue();
525
+ }
484
526
  private:
485
527
  std::shared_ptr<FrontendConverter> parameterConverter;
486
528
  };
@@ -58,6 +58,20 @@ class JNIDeallocator(shouldCreateDestructorThread: Boolean = true) : AutoCloseab
58
58
  destructorThread?.interrupt()
59
59
  }
60
60
 
61
+ /**
62
+ * Deallocates the hybrid objects whose Java wrappers were already garbage collected,
63
+ * without waiting for the destructor thread. Returns how many objects were deallocated.
64
+ */
65
+ internal fun processPendingReferences(): Int = synchronized(this) {
66
+ var processed = 0
67
+ while (true) {
68
+ val current = referenceQueue.poll() ?: break
69
+ destructorMap.remove(current)?.resetNative()
70
+ processed++
71
+ }
72
+ processed
73
+ }
74
+
61
75
  /**
62
76
  * Returns references to all hybrid objects that contain references to the jsi value
63
77
  * and are present in the memory.
@@ -7,7 +7,7 @@ open class Exception: CodedError, ChainableException, CustomStringConvertible, C
7
7
  String describing the reason of the exception.
8
8
  */
9
9
  open var reason: String {
10
- "undefined reason"
10
+ customReason ?? "undefined reason"
11
11
  }
12
12
 
13
13
  /**
@@ -20,6 +20,12 @@ open class Exception: CodedError, ChainableException, CustomStringConvertible, C
20
20
  */
21
21
  let customCode: String?
22
22
 
23
+ /**
24
+ A custom reason of the exception, given as the `description` to the initializer that takes a name.
25
+ When unset, the `reason` is expected to come from an override in a subclass.
26
+ */
27
+ let customReason: String?
28
+
23
29
  /**
24
30
  The default initializer that captures the place in the code where the exception was created.
25
31
  - Warning: Call it only without arguments!
@@ -27,13 +33,14 @@ open class Exception: CodedError, ChainableException, CustomStringConvertible, C
27
33
  public init(file: String = #fileID, line: UInt = #line, function: String = #function) {
28
34
  self.origin = ExceptionOrigin(file: file, line: line, function: function)
29
35
  self.customCode = nil
36
+ self.customReason = nil
30
37
  }
31
38
 
32
39
  public init(name: String, description: String, code: String? = nil, file: String = #fileID, line: UInt = #line, function: String = #function) {
33
40
  self.origin = ExceptionOrigin(file: file, line: line, function: function)
34
41
  self.customCode = code
42
+ self.customReason = description
35
43
  self.name = name
36
- self.description = description
37
44
  }
38
45
 
39
46
  // MARK: - CodedError
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "57.0.19",
3
+ "version": "57.0.20",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  "@types/invariant": "^2.2.33",
67
67
  "expo-module-scripts": "56.0.3"
68
68
  },
69
- "gitHead": "9349d1457178dd9cccb8bdd3aed47473c49134e1",
69
+ "gitHead": "ad1efa3fa5802156660198ba5d787f91014d8df3",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",