expo-modules-jsi 57.0.5 → 57.0.7

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
+ ## 57.0.7 — 2026-09-01
14
+
15
+ ### 💡 Others
16
+
17
+ - [iOS] `JavaScriptValue.undefined` and `JavaScriptValue.null` now return shared immortal instances instead of allocating a new value on each access, removing one allocation from every void-returning host call. ([#49545](https://github.com/expo/expo/pull/49545) by [@tsapeta](https://github.com/tsapeta))
18
+ - [iOS] `CppError::tryCatch` now takes a C++ callable instead of an Objective-C block. Every caller already passes a pure C++ body, so the block bridged no Swift closure and only added a non-inlinable indirect call and an Objective-C runtime dependency on the JS call/eval error-handling path. ([#48333](https://github.com/expo/expo/pull/48333) by [@tsapeta](https://github.com/tsapeta))
19
+
20
+ ## 57.0.6 — 2026-08-26
21
+
22
+ ### 💡 Others
23
+
24
+ - [iOS] `JavaScriptActor.assumeIsolated` no longer heap-allocates a closure box per call by keeping its `operation` non-escaping, making synchronous host calls ~1.6× faster. ([#47837](https://github.com/expo/expo/pull/47837) by [@tsapeta](https://github.com/tsapeta))
25
+
13
26
  ## 57.0.5 — 2026-08-20
14
27
 
15
28
  ### 🐛 Bug fixes
@@ -1,8 +1,5 @@
1
1
  import Foundation
2
2
 
3
- /// Name of the JavaScript thread created by React Native. Copied from `RCTJSThreadManager.mm`.
4
- private let jsThreadName = "com.facebook.react.runtime.JavaScript"
5
-
6
3
  /// Global actor that is used to isolate the code that should only be executed from the JavaScript thread.
7
4
  /// Theoretically it does not act as a real actor; it uses a serial executor that executes jobs **synchronously**
8
5
  /// without hopping to the proper thread. Meaning that running these jobs on the JavaScript thread must be ensured
@@ -23,25 +20,56 @@ public actor JavaScriptActor: GlobalActor {
23
20
  /// An equivalent of `MainActor.assumeIsolated`, but for the `JavaScriptActor`. Assumes that the currently executing
24
21
  /// synchronous function is actually executing on the JavaScript thread and invokes an isolated version of the operation,
25
22
  /// allowing synchronous access to JavaScript runtime state without hopping through asynchronous boundaries.
26
- ///
27
- /// See https://stackoverflow.com/a/79346971 for more information about the implementation.
28
- public static func assumeIsolated<T: ~Copyable>(_ operation: @JavaScriptActor () throws -> T) rethrows -> T {
29
- typealias YesActor = @JavaScriptActor () throws -> T
30
- typealias NoActor = () throws -> T
23
+ /// The nonthrowing overload keeps `operation` nonescaping so its captured context can remain stack-allocated.
24
+ @_alwaysEmitIntoClient
25
+ @inline(__always)
26
+ public static func assumeIsolated<T: ~Copyable>(_ operation: @JavaScriptActor () -> T) -> T {
27
+ typealias IsolatedRunner = @JavaScriptActor (@JavaScriptActor () -> T) -> T
28
+ typealias NonisolatedRunner = (@JavaScriptActor () -> T) -> T
31
29
 
32
30
  // This will crash if the current context cannot be isolated.
33
- shared.executor.checkIsolated()
31
+ checkIsolated()
32
+
33
+ // Cast the capture-free runner rather than `operation` itself. `operation` remains nonescaping,
34
+ // so its captures can stay in the caller's stack frame.
35
+ let runner = unsafeBitCast(runIsolated as IsolatedRunner, to: NonisolatedRunner.self)
36
+ return runner(operation)
37
+ }
34
38
 
35
- // To do the unsafe cast, we have to pretend it's @escaping.
36
- return try withoutActuallyEscaping(operation) { (_ fn: @escaping YesActor) throws -> T in
37
- let rawFn = unsafeBitCast(fn, to: NoActor.self)
38
- return try rawFn()
39
+ /// Throwing counterpart to the nonthrowing overload above. The generic error type keeps
40
+ /// `operation` nonescaping while preserving the exact error it can throw.
41
+ @_alwaysEmitIntoClient
42
+ @inline(__always)
43
+ public static func assumeIsolated<T: ~Copyable, E: Error>(
44
+ _ operation: @JavaScriptActor () throws(E) -> T
45
+ ) throws(E) -> T {
46
+ // Casting a `throws(E)` function value requires newer OS runtime support. Wrapping the
47
+ // operation in the nonthrowing fast path keeps this backdeployable and stack-allocated.
48
+ let result: Result<T, E> = assumeIsolated {
49
+ return Result(catching: operation)
39
50
  }
51
+ return try result.get()
40
52
  }
41
53
 
42
- /// Stops program execution if the actor's executor is not isolating the current context.
54
+ /// In debug builds, asserts if the actor's executor is not isolating the current context.
55
+ @inlinable
56
+ @inline(__always)
43
57
  public static func checkIsolated() {
44
- shared.executor.checkIsolated()
58
+ // Using `assert` instead of `precondition` because this check is a heuristic based on
59
+ // thread name, not a precise isolation guarantee. Worklet runtimes legitimately run on
60
+ // the UI thread, which would cause a false-positive crash with `precondition`.
61
+ assert(
62
+ // JavaScript thread name copied from `RCTJSThreadManager.mm`.
63
+ Thread.current.name == "com.facebook.react.runtime.JavaScript" || !Thread.isMultiThreaded()
64
+ || ProcessInfo.processInfo.processName == "xctest",
65
+ "JavaScriptActor operations must be run on the JavaScript thread"
66
+ )
67
+ }
68
+
69
+ @JavaScriptActor
70
+ @usableFromInline
71
+ internal static func runIsolated<T: ~Copyable>(_ operation: @JavaScriptActor () -> T) -> T {
72
+ return operation()
45
73
  }
46
74
  }
47
75
 
@@ -57,20 +85,11 @@ internal class JavaScriptExecutor: SerialExecutor, @unchecked Sendable {
57
85
  return UnownedSerialExecutor(ordinary: self)
58
86
  }
59
87
 
60
- /// Stops program execution if the executor is not isolating the current context.
88
+ /// Runtime hook used by Swift's actor data-race checks for synchronous entry into
89
+ /// `@JavaScriptActor` code. Do not remove: the `SerialExecutor` default always fails
90
+ /// when there is no active Swift task carrying this executor.
61
91
  func checkIsolated() {
62
- // Using `assert` instead of `precondition` because this check is a heuristic based on
63
- // thread name, not a precise isolation guarantee. Worklet runtimes legitimately run on
64
- // the UI thread, which would cause a false-positive crash with `precondition`.
65
- assert(isIsolatingCurrentContext() == true, "JavaScriptActor operations must be run on the JavaScript thread")
66
- }
67
-
68
- /// Checks whether the executor isolates the current context, i.e. the current thread is a JavaScript thread.
69
- /// The condition is also met when running tests and in single threaded environments.
70
- func isIsolatingCurrentContext() -> Bool? {
71
- // We must be careful as it relies on the thread name given by React Native.
72
- return Thread.current.name == jsThreadName || !Thread.isMultiThreaded()
73
- || ProcessInfo.processInfo.processName == "xctest"
92
+ JavaScriptActor.checkIsolated()
74
93
  }
75
94
  }
76
95
 
@@ -600,18 +600,25 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
600
600
 
601
601
  // MARK: - Runtime-free initializers
602
602
 
603
+ // Shared immortal instances for the runtime-free `undefined`/`null` kinds. The class is fully
604
+ // immutable and these carry no runtime and a trivial `jsi::Value`, so one instance can be safely
605
+ // handed out from any isolation context. Every void-returning `@JS` function returns `.undefined`,
606
+ // so a computed getter here would allocate on every host call.
607
+ private static let sharedUndefined = JavaScriptValue(nil, facebook.jsi.Value.undefined())
608
+ private static let sharedNull = JavaScriptValue(nil, facebook.jsi.Value.null())
609
+
603
610
  /// This is a lightweight way to create an undefined value that can be used in contexts
604
611
  /// where a runtime is not available or needed. The resulting value can be passed to
605
612
  /// JavaScript functions or used in comparisons.
606
613
  public static var undefined: JavaScriptValue {
607
- JavaScriptValue(nil, facebook.jsi.Value.undefined())
614
+ return sharedUndefined
608
615
  }
609
616
 
610
617
  /// This is a lightweight way to create a null value that can be used in contexts
611
618
  /// where a runtime is not available or needed. The resulting value represents
612
619
  /// JavaScript's `null`, which is distinct from `undefined`.
613
620
  public static var null: JavaScriptValue {
614
- JavaScriptValue(nil, facebook.jsi.Value.null())
621
+ return sharedNull
615
622
  }
616
623
 
617
624
  /// This is a lightweight way to create a boolean true value that can be used in contexts
@@ -68,12 +68,18 @@ public:
68
68
  }
69
69
 
70
70
  /**
71
- Executes a block of code and catches any C++ exceptions that are thrown.
71
+ Executes a C++ callable and catches any C++ exceptions that are thrown.
72
72
  Caught exceptions are stored in thread-local storage and can be retrieved
73
73
  using `getCurrent()`. The function returns `nullptr` when an exception occurs.
74
+
75
+ Takes a forwarding reference to a C++ callable (e.g. a lambda) rather than an
76
+ Objective-C block. Every caller passes a pure C++ body, so no Swift closure
77
+ crosses this boundary; the block only added a non-inlinable indirect call and
78
+ a dependency on the Objective-C runtime. This helper is C++-only and is not
79
+ imported into Swift.
74
80
  */
75
- template <typename Result>
76
- inline static Result tryCatch(jsi::IRuntime &runtime, Result(^block)(void)) {
81
+ template <typename Fn>
82
+ inline static auto tryCatch(jsi::IRuntime &runtime, Fn &&block) -> decltype(block()) {
77
83
  try {
78
84
  return block();
79
85
  } catch (jsi::JSError e) {
@@ -136,25 +136,25 @@ inline void destroyRuntime(jsi::Runtime &runtime) {
136
136
  }
137
137
 
138
138
  inline jsi::Value evaluateJavaScript(jsi::IRuntime &runtime, const std::shared_ptr<const jsi::Buffer>& buffer, const std::string& sourceURL) {
139
- return expo::CppError::tryCatch(runtime, ^{
139
+ return expo::CppError::tryCatch(runtime, [&] {
140
140
  return runtime.evaluateJavaScript(buffer, sourceURL);
141
141
  });
142
142
  }
143
143
 
144
144
  inline jsi::Value callFunction(jsi::IRuntime &runtime, const jsi::Function &function, const jsi::Value *_Nullable args, size_t count) {
145
- return expo::CppError::tryCatch(runtime, ^{
145
+ return expo::CppError::tryCatch(runtime, [&] {
146
146
  return function.call(runtime, args, count);
147
147
  });
148
148
  }
149
149
 
150
150
  inline jsi::Value callFunctionWithThis(jsi::IRuntime &runtime, const jsi::Function &function, const jsi::Object &jsThis, const jsi::Value *_Nullable args, size_t count) {
151
- return expo::CppError::tryCatch(runtime, ^{
151
+ return expo::CppError::tryCatch(runtime, [&] {
152
152
  return function.callWithThis(runtime, jsThis, args, count);
153
153
  });
154
154
  }
155
155
 
156
156
  inline jsi::Value callAsConstructor(jsi::IRuntime &runtime, const jsi::Function &function, const jsi::Value *_Nullable args, size_t count) {
157
- return expo::CppError::tryCatch(runtime, ^{
157
+ return expo::CppError::tryCatch(runtime, [&] {
158
158
  return function.callAsConstructor(runtime, args, count);
159
159
  });
160
160
  }
@@ -58,6 +58,17 @@ struct JavaScriptActorTests {
58
58
  }
59
59
  }
60
60
 
61
+ @Test
62
+ func `assumeIsolated preserves typed error`() {
63
+ struct CustomError: Error {}
64
+
65
+ #expect(throws: CustomError.self) {
66
+ try JavaScriptActor.assumeIsolated { () throws(CustomError) -> Int in
67
+ throw CustomError()
68
+ }
69
+ }
70
+ }
71
+
61
72
  @Test
62
73
  func `assumeIsolated can modify captured variables`() {
63
74
  var counter = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-jsi",
3
- "version": "57.0.5",
3
+ "version": "57.0.7",
4
4
  "description": "The JavaScript Interface for Expo Modules",
5
5
  "main": "index.js",
6
6
  "sideEffects": [],
@@ -41,7 +41,7 @@
41
41
  "./apple/scripts/test.sh"
42
42
  ]
43
43
  },
44
- "gitHead": "499024a441d67293aa60afcbe02051999a54f1bb",
44
+ "gitHead": "b76ecf192c5325793bcea31dd9bb8efd91f9ad7f",
45
45
  "scripts": {
46
46
  "build": "apple/scripts/build-xcframework.sh",
47
47
  "clean": "apple/scripts/clear-caches.sh",