expo-modules-jsi 57.0.0 → 57.0.2
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 +21 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +70 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRef.swift +12 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +83 -1
- package/apple/Sources/ExpoModulesJSI/Runtime/LongLivedObjectCollection.swift +46 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptError.swift +15 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +89 -31
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +4 -2
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +10 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/RuntimeScheduler.h +3 -2
- package/apple/Tests/JavaScriptCodableDateTests.swift +270 -0
- package/apple/Tests/JavaScriptErrorTests.swift +33 -0
- package/apple/Tests/JavaScriptPromiseTests.swift +125 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +67 -0
- package/apple/Tests/LongLivedObjectCollectionTests.swift +228 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,26 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 57.0.2 — 2026-07-15
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [iOS] Add `JavaScriptRef.withValue`, a non-consuming borrow accessor that reads the referenced value without taking it, so a long-lived reference can be read repeatedly. ([#47238](https://github.com/expo/expo/pull/47238) by [@tsapeta](https://github.com/tsapeta))
|
|
18
|
+
- [iOS] Add `JavaScriptRuntime.longLivedObjects`, a `LongLivedObjectCollection` that keeps `LongLivedObject`s (such as in-flight promises) alive across asynchronous boundaries and releases any that remain when the runtime is torn down. ([#47511](https://github.com/expo/expo/pull/47511) by [@tsapeta](https://github.com/tsapeta))
|
|
19
|
+
- [iOS] Add a `JavaScriptCodable` conformance for `Date`: it encodes to a JS `Date` and decodes from a JS `Date`, a number of milliseconds since the epoch, or a string parsed by the JS engine's `Date` constructor. ([#47602](https://github.com/expo/expo/pull/47602) by [@tsapeta](https://github.com/tsapeta))
|
|
20
|
+
|
|
21
|
+
### 🐛 Bug fixes
|
|
22
|
+
|
|
23
|
+
- [iOS] Fixed a use-after-free when a `JavaScriptPromise` outlives its runtime (e.g. an async function's promise held by a completion handler that fires after `reloadAsync()`) by having the runtime's `LongLivedObjectCollection` own its JSI values and release them on the JavaScript thread when the wrapper is dropped or at teardown, instead of against a freed runtime. ([#47521](https://github.com/expo/expo/pull/47521) by [@tsapeta](https://github.com/tsapeta))
|
|
24
|
+
- [iOS] Preserve the `code` on the JavaScript error when an async function rejects with a `JavaScriptThrowable` (e.g. an `Exception`), instead of stringifying it and dropping the `code`, mirroring the synchronous throw path. ([#47259](https://github.com/expo/expo/pull/47259) by [@wwdrew](https://github.com/wwdrew))
|
|
25
|
+
- [iOS] Fixed a standalone `JavaScriptRuntime` leaking its underlying Hermes runtime: a runtime it creates itself is now destroyed on `deinit`, while runtimes adopted from elsewhere (e.g. React Native) are left untouched. ([#47515](https://github.com/expo/expo/pull/47515) by [@tsapeta](https://github.com/tsapeta))
|
|
26
|
+
|
|
27
|
+
## 57.0.1 — 2026-07-07
|
|
28
|
+
|
|
29
|
+
### 🐛 Bug fixes
|
|
30
|
+
|
|
31
|
+
- [iOS] Return `NSNull` instead of trapping in the deprecated `JavaScriptValue.getAny()` when it encounters a unrepresentable value. ([#47381](https://github.com/expo/expo/pull/47381) by [@alanjhughes](https://github.com/alanjhughes))
|
|
32
|
+
|
|
13
33
|
## 57.0.0 — 2026-06-25
|
|
14
34
|
|
|
15
35
|
### 🛠 Breaking changes
|
|
@@ -52,6 +72,7 @@
|
|
|
52
72
|
|
|
53
73
|
### 🐛 Bug fixes
|
|
54
74
|
|
|
75
|
+
- [iOS] Fixed `Build ExpoModulesJSI xcframework` build phase failing on Xcode 26 because the nested SwiftPM build ignored `-derivedDataPath` and wrote products outside the expected location. ([#46326](https://github.com/expo/expo/issues/46326) by [@Kurogoma4D](https://github.com/Kurogoma4D))
|
|
55
76
|
- [iOS] Fixed the xcframework build failing with a `sed` error when building in an environment that uses GNU `sed` instead of BSD `sed` (e.g. a Nix shell). ([#46389](https://github.com/expo/expo/pull/46389) by [@niteshbalusu11](https://github.com/niteshbalusu11))
|
|
56
77
|
- [iOS] Propagate `JavaScriptPromise` setup failures instead of trapping the app. ([#46106](https://github.com/expo/expo/issues/46106) by [@qutrek](https://github.com/qutrek)) ([#46145](https://github.com/expo/expo/pull/46145) by [@mvincentong](https://github.com/mvincentong))
|
|
57
78
|
- Fix build framework for macOS ([#46413](https://github.com/expo/expo/pull/46413) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
// `Date` encodes to a JS `Date` and decodes from a JS `Date`, a number of milliseconds since the epoch,
|
|
6
|
+
// or a string. Strings are parsed by the runtime's own `new Date(str)` (so parsing is identical on every
|
|
7
|
+
// platform, with no native date parser to maintain), which inherits JS's quirks: a zone-less string is
|
|
8
|
+
// read as local time, and an unparseable one becomes an `Invalid Date` (`NaN`), decoded as a thrown error.
|
|
9
|
+
// A `Date` is an absolute instant with no timezone/calendar; resolution is milliseconds.
|
|
10
|
+
|
|
11
|
+
extension Date: JavaScriptCodable {
|
|
12
|
+
@JavaScriptActor
|
|
13
|
+
@inlinable
|
|
14
|
+
public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Date
|
|
15
|
+
{
|
|
16
|
+
// The cheap tag checks come before `is("Date")`, which does a global lookup plus an `instanceof`
|
|
17
|
+
// walk; a number or string can't be a `Date`, so the order is behavior-neutral.
|
|
18
|
+
if value.isNumber() {
|
|
19
|
+
return try dateFromMilliseconds(value.getDouble())
|
|
20
|
+
}
|
|
21
|
+
if value.isString() {
|
|
22
|
+
let dateConstructor = try runtime.global().getPropertyAsFunction("Date")
|
|
23
|
+
let constructed = try dateConstructor.callAsConstructor(value.copy()).asObject()
|
|
24
|
+
return try dateFromMilliseconds(constructed.callFunction("getTime").asDouble())
|
|
25
|
+
}
|
|
26
|
+
if value.is("Date") {
|
|
27
|
+
return try dateFromMilliseconds(value.asObject().callFunction("getTime").asDouble())
|
|
28
|
+
}
|
|
29
|
+
throw InvalidDateException()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@JavaScriptActor
|
|
33
|
+
@inlinable
|
|
34
|
+
public static func encode(_ value: Date, in runtime: borrowing JavaScriptRuntime) throws -> JavaScriptValue {
|
|
35
|
+
let milliseconds = value.timeIntervalSince1970 * 1000.0
|
|
36
|
+
let dateConstructor = try runtime.global().getPropertyAsFunction("Date")
|
|
37
|
+
// Typed explicitly so the variadic `callAsConstructor` overload is chosen over the `JavaScriptValuesBuffer?` one.
|
|
38
|
+
let millisecondsValue: JavaScriptValue = .number(milliseconds)
|
|
39
|
+
return try dateConstructor.callAsConstructor(millisecondsValue)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The largest magnitude, in milliseconds, a JS `Date` can represent (100,000,000 days from the epoch,
|
|
44
|
+
/// ECMAScript TimeClip); a value beyond it is an `Invalid Date`.
|
|
45
|
+
@usableFromInline
|
|
46
|
+
let maxJavaScriptDateMilliseconds: Double = 8_640_000_000_000_000
|
|
47
|
+
|
|
48
|
+
/// Builds a `Date` from a milliseconds value, applying the JS `Date` constructor's TimeClip: a non-finite
|
|
49
|
+
/// or out-of-range value throws, an in-range one is truncated toward zero. This keeps the number branch
|
|
50
|
+
/// faithful to `new Date(number)`; the `Date`/string branches pass an already-clipped `getTime()` through.
|
|
51
|
+
@usableFromInline
|
|
52
|
+
func dateFromMilliseconds(_ milliseconds: Double) throws -> Date {
|
|
53
|
+
guard milliseconds.isFinite, abs(milliseconds) <= maxJavaScriptDateMilliseconds else {
|
|
54
|
+
throw InvalidDateException()
|
|
55
|
+
}
|
|
56
|
+
return Date(timeIntervalSince1970: milliseconds.rounded(.towardZero) / 1000.0)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Thrown when a JavaScript value can't be converted to a `Date`. Named after JS's own "Invalid Date".
|
|
60
|
+
public struct InvalidDateException: JavaScriptThrowable {
|
|
61
|
+
@usableFromInline
|
|
62
|
+
init() {}
|
|
63
|
+
|
|
64
|
+
public var code: String {
|
|
65
|
+
"ERR_INVALID_DATE"
|
|
66
|
+
}
|
|
67
|
+
public var message: String {
|
|
68
|
+
"Cannot convert the JavaScript value to a Date because it is not a Date, a number of milliseconds since the epoch, or a parseable date string"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -52,6 +52,18 @@ public final class JavaScriptRef<T: JavaScriptType & ~Copyable>: JavaScriptType,
|
|
|
52
52
|
return value.take()
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/// Borrows the referenced value for the duration of `body` without consuming it, so the reference
|
|
56
|
+
/// keeps holding the value and can be read again. `body` receives the value as a borrow, or `nil` when
|
|
57
|
+
/// the reference is empty (already taken, released, or never set), and its result is returned as-is.
|
|
58
|
+
/// Use this instead of `take()` when the value must stay in the reference (e.g. a long-lived ref read
|
|
59
|
+
/// repeatedly).
|
|
60
|
+
///
|
|
61
|
+
/// `body` returns `R` directly (rather than this method wrapping it in `R?`) so the result can itself
|
|
62
|
+
/// be a non-`Copyable` optional like `JavaScriptObject?`, which can't be nested inside another optional.
|
|
63
|
+
public func withValue<R: ~Copyable>(_ body: (borrowing T?) throws -> R?) rethrows -> R? {
|
|
64
|
+
return try body(value)
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
/// Takes the value as a `JavaScriptValue`. Returns `undefined` value if the reference does not hold any value.
|
|
56
68
|
public func asValue() -> JavaScriptValue {
|
|
57
69
|
return take()?.asValue() ?? .undefined
|
|
@@ -38,6 +38,12 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
38
38
|
internal let runtimePointee: facebook.jsi.Runtime
|
|
39
39
|
internal let scheduler: expo.RuntimeScheduler
|
|
40
40
|
|
|
41
|
+
/// Whether this wrapper owns the underlying `jsi::Runtime` and must destroy it on `deinit`. True
|
|
42
|
+
/// only for the standalone `init()`, which creates the runtime via `createHermesRuntime()`. The
|
|
43
|
+
/// other initializers adopt a runtime owned elsewhere (e.g. React Native), which must never be
|
|
44
|
+
/// freed here, matching the immortal (no-op) release semantics of the imported reference type.
|
|
45
|
+
private let ownsRuntime: Bool
|
|
46
|
+
|
|
41
47
|
/// Thread ID of the JavaScript thread, captured at construction time. Used by `isOnJavaScriptThread()`
|
|
42
48
|
/// for a fast integer comparison instead of `Thread.current.name == "..."`.
|
|
43
49
|
/// Assumes runtime initializers always run on the JS thread.
|
|
@@ -57,6 +63,8 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
57
63
|
self.runtimePointee = runtime
|
|
58
64
|
self.pointee = expo.iruntime(runtime)
|
|
59
65
|
self.scheduler = expo.RuntimeScheduler()
|
|
66
|
+
self.ownsRuntime = false
|
|
67
|
+
installLongLivedObjectsTeardown()
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/// Creates a standalone Hermes runtime. Scheduled tasks run synchronously —
|
|
@@ -66,6 +74,8 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
66
74
|
self.runtimePointee = runtime
|
|
67
75
|
self.pointee = expo.iruntime(runtime)
|
|
68
76
|
self.scheduler = expo.RuntimeScheduler()
|
|
77
|
+
self.ownsRuntime = true
|
|
78
|
+
installLongLivedObjectsTeardown()
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
/// Creates a runtime from a raw pointer to the underlying `facebook.jsi.Runtime`.
|
|
@@ -76,6 +86,8 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
76
86
|
self.runtimePointee = runtime
|
|
77
87
|
self.pointee = expo.iruntime(runtime)
|
|
78
88
|
self.scheduler = expo.RuntimeScheduler()
|
|
89
|
+
self.ownsRuntime = false
|
|
90
|
+
installLongLivedObjectsTeardown()
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
/// Creates a runtime bound to a host-provided React `RuntimeScheduler`. Calls to
|
|
@@ -84,7 +96,11 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
84
96
|
/// React Native factory uses.
|
|
85
97
|
///
|
|
86
98
|
/// - `unsafePointer`: raw pointer to the underlying `facebook::jsi::Runtime`.
|
|
87
|
-
/// - `scheduler`:
|
|
99
|
+
/// - `scheduler`: opaque host-owned handle that `dispatch` resolves to the real
|
|
100
|
+
/// scheduler. The React Native factory passes a handle that references the
|
|
101
|
+
/// `react::RuntimeScheduler` weakly (see `EXReactSchedulerDispatch.h` in
|
|
102
|
+
/// `ExpoModulesCore`), so dispatching after the React instance tore the
|
|
103
|
+
/// scheduler down safely drops the task.
|
|
88
104
|
/// - `dispatch`: raw pointer to a C function with signature
|
|
89
105
|
/// `void (*)(void *scheduler, int priority, void (^callback)())`.
|
|
90
106
|
public init(
|
|
@@ -97,6 +113,27 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
97
113
|
self.runtimePointee = runtime
|
|
98
114
|
self.pointee = expo.iruntime(runtime)
|
|
99
115
|
self.scheduler = expo.RuntimeScheduler(scheduler, fn)
|
|
116
|
+
self.ownsRuntime = false
|
|
117
|
+
installLongLivedObjectsTeardown()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
deinit {
|
|
121
|
+
// Destroy the runtime only if this wrapper created it (standalone `init()`); adopted runtimes
|
|
122
|
+
// are owned elsewhere (e.g. React Native) and must not be freed here.
|
|
123
|
+
guard ownsRuntime else {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
// Release cached JSI objects (e.g. `PropNameID`s) before the runtime goes away. Swift tears
|
|
127
|
+
// down stored properties only after `deinit` returns, so leaving them for that phase would
|
|
128
|
+
// destroy them against an already-freed runtime, which JSI forbids: all objects associated with
|
|
129
|
+
// a runtime must be destroyed before the runtime itself.
|
|
130
|
+
//
|
|
131
|
+
// No thread hop is needed. JSI documents that destructors are safe to call from any thread; the
|
|
132
|
+
// requirement is only that there is no concurrent access, which holds here since `deinit` runs
|
|
133
|
+
// when the last reference is gone. `deinit` is `nonisolated`, so it can touch the actor-isolated
|
|
134
|
+
// registry directly given that exclusive access.
|
|
135
|
+
propNameIdsRegistry.removeAll()
|
|
136
|
+
expo.destroyRuntime(runtimePointee)
|
|
100
137
|
}
|
|
101
138
|
|
|
102
139
|
/// Provides scoped access to a raw pointer to the underlying `facebook.jsi.Runtime`.
|
|
@@ -637,6 +674,51 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
|
|
|
637
674
|
|
|
638
675
|
@JavaScriptActor
|
|
639
676
|
internal var propNameIdsRegistry: [String: JavaScriptPropNameID] = [:]
|
|
677
|
+
|
|
678
|
+
// MARK: - Long-lived objects
|
|
679
|
+
|
|
680
|
+
/// Registry of JSI objects (such as in-flight promises) that must outlive the native call that
|
|
681
|
+
/// created them. Cleared when the runtime is torn down so their JSI state is released on the
|
|
682
|
+
/// JavaScript thread while the runtime is still valid.
|
|
683
|
+
@JavaScriptActor
|
|
684
|
+
public let longLivedObjects = LongLivedObjectCollection()
|
|
685
|
+
|
|
686
|
+
/// Attaches a native state to a dedicated JS object whose deallocator clears ``longLivedObjects``
|
|
687
|
+
/// when the runtime is torn down. The object is pinned by storing it as a property on `global`, so
|
|
688
|
+
/// it stays reachable within the JavaScript heap for the runtime's whole life (no Swift-side strong
|
|
689
|
+
/// reference) and its native state drops only when the runtime's object graph is destroyed. That
|
|
690
|
+
/// fires the deallocator on the JavaScript thread while the runtime is still valid, the point at
|
|
691
|
+
/// which any surviving long-lived objects can safely release their JSI state.
|
|
692
|
+
private func installLongLivedObjectsTeardown() {
|
|
693
|
+
// Runtime initializers run on the JavaScript thread but aren't actor-isolated, so reach the
|
|
694
|
+
// isolated object/native-state/`clear()` APIs through the actor.
|
|
695
|
+
JavaScriptActor.assumeIsolated {
|
|
696
|
+
// Capture the collection strongly, not `self`: teardown may run as the runtime itself
|
|
697
|
+
// deallocates, and the sweep must still call `allowRelease()` on survivors. Holding the
|
|
698
|
+
// collection keeps it alive for the sweep; it does not retain the runtime.
|
|
699
|
+
let longLivedObjects = self.longLivedObjects
|
|
700
|
+
let nativeState = JavaScriptNativeState()
|
|
701
|
+
nativeState.setDeallocator { nativeState in
|
|
702
|
+
// Fires as the teardown object is released on the JavaScript thread with the runtime still
|
|
703
|
+
// valid, so releasing JSI state here is safe. Mirrors the caveat on `AppContext.NativeState`:
|
|
704
|
+
// a future cross-runtime path that could drop this state from another thread would have to
|
|
705
|
+
// hop back to the JavaScript thread first.
|
|
706
|
+
JavaScriptActor.assumeIsolated {
|
|
707
|
+
longLivedObjects.clear()
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
let object = createObject()
|
|
711
|
+
object.setNativeState(nativeState)
|
|
712
|
+
// Pin via a JS-heap reference on `global` rather than a Swift property, so the object's
|
|
713
|
+
// lifetime is governed by the runtime's object graph. The property name is unique per wrapper
|
|
714
|
+
// (keyed by this wrapper's address), so several wrappers of the same underlying runtime (e.g.
|
|
715
|
+
// via `init(unsafePointer:)`) each pin their own teardown object instead of overwriting a
|
|
716
|
+
// shared slot, which would let one wrapper's collection be swept early while the runtime is
|
|
717
|
+
// still alive.
|
|
718
|
+
let wrapperAddress = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
|
719
|
+
global().setProperty("__expo_long_lived_objects_teardown_\(wrapperAddress)__", value: object.asValue())
|
|
720
|
+
}
|
|
721
|
+
}
|
|
640
722
|
}
|
|
641
723
|
|
|
642
724
|
private func createFunctionClosure(
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/// An object whose lifetime must extend past the native call that created it, such as an in-flight
|
|
2
|
+
/// promise holding its resolve/reject state. Register it with a ``LongLivedObjectCollection``.
|
|
3
|
+
@JavaScriptActor
|
|
4
|
+
public protocol LongLivedObject: AnyObject {
|
|
5
|
+
/// Called on the JavaScript thread when the runtime tears down while this object is still
|
|
6
|
+
/// registered, to release any held JSI state. Not called on the normal completion path, where the
|
|
7
|
+
/// object removes itself after releasing its own state.
|
|
8
|
+
func allowRelease()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Keeps ``LongLivedObject``s alive across asynchronous boundaries and releases any that remain when
|
|
12
|
+
/// the runtime is torn down. Isolated to ``JavaScriptActor``, so it needs no lock.
|
|
13
|
+
@JavaScriptActor
|
|
14
|
+
public final class LongLivedObjectCollection {
|
|
15
|
+
// Keyed by identity, effectively a set. A dictionary rather than a `Set` because
|
|
16
|
+
// `any LongLivedObject` is an existential and cannot conform to `Hashable`.
|
|
17
|
+
private var registeredObjects: [ObjectIdentifier: any LongLivedObject] = [:]
|
|
18
|
+
|
|
19
|
+
// `nonisolated` so ``JavaScriptRuntime`` can create one as a stored-property default.
|
|
20
|
+
internal nonisolated init() {}
|
|
21
|
+
|
|
22
|
+
/// Registers an object, keeping it alive until it is removed or the collection is cleared.
|
|
23
|
+
public func add(_ object: any LongLivedObject) {
|
|
24
|
+
registeredObjects[ObjectIdentifier(object)] = object
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Removes an object that finished on its own, without calling ``LongLivedObject/allowRelease()``.
|
|
28
|
+
public func remove(_ object: any LongLivedObject) {
|
|
29
|
+
registeredObjects[ObjectIdentifier(object)] = nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Teardown sweep: calls ``LongLivedObject/allowRelease()`` on every remaining object and empties
|
|
33
|
+
/// the collection. Must run on the JavaScript thread while the runtime is still valid.
|
|
34
|
+
public func clear() {
|
|
35
|
+
let survivors = registeredObjects.values
|
|
36
|
+
registeredObjects.removeAll()
|
|
37
|
+
for object in survivors {
|
|
38
|
+
object.allowRelease()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Number of currently registered objects.
|
|
43
|
+
public var count: Int {
|
|
44
|
+
return registeredObjects.count
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -50,6 +50,21 @@ public final class JavaScriptError: Error, Sendable {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/// Returns the `JavaScriptError` representing an arbitrary native error. An existing
|
|
54
|
+
/// `JavaScriptError` is returned unchanged so its wrapped value reaches JS as-is; a
|
|
55
|
+
/// `JavaScriptThrowable` is routed through the code-preserving initializer above; any
|
|
56
|
+
/// other error is stringified into a generic `Error`.
|
|
57
|
+
@inlinable
|
|
58
|
+
public static func from(_ error: any Error, in runtime: JavaScriptRuntime) -> JavaScriptError {
|
|
59
|
+
if let jsError = error as? JavaScriptError {
|
|
60
|
+
return jsError
|
|
61
|
+
}
|
|
62
|
+
if let throwable = error as? JavaScriptThrowable {
|
|
63
|
+
return JavaScriptError(runtime, from: throwable)
|
|
64
|
+
}
|
|
65
|
+
return JavaScriptError(runtime, message: String(describing: error))
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
/// Returns the error as a `JavaScriptValue`, which may be an arbitrary value rather than an
|
|
54
69
|
/// `Error` instance when the error was created from one.
|
|
55
70
|
public func toValue() -> JavaScriptValue {
|
|
@@ -11,46 +11,93 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
11
11
|
private typealias PromiseContinuation = CheckedContinuation<JavaScriptValue.Ref, any Error>
|
|
12
12
|
|
|
13
13
|
private weak let runtime: JavaScriptRuntime?
|
|
14
|
-
private var object: JavaScriptObject
|
|
15
14
|
private let deferredPromise = DeferredPromise()
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
/// Owns the promise's JSI values (the object and, for a deferred promise, its resolve/reject
|
|
17
|
+
/// functions). Registered with the runtime's ``LongLivedObjectCollection`` so they're released on
|
|
18
|
+
/// the JS thread rather than against a freed runtime when a wrapper outlives its runtime (e.g. an
|
|
19
|
+
/// async function's promise held by a URLSession delegate).
|
|
20
|
+
///
|
|
21
|
+
/// There are two release paths, both on the JS thread while the runtime is alive:
|
|
22
|
+
/// - When the ``JavaScriptPromise`` wrapper is dropped, its `deinit` schedules a job that
|
|
23
|
+
/// deregisters this state and releases the values, so a stream of promises doesn't pin their
|
|
24
|
+
/// objects (and resolution values) until teardown.
|
|
25
|
+
/// - If the wrapper outlives the runtime, the teardown sweep (``allowRelease()``) releases whatever
|
|
26
|
+
/// is still registered before the runtime is destroyed.
|
|
27
|
+
///
|
|
28
|
+
/// The state stays registered for as long as the wrapper is alive, even after the promise settles,
|
|
29
|
+
/// so ``asValue()`` keeps returning a valid object. Settling only releases the resolve/reject
|
|
30
|
+
/// functions, which can no longer be called and are the bulk of a deferred promise's held state.
|
|
31
|
+
@JavaScriptActor
|
|
32
|
+
private final class LongLivedState: LongLivedObject {
|
|
33
|
+
// Stored as `JavaScriptValue` (a reference type), not `JavaScriptObject`: a `Copyable` value read
|
|
34
|
+
// back through `JavaScriptRef.withValue` avoids the copy a borrowed `~Copyable` object would trap on.
|
|
35
|
+
let object = JavaScriptValue.Ref()
|
|
36
|
+
let resolveFunction = JavaScriptValue.Ref()
|
|
37
|
+
let rejectFunction = JavaScriptValue.Ref()
|
|
38
|
+
|
|
39
|
+
func allowRelease() {
|
|
40
|
+
object.release()
|
|
41
|
+
resolveFunction.release()
|
|
42
|
+
rejectFunction.release()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private let longLivedState = LongLivedState()
|
|
47
|
+
|
|
48
|
+
/// Dropping the wrapper means no native code can settle or read this promise anymore, so release
|
|
49
|
+
/// its long-lived state. The values can only be touched on the JS thread while the runtime is
|
|
50
|
+
/// alive, so schedule the work there; if the runtime is already gone, the teardown sweep has
|
|
51
|
+
/// released everything and there is nothing to do (this is the `#47454` off-thread-drop case).
|
|
52
|
+
deinit {
|
|
53
|
+
guard let runtime else {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
// Capture the collection, not the runtime, so a wrapper's deinit can't prolong the runtime's
|
|
57
|
+
// lifetime by keeping it alive until the scheduled job drains.
|
|
58
|
+
let longLivedObjects = runtime.longLivedObjects
|
|
59
|
+
runtime.schedule { [longLivedState] in
|
|
60
|
+
longLivedObjects.remove(longLivedState)
|
|
61
|
+
longLivedState.allowRelease()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
21
64
|
|
|
22
65
|
/// Initializes a promise from the existing object. The promise may already be settled.
|
|
23
66
|
/// It cannot be resolved/rejected from the outside, i.e. `resolve` and `reject` functions are no-op.
|
|
24
67
|
@JavaScriptActor
|
|
25
68
|
public init(_ runtime: JavaScriptRuntime, _ object: consuming JavaScriptObject) throws {
|
|
26
69
|
self.runtime = runtime
|
|
27
|
-
|
|
70
|
+
longLivedState.object.reset(object.asValue())
|
|
28
71
|
try setUpCallbacks()
|
|
72
|
+
// Register only after setup succeeds, so a failed initializer (e.g. `then` unavailable) doesn't
|
|
73
|
+
// leave the state pinned in the collection until teardown. Owns the promise's JSI values from
|
|
74
|
+
// here until teardown (see `LongLivedState`).
|
|
75
|
+
runtime.longLivedObjects.add(longLivedState)
|
|
29
76
|
}
|
|
30
77
|
|
|
31
78
|
/// Creates a new promise whose resolver or rejecter must be called from the outside (also known as a deferred promise).
|
|
32
79
|
@JavaScriptActor
|
|
33
80
|
public init(_ runtime: JavaScriptRuntime) throws {
|
|
34
81
|
self.runtime = runtime
|
|
35
|
-
// Initialize the non-copyable field before any throwing work. Swift requires a
|
|
36
|
-
// consistently initialized value on every throwing initializer path.
|
|
37
|
-
self.object = runtime.createObject()
|
|
38
82
|
|
|
39
83
|
// Create function that is the promise setup. It is called immediately on `callAsConstructor`.
|
|
40
|
-
let setup = runtime.createFunction { [weak
|
|
41
|
-
resolveFunction
|
|
42
|
-
rejectFunction
|
|
84
|
+
let setup = runtime.createFunction { [weak longLivedState] this, arguments in
|
|
85
|
+
longLivedState?.resolveFunction.reset(arguments[0])
|
|
86
|
+
longLivedState?.rejectFunction.reset(arguments[1])
|
|
43
87
|
return .undefined
|
|
44
88
|
}
|
|
45
89
|
|
|
46
|
-
|
|
90
|
+
let object =
|
|
47
91
|
try runtime
|
|
48
92
|
.global()
|
|
49
93
|
.getPropertyAsFunction(.cached(runtime, "Promise"))
|
|
50
94
|
.callAsConstructor(setup.asValue())
|
|
51
|
-
|
|
52
|
-
|
|
95
|
+
longLivedState.object.reset(object)
|
|
53
96
|
try setUpCallbacks()
|
|
97
|
+
// Register only after setup succeeds, so a failed initializer (e.g. `then` unavailable) doesn't
|
|
98
|
+
// leave the state pinned in the collection until teardown. Owns the promise's JSI values from
|
|
99
|
+
// here until teardown (see `LongLivedState`).
|
|
100
|
+
runtime.longLivedObjects.add(longLivedState)
|
|
54
101
|
}
|
|
55
102
|
|
|
56
103
|
@JavaScriptActor
|
|
@@ -59,7 +106,7 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
59
106
|
}
|
|
60
107
|
|
|
61
108
|
public var isDeferred: Bool {
|
|
62
|
-
return !resolveFunction.isEmpty && !rejectFunction.isEmpty
|
|
109
|
+
return !longLivedState.resolveFunction.isEmpty && !longLivedState.rejectFunction.isEmpty
|
|
63
110
|
}
|
|
64
111
|
|
|
65
112
|
@JavaScriptActor
|
|
@@ -68,7 +115,10 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
68
115
|
}
|
|
69
116
|
|
|
70
117
|
public func asValue() -> JavaScriptValue {
|
|
71
|
-
|
|
118
|
+
// Read without consuming, so the state keeps owning the object (unlike `Ref.asValue()`).
|
|
119
|
+
return longLivedState.object.withValue { object in
|
|
120
|
+
return object
|
|
121
|
+
} ?? .undefined
|
|
72
122
|
}
|
|
73
123
|
|
|
74
124
|
public func resolve<V: JavaScriptRepresentable>(_ value: V) {
|
|
@@ -77,17 +127,18 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
77
127
|
}
|
|
78
128
|
|
|
79
129
|
// `resolve` is not isolated, so make sure to jump to JS thread.
|
|
80
|
-
runtime.schedule(priority: .immediate) { [
|
|
130
|
+
runtime.schedule(priority: .immediate) { [longLivedState] in
|
|
81
131
|
// If the promise is already settled, do nothing.
|
|
82
|
-
guard let resolver = resolveFunction.take() else {
|
|
132
|
+
guard let resolver = longLivedState.resolveFunction.take() else {
|
|
83
133
|
return
|
|
84
134
|
}
|
|
85
135
|
// Call the actual resolver given in the Promise setup.
|
|
86
136
|
// This will also call `deferredPromise.resolve` in the `then` handler.
|
|
87
137
|
_ = try! resolver.getFunction().call(arguments: value)
|
|
88
138
|
|
|
89
|
-
//
|
|
90
|
-
|
|
139
|
+
// The rejecter can't be called anymore. The state stays registered so it keeps owning the
|
|
140
|
+
// object until the wrapper is dropped (or the teardown sweep runs).
|
|
141
|
+
longLivedState.rejectFunction.release()
|
|
91
142
|
}
|
|
92
143
|
}
|
|
93
144
|
|
|
@@ -97,23 +148,24 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
97
148
|
}
|
|
98
149
|
|
|
99
150
|
// `reject` is not isolated, so make sure to jump to JS thread.
|
|
100
|
-
runtime.schedule(priority: .immediate) { [
|
|
151
|
+
runtime.schedule(priority: .immediate) { [longLivedState] in
|
|
101
152
|
// If the promise is already settled, do nothing.
|
|
102
|
-
guard let rejecter = rejectFunction.take() else {
|
|
153
|
+
guard let rejecter = longLivedState.rejectFunction.take() else {
|
|
103
154
|
return
|
|
104
155
|
}
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
let errorValue =
|
|
156
|
+
// Convert the error to its JavaScript representation. This preserves an existing
|
|
157
|
+
// `JavaScriptError`'s wrapped value and a `JavaScriptThrowable`'s structured `code`
|
|
158
|
+
// (mirroring the synchronous throw path in `forwardingSwiftErrorsToJS`), so the `code`
|
|
159
|
+
// is not lost on async rejection. See `JavaScriptError.from(_:in:)`.
|
|
160
|
+
let errorValue = JavaScriptError.from(error, in: runtime).toValue()
|
|
110
161
|
|
|
111
162
|
// Call the actual rejecter given in the Promise setup.
|
|
112
163
|
// This will also call `deferredPromise.reject` in the `then` handler.
|
|
113
164
|
_ = try! rejecter.getFunction().call(arguments: errorValue)
|
|
114
165
|
|
|
115
|
-
//
|
|
116
|
-
|
|
166
|
+
// The resolver can't be called anymore. The state stays registered so it keeps owning the
|
|
167
|
+
// object until the wrapper is dropped (or the teardown sweep runs).
|
|
168
|
+
longLivedState.resolveFunction.release()
|
|
117
169
|
}
|
|
118
170
|
}
|
|
119
171
|
|
|
@@ -140,6 +192,12 @@ public struct JavaScriptPromise: JavaScriptType, ~Copyable {
|
|
|
140
192
|
}
|
|
141
193
|
return .undefined
|
|
142
194
|
}
|
|
143
|
-
try object.
|
|
195
|
+
_ = try longLivedState.object.withValue { object in
|
|
196
|
+
try object?.getObject().callFunction(
|
|
197
|
+
.cached(runtime, "then"),
|
|
198
|
+
arguments: onFulfilled.asValue(),
|
|
199
|
+
onRejected.asValue()
|
|
200
|
+
)
|
|
201
|
+
}
|
|
144
202
|
}
|
|
145
203
|
}
|
|
@@ -208,7 +208,8 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
208
208
|
return result
|
|
209
209
|
}
|
|
210
210
|
if object.isFunction() {
|
|
211
|
-
|
|
211
|
+
// Don't trap, callers convert speculatively under `try?`, which can't catch a `fatalError`.
|
|
212
|
+
return NSNull()
|
|
212
213
|
}
|
|
213
214
|
var result = [String: Any]()
|
|
214
215
|
|
|
@@ -222,7 +223,8 @@ public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
|
|
|
222
223
|
}
|
|
223
224
|
return result
|
|
224
225
|
}
|
|
225
|
-
|
|
226
|
+
// Unrepresentable kind (e.g. symbol). Don't trap, for the same reason as above.
|
|
227
|
+
return NSNull()
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
/// Returns the value as a boolean, or asserts if not a boolean.
|
|
@@ -125,6 +125,16 @@ inline bool isHostObject(jsi::IRuntime &runtime, const jsi::Object &object) {
|
|
|
125
125
|
|
|
126
126
|
jsi::Runtime* createHermesRuntime();
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
Destroys a `jsi::Runtime` created by `createHermesRuntime()`. Since the runtime is imported into
|
|
130
|
+
Swift as an immortal reference type (no ARC-managed lifetime), Swift can't `delete` it directly, so
|
|
131
|
+
the standalone `JavaScriptRuntime` calls this from its `deinit` to free the runtime it owns. Must
|
|
132
|
+
not be called on a runtime owned elsewhere (e.g. the React Native-provided one).
|
|
133
|
+
*/
|
|
134
|
+
inline void destroyRuntime(jsi::Runtime &runtime) {
|
|
135
|
+
delete &runtime;
|
|
136
|
+
}
|
|
137
|
+
|
|
128
138
|
inline jsi::Value evaluateJavaScript(jsi::IRuntime &runtime, const std::shared_ptr<const jsi::Buffer>& buffer, const std::string& sourceURL) {
|
|
129
139
|
return expo::CppError::tryCatch(runtime, ^{
|
|
130
140
|
return runtime.evaluateJavaScript(buffer, sourceURL);
|
|
@@ -31,8 +31,9 @@ public:
|
|
|
31
31
|
using ScheduleTaskCallback = void(^)();
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
Trampoline implemented by the host
|
|
35
|
-
react::RuntimeScheduler
|
|
34
|
+
Trampoline implemented by the host. It resolves `nativeScheduler` (an opaque
|
|
35
|
+
host-owned handle) to the real react::RuntimeScheduler and calls scheduleTask
|
|
36
|
+
on it, or drops the task when the scheduler no longer exists. Keeping it as a
|
|
36
37
|
function pointer keeps React types out of this header.
|
|
37
38
|
*/
|
|
38
39
|
using ScheduleFn = void (*)(void *nativeScheduler, int priority, ScheduleTaskCallback callback);
|