expo-modules-jsi 58.0.7 → 58.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/apple/Benchmarks/ContainerCodingBenchmarks.swift +74 -0
- package/apple/Benchmarks/HostFunctionBenchmarks.swift +41 -0
- package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
- package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
- package/apple/Benchmarks/ValueAccessBenchmarks.swift +23 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +210 -21
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Set.swift +79 -0
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
- package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
- package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +43 -31
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +97 -7
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +54 -1
- package/apple/Sources/ExpoModulesJSI/Utilities/Errors.swift +19 -0
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
- package/apple/Sources/ExpoModulesJSI/Utilities/UncheckedSendable.swift +13 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +68 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/RuntimeScheduler.h +18 -5
- package/apple/Tests/JavaScriptCodableSetTests.swift +115 -0
- package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
- package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +86 -3
- package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
- package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
- package/apple/scripts/build-xcframework.sh +15 -0
- package/package.json +1 -1
|
@@ -165,6 +165,8 @@ inline void collectGarbage(jsi::IRuntime &runtime, const std::string &cause) {
|
|
|
165
165
|
// owns nothing; an object or string there would leak its engine handle. Both callers,
|
|
166
166
|
// `createHostFunction` and `HostObject::get`, pass a default-constructed slot. The Swift side picks
|
|
167
167
|
// the helper by kind in `JavaScriptValue.writeJSIValue(to:)` and moves everything else.
|
|
168
|
+
// `JavaScriptValue.write(_:to:)` moves a uniquely referenced value's handle into the slot the same
|
|
169
|
+
// way, through `emplaceMovedValue`.
|
|
168
170
|
inline void emplaceUndefined(jsi::Value *_Nonnull slot) noexcept {
|
|
169
171
|
::new (slot) jsi::Value();
|
|
170
172
|
}
|
|
@@ -181,6 +183,31 @@ inline void emplaceNumber(jsi::Value *_Nonnull slot, double value) noexcept {
|
|
|
181
183
|
::new (slot) jsi::Value(value);
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
/**
|
|
187
|
+
Moves the value at `source` into the engine's result slot. Placement new, like the `emplace*`
|
|
188
|
+
helpers above: the slot must hold a value that owns nothing. `source` is left holding a null
|
|
189
|
+
pointer, so destroying it afterwards releases nothing.
|
|
190
|
+
*/
|
|
191
|
+
inline void emplaceMovedValue(jsi::Value *_Nonnull slot, jsi::Value *_Nonnull source) noexcept {
|
|
192
|
+
::new (slot) jsi::Value(std::move(*source));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
Creates a string value straight from UTF-8 bytes. The `jsi::String` returned by `createFromUtf8`
|
|
197
|
+
is moved into the value, so the engine allocates one handle. Going through `jsi::Value(runtime,
|
|
198
|
+
const jsi::String &)` from Swift instead clones the handle and then releases the original.
|
|
199
|
+
*/
|
|
200
|
+
inline jsi::Value createStringValueFromUtf8(jsi::IRuntime &runtime, const uint8_t *_Nonnull utf8, size_t length) {
|
|
201
|
+
return jsi::Value(jsi::String::createFromUtf8(runtime, utf8, length));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
Same as `createStringValueFromUtf8`, for bytes the caller knows to be ASCII.
|
|
206
|
+
*/
|
|
207
|
+
inline jsi::Value createStringValueFromAscii(jsi::IRuntime &runtime, const char *_Nonnull ascii, size_t length) {
|
|
208
|
+
return jsi::Value(jsi::String::createFromAscii(runtime, ascii, length));
|
|
209
|
+
}
|
|
210
|
+
|
|
184
211
|
inline jsi::Value callFunction(jsi::IRuntime &runtime, const jsi::Function &function, const jsi::Value *_Nullable args, size_t count) {
|
|
185
212
|
return expo::CppError::tryCatch(runtime, [&] {
|
|
186
213
|
return function.call(runtime, args, count);
|
|
@@ -193,6 +220,47 @@ inline jsi::Value callFunctionWithThis(jsi::IRuntime &runtime, const jsi::Functi
|
|
|
193
220
|
});
|
|
194
221
|
}
|
|
195
222
|
|
|
223
|
+
/**
|
|
224
|
+
Calls `defineProperty`, the runtime's `Object.defineProperty`, on `object` with a new descriptor that holds
|
|
225
|
+
`value` (when `hasValue` is set) and only the attributes that are `true`, like a descriptor built in
|
|
226
|
+
JavaScript. Taking the function and the descriptor keys from the caller lets it create them once per runtime,
|
|
227
|
+
and building the descriptor and the arguments here avoids a round trip through Swift for each of them.
|
|
228
|
+
*/
|
|
229
|
+
inline void defineProperty(
|
|
230
|
+
jsi::IRuntime &runtime,
|
|
231
|
+
const jsi::Function &defineProperty,
|
|
232
|
+
const jsi::PropNameID &configurableKey,
|
|
233
|
+
const jsi::PropNameID &enumerableKey,
|
|
234
|
+
const jsi::PropNameID &writableKey,
|
|
235
|
+
const jsi::PropNameID &valueKey,
|
|
236
|
+
const jsi::Object &object,
|
|
237
|
+
const uint8_t *_Nonnull nameUtf8,
|
|
238
|
+
size_t nameLength,
|
|
239
|
+
const jsi::Value &value,
|
|
240
|
+
bool hasValue,
|
|
241
|
+
bool writable,
|
|
242
|
+
bool enumerable,
|
|
243
|
+
bool configurable
|
|
244
|
+
) {
|
|
245
|
+
expo::CppError::tryCatch(runtime, [&] {
|
|
246
|
+
jsi::Object descriptor(runtime);
|
|
247
|
+
if (configurable) {
|
|
248
|
+
descriptor.setProperty(runtime, configurableKey, true);
|
|
249
|
+
}
|
|
250
|
+
if (enumerable) {
|
|
251
|
+
descriptor.setProperty(runtime, enumerableKey, true);
|
|
252
|
+
}
|
|
253
|
+
if (writable) {
|
|
254
|
+
descriptor.setProperty(runtime, writableKey, true);
|
|
255
|
+
}
|
|
256
|
+
if (hasValue) {
|
|
257
|
+
descriptor.setProperty(runtime, valueKey, value);
|
|
258
|
+
}
|
|
259
|
+
defineProperty.call(runtime, object, jsi::String::createFromUtf8(runtime, nameUtf8, nameLength), descriptor);
|
|
260
|
+
return jsi::Value::undefined();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
196
264
|
inline jsi::Value callAsConstructor(jsi::IRuntime &runtime, const jsi::Function &function, const jsi::Value *_Nullable args, size_t count) {
|
|
197
265
|
return expo::CppError::tryCatch(runtime, [&] {
|
|
198
266
|
return function.callAsConstructor(runtime, args, count);
|
|
@@ -44,21 +44,34 @@ private:
|
|
|
44
44
|
|
|
45
45
|
std::atomic<int> refCount{1};
|
|
46
46
|
|
|
47
|
+
RuntimeScheduler(void *scheduler, ScheduleFn fn) noexcept
|
|
48
|
+
: nativeScheduler(scheduler), scheduleFn(fn) {}
|
|
49
|
+
|
|
50
|
+
RuntimeScheduler() noexcept {}
|
|
51
|
+
|
|
47
52
|
public:
|
|
53
|
+
// Swift creates instances through the static `create` functions rather than the constructors.
|
|
54
|
+
// Annotating the constructors with `SWIFT_RETURNS_RETAINED` silences the Swift 6.4 "cannot infer
|
|
55
|
+
// ownership" warning, but Swift 6.2 rejects that annotation on a constructor as an error. Static
|
|
56
|
+
// functions returning a shared reference accept the annotation on every Swift version.
|
|
57
|
+
|
|
48
58
|
/**
|
|
49
|
-
|
|
59
|
+
Creates a scheduler bound to a host-provided native RuntimeScheduler.
|
|
50
60
|
`scheduleTask` dispatches through `fn`, which the host implements against
|
|
51
61
|
the real react::RuntimeScheduler.
|
|
52
62
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
static RuntimeScheduler *create(void *scheduler, ScheduleFn fn) noexcept SWIFT_RETURNS_RETAINED {
|
|
64
|
+
return new RuntimeScheduler(scheduler, fn);
|
|
65
|
+
}
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
|
-
|
|
68
|
+
Creates a no-op scheduler. Scheduled tasks run synchronously on the
|
|
58
69
|
caller's thread — intended for standalone runtimes (e.g. tests) that have
|
|
59
70
|
no React scheduler.
|
|
60
71
|
*/
|
|
61
|
-
|
|
72
|
+
static RuntimeScheduler *create() noexcept SWIFT_RETURNS_RETAINED {
|
|
73
|
+
return new RuntimeScheduler();
|
|
74
|
+
}
|
|
62
75
|
|
|
63
76
|
RuntimeScheduler(const RuntimeScheduler &) = delete;
|
|
64
77
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Testing
|
|
5
|
+
|
|
6
|
+
@Suite("JavaScriptCodable+Set")
|
|
7
|
+
@JavaScriptActor
|
|
8
|
+
struct JavaScriptCodableSetTests {
|
|
9
|
+
let runtime = JavaScriptRuntime()
|
|
10
|
+
|
|
11
|
+
@Test
|
|
12
|
+
func `decodes a set of strings from a JS Set`() throws {
|
|
13
|
+
let decoded = try Set<String>.decode(runtime.eval("new Set(['a', 'b'])"), in: runtime)
|
|
14
|
+
#expect(decoded == ["a", "b"])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
func `decodes a set from a subclass of Set`() throws {
|
|
19
|
+
let decoded = try Set<Int>.decode(runtime.eval("new (class extends Set {})([1, 2])"), in: runtime)
|
|
20
|
+
#expect(decoded == [1, 2])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
func `arrayizes a non-set object into a single-element set`() throws {
|
|
25
|
+
let decoded = try Set<[String: Int]>.decode(runtime.eval("({ a: 1 })"), in: runtime)
|
|
26
|
+
#expect(decoded == [["a": 1]])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
func `decoding many sets leaves no helper on the global object`() throws {
|
|
31
|
+
let runtime = JavaScriptRuntime()
|
|
32
|
+
for _ in 0..<100 {
|
|
33
|
+
_ = try Set<Int>.decode(runtime.eval("new Set([1, 2, 3])"), in: runtime)
|
|
34
|
+
}
|
|
35
|
+
// The only object the wrapper may leave on `globalThis` is the long-lived-objects anchor.
|
|
36
|
+
let ownGlobals = try runtime.eval("Object.getOwnPropertyNames(globalThis).length").getInt()
|
|
37
|
+
let freshGlobals = try JavaScriptRuntime().eval("Object.getOwnPropertyNames(globalThis).length").getInt()
|
|
38
|
+
#expect(ownGlobals - freshGlobals <= 1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test
|
|
42
|
+
func `decodes a set from an array, collapsing duplicates`() throws {
|
|
43
|
+
let decoded = try Set<Int>.decode(runtime.eval("[1, 2, 2, 3, 1]"), in: runtime)
|
|
44
|
+
#expect(decoded == [1, 2, 3])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
func `arrayizes a non-array, non-set scalar into a set`() throws {
|
|
49
|
+
#expect(try Set<Int>.decode(runtime.eval("42"), in: runtime) == [42])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
func `encodes a set to a JS Set`() throws {
|
|
54
|
+
let encoded = try Set<Int>.encode([1, 2, 3], in: runtime)
|
|
55
|
+
#expect(encoded.is("Set"))
|
|
56
|
+
let object = encoded.getObject()
|
|
57
|
+
#expect(object.getProperty("size").getInt() == 3)
|
|
58
|
+
#expect(try object.callFunction("has", arguments: 2).getBool() == true)
|
|
59
|
+
#expect(try object.callFunction("has", arguments: 4).getBool() == false)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
func `round-trips a set of sets`() throws {
|
|
64
|
+
let value: Set<Set<String>> = [["a"], ["b", "c"]]
|
|
65
|
+
let encoded = try Set<Set<String>>.encode(value, in: runtime)
|
|
66
|
+
#expect(try Set<Set<String>>.decode(encoded, in: runtime) == value)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Test
|
|
70
|
+
func `round-trips an empty set`() throws {
|
|
71
|
+
#expect(try Set<Int>.decode(runtime.eval("new Set()"), in: runtime) == [])
|
|
72
|
+
let encoded = try Set<Int>.encode([], in: runtime)
|
|
73
|
+
#expect(encoded.is("Set"))
|
|
74
|
+
#expect(encoded.getObject().getProperty("size").getInt() == 0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
func `set decode rejects an element of the wrong type`() throws {
|
|
79
|
+
#expect(throws: (any Error).self) {
|
|
80
|
+
try Set<Int>.decode(runtime.eval("new Set([1, 'a'])"), in: runtime)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
func `set decode collapses distinct JS objects that decode to equal values`() throws {
|
|
86
|
+
// JS `Set` compares objects by reference, so it holds both objects; Swift compares the decoded
|
|
87
|
+
// dictionaries by value, so they collapse into one element.
|
|
88
|
+
let decoded = try Set<[String: Int]>.decode(runtime.eval("new Set([{ a: 1 }, { a: 1 }, { b: 2 }])"), in: runtime)
|
|
89
|
+
#expect(decoded == [["a": 1], ["b": 2]])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Test
|
|
93
|
+
func `encodes a set of dictionaries to a JS Set of distinct objects`() throws {
|
|
94
|
+
let encoded = try Set<[String: Int]>.encode([["a": 1], ["b": 2]], in: runtime)
|
|
95
|
+
runtime.global().setProperty("encodedSet", value: encoded)
|
|
96
|
+
let summary = try runtime.eval(
|
|
97
|
+
"Array.from(encodedSet, (entry) => JSON.stringify(entry)).sort().join(',')"
|
|
98
|
+
)
|
|
99
|
+
#expect(summary.getString() == #"{"a":1},{"b":2}"#)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
func `decodes a set argument and encodes a set result through a host function`() throws {
|
|
104
|
+
let fn = runtime.createFunction("uppercased") { [self] _, arguments in
|
|
105
|
+
let strings = try Set<String>.decode(arguments.unownedValue(at: 0), in: runtime)
|
|
106
|
+
return try Set<String>.encode(Set(strings.map { $0.uppercased() }), in: runtime)
|
|
107
|
+
}
|
|
108
|
+
runtime.global().setProperty("uppercased", value: fn.asValue())
|
|
109
|
+
let result = try runtime.eval(
|
|
110
|
+
"const result = uppercased(new Set(['a', 'b']))",
|
|
111
|
+
"result instanceof Set && result.size === 2 && result.has('A') && result.has('B')"
|
|
112
|
+
)
|
|
113
|
+
#expect(result.getBool() == true)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
import Foundation
|
|
5
|
+
import Testing
|
|
6
|
+
|
|
7
|
+
@Suite("JavaScriptDecodable.decodableKinds")
|
|
8
|
+
@JavaScriptActor
|
|
9
|
+
struct JavaScriptDecodableKindsTests {
|
|
10
|
+
let runtime = JavaScriptRuntime()
|
|
11
|
+
|
|
12
|
+
/// One sample value of each JS type, keyed by the source that evaluates to it.
|
|
13
|
+
static let samples = [
|
|
14
|
+
"42", "1.5", "'text'", "true", "null", "undefined", "10n", "Symbol()", "({})", "[1]", "new Uint8Array(1)",
|
|
15
|
+
"(function () {})",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
// MARK: - JavaScriptValueKinds(of:)
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
func `reads the kind of every JS type from owning and unowned values`() throws {
|
|
22
|
+
let expected: [String: JavaScriptValueKinds] = [
|
|
23
|
+
"42": .number, "1.5": .number, "'text'": .string, "true": .bool, "null": .null,
|
|
24
|
+
"undefined": .undefined, "10n": .bigint, "Symbol()": .symbol, "({})": .object, "[1]": .object,
|
|
25
|
+
"new Uint8Array(1)": .object, "(function () {})": .object,
|
|
26
|
+
]
|
|
27
|
+
for source in Self.samples {
|
|
28
|
+
let value = try runtime.eval(source)
|
|
29
|
+
#expect(JavaScriptValueKinds(of: value) == expected[source], "owning \(source)")
|
|
30
|
+
|
|
31
|
+
let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: value)
|
|
32
|
+
let unownedKind = JavaScriptValueKinds(of: buffer.unownedValue(at: 0))
|
|
33
|
+
#expect(unownedKind == expected[source], "unowned \(source)")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// MARK: - decodableKinds
|
|
38
|
+
|
|
39
|
+
/// Checks that `decodableKinds` contains the kind of exactly the `accepted` samples, and that `decode`
|
|
40
|
+
/// throws for every rejected one. A rejection must be definitive, because a union skips the case
|
|
41
|
+
/// without trying to decode it.
|
|
42
|
+
private func expectDecodableKinds<T: JavaScriptDecodable>(
|
|
43
|
+
_ type: T.Type,
|
|
44
|
+
accept accepted: Set<String>,
|
|
45
|
+
sourceLocation: SourceLocation = #_sourceLocation
|
|
46
|
+
) throws {
|
|
47
|
+
for source in Self.samples {
|
|
48
|
+
let value = try runtime.eval(source)
|
|
49
|
+
let expected = accepted.contains(source)
|
|
50
|
+
#expect(
|
|
51
|
+
T.decodableKinds.contains(JavaScriptValueKinds(of: value)) == expected,
|
|
52
|
+
"\(source)",
|
|
53
|
+
sourceLocation: sourceLocation
|
|
54
|
+
)
|
|
55
|
+
if !expected {
|
|
56
|
+
#expect(throws: (any Error).self, "decode \(source)", sourceLocation: sourceLocation) {
|
|
57
|
+
_ = try T.decode(value, in: runtime)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static let numbers: Set<String> = ["42", "1.5"]
|
|
64
|
+
static let objects: Set<String> = ["({})", "[1]", "new Uint8Array(1)", "(function () {})"]
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
func `Bool accepts only booleans`() throws {
|
|
68
|
+
try expectDecodableKinds(Bool.self, accept: ["true"])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
func `String accepts only strings`() throws {
|
|
73
|
+
try expectDecodableKinds(String.self, accept: ["'text'"])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
func `floating-point types accept only numbers`() throws {
|
|
78
|
+
try expectDecodableKinds(Double.self, accept: Self.numbers)
|
|
79
|
+
try expectDecodableKinds(Float.self, accept: Self.numbers)
|
|
80
|
+
try expectDecodableKinds(CGFloat.self, accept: Self.numbers)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
func `narrow integer types accept only numbers`() throws {
|
|
85
|
+
try expectDecodableKinds(Int8.self, accept: Self.numbers)
|
|
86
|
+
try expectDecodableKinds(Int16.self, accept: Self.numbers)
|
|
87
|
+
try expectDecodableKinds(Int32.self, accept: Self.numbers)
|
|
88
|
+
try expectDecodableKinds(UInt8.self, accept: Self.numbers)
|
|
89
|
+
try expectDecodableKinds(UInt16.self, accept: Self.numbers)
|
|
90
|
+
try expectDecodableKinds(UInt32.self, accept: Self.numbers)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
func `64-bit integer types accept numbers and bigints`() throws {
|
|
95
|
+
let accepted = Self.numbers.union(["10n"])
|
|
96
|
+
try expectDecodableKinds(Int.self, accept: accepted)
|
|
97
|
+
try expectDecodableKinds(Int64.self, accept: accepted)
|
|
98
|
+
try expectDecodableKinds(UInt.self, accept: accepted)
|
|
99
|
+
try expectDecodableKinds(UInt64.self, accept: accepted)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
func `Optional accepts null, undefined and what the wrapped type accepts`() throws {
|
|
104
|
+
try expectDecodableKinds(String?.self, accept: ["'text'", "null", "undefined"])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Test
|
|
108
|
+
func `Array accepts objects and what the element type accepts`() throws {
|
|
109
|
+
// A non-array value is decoded as a single-element array.
|
|
110
|
+
try expectDecodableKinds([String].self, accept: Self.objects.union(["'text'"]))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
func `Dictionary accepts only objects`() throws {
|
|
115
|
+
try expectDecodableKinds([String: Int].self, accept: Self.objects)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
func `Data accepts only objects`() throws {
|
|
120
|
+
try expectDecodableKinds(Data.self, accept: Self.objects)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Test
|
|
124
|
+
func `Date accepts numbers, strings and objects`() throws {
|
|
125
|
+
try expectDecodableKinds(Date.self, accept: Self.numbers.union(["'text'"]).union(Self.objects))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
func `JavaScriptValue accepts every value`() throws {
|
|
130
|
+
try expectDecodableKinds(JavaScriptValue.self, accept: Set(Self.samples))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import ExpoModulesJSI
|
|
2
|
+
import Testing
|
|
3
|
+
|
|
4
|
+
private final class CachedBox {
|
|
5
|
+
let value: Int
|
|
6
|
+
|
|
7
|
+
init(_ value: Int) {
|
|
8
|
+
self.value = value
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Suite
|
|
13
|
+
@JavaScriptActor
|
|
14
|
+
struct JavaScriptRuntimeCacheTests {
|
|
15
|
+
let runtime = JavaScriptRuntime()
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
func `creates the value on first use and reuses it afterwards`() {
|
|
19
|
+
let key = JavaScriptRuntime.Cache.Key<CachedBox>()
|
|
20
|
+
var makeCount = 0
|
|
21
|
+
|
|
22
|
+
let first = runtime.cached(key) {
|
|
23
|
+
makeCount += 1
|
|
24
|
+
return CachedBox(1)
|
|
25
|
+
}
|
|
26
|
+
let second = runtime.cached(key) {
|
|
27
|
+
makeCount += 1
|
|
28
|
+
return CachedBox(2)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#expect(makeCount == 1)
|
|
32
|
+
#expect(first === second)
|
|
33
|
+
#expect(second.value == 1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
func `keys are independent`() {
|
|
38
|
+
let firstKey = JavaScriptRuntime.Cache.Key<CachedBox>()
|
|
39
|
+
let secondKey = JavaScriptRuntime.Cache.Key<CachedBox>()
|
|
40
|
+
|
|
41
|
+
#expect(runtime.cached(firstKey) { CachedBox(1) }.value == 1)
|
|
42
|
+
#expect(runtime.cached(secondKey) { CachedBox(2) }.value == 2)
|
|
43
|
+
#expect(runtime.cached(firstKey) { CachedBox(3) }.value == 1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
func `keys can be used in any order`() {
|
|
48
|
+
let keys = (0..<16).map { _ in JavaScriptRuntime.Cache.Key<CachedBox>() }
|
|
49
|
+
|
|
50
|
+
// Using the last key first grows the storage past all the others, which must then stay empty.
|
|
51
|
+
#expect(runtime.cached(keys[15]) { CachedBox(15) }.value == 15)
|
|
52
|
+
#expect(runtime.cached(keys[3]) { CachedBox(3) }.value == 3)
|
|
53
|
+
#expect(runtime.cached(keys[15]) { CachedBox(-1) }.value == 15)
|
|
54
|
+
#expect(runtime.cached(keys[0]) { CachedBox(0) }.value == 0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
func `each runtime has its own entries`() {
|
|
59
|
+
let key = JavaScriptRuntime.Cache.Key<CachedBox>()
|
|
60
|
+
let otherRuntime = JavaScriptRuntime()
|
|
61
|
+
|
|
62
|
+
#expect(runtime.cached(key) { CachedBox(1) }.value == 1)
|
|
63
|
+
#expect(otherRuntime.cached(key) { CachedBox(2) }.value == 2)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
func `a value can be created from other cached values`() {
|
|
68
|
+
let baseKey = JavaScriptRuntime.Cache.Key<CachedBox>()
|
|
69
|
+
let derivedKey = JavaScriptRuntime.Cache.Key<CachedBox>()
|
|
70
|
+
|
|
71
|
+
// The nested lookup runs while the outer one is still creating its value.
|
|
72
|
+
let derived = runtime.cached(derivedKey) {
|
|
73
|
+
let base = runtime.cached(baseKey) { CachedBox(20) }
|
|
74
|
+
return CachedBox(base.value + 1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#expect(derived.value == 21)
|
|
78
|
+
#expect(runtime.cached(baseKey) { CachedBox(0) }.value == 20)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test
|
|
82
|
+
func `a throwing make caches nothing`() throws {
|
|
83
|
+
struct MakeError: Error {}
|
|
84
|
+
let key = JavaScriptRuntime.Cache.Key<CachedBox>()
|
|
85
|
+
|
|
86
|
+
#expect(throws: MakeError.self) {
|
|
87
|
+
try runtime.cached(key) { throw MakeError() }
|
|
88
|
+
}
|
|
89
|
+
#expect(runtime.cached(key) { CachedBox(1) }.value == 1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Test
|
|
93
|
+
func `caches JavaScript values`() throws {
|
|
94
|
+
let key = JavaScriptRuntime.Cache.Key<JavaScriptValue>()
|
|
95
|
+
|
|
96
|
+
let first = try runtime.cached(key) { try runtime.eval("({ answer: 42 })") }
|
|
97
|
+
let second = try runtime.cached(key) { try runtime.eval("({ answer: 0 })") }
|
|
98
|
+
|
|
99
|
+
#expect(first === second)
|
|
100
|
+
#expect(second.getObject().getProperty("answer").getInt() == 42)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Test
|
|
104
|
+
func `cached values are released with the runtime`() {
|
|
105
|
+
let key = JavaScriptRuntime.Cache.Key<CachedBox>()
|
|
106
|
+
weak var cachedValue: CachedBox?
|
|
107
|
+
|
|
108
|
+
do {
|
|
109
|
+
let runtime = JavaScriptRuntime()
|
|
110
|
+
cachedValue = runtime.cached(key) { CachedBox(1) }
|
|
111
|
+
#expect(cachedValue != nil)
|
|
112
|
+
withExtendedLifetime(runtime) {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#expect(cachedValue == nil)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -34,6 +34,40 @@ struct JavaScriptRuntimeTests {
|
|
|
34
34
|
#expect(fn.asValue().isFunction() == true)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
@Test
|
|
38
|
+
func `host function returns a fresh string`() throws {
|
|
39
|
+
let fn = runtime.createFunction("greet") { [runtime] _, _ in
|
|
40
|
+
return JavaScriptValue(runtime, "hello")
|
|
41
|
+
}
|
|
42
|
+
runtime.global().setProperty("greet", value: fn)
|
|
43
|
+
#expect(try runtime.eval("greet() + ' ' + greet()").getString() == "hello hello")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
func `host function returns a shared value repeatedly`() throws {
|
|
48
|
+
// The same instance is returned on every call, so the host call must copy the engine handle
|
|
49
|
+
// instead of moving it out: the captured value has to stay valid for the next call.
|
|
50
|
+
let shared = JavaScriptValue(runtime, "shared")
|
|
51
|
+
let fn = runtime.createFunction("shared") { _, _ in
|
|
52
|
+
return shared
|
|
53
|
+
}
|
|
54
|
+
runtime.global().setProperty("shared", value: fn)
|
|
55
|
+
#expect(try runtime.eval("shared() + ' ' + shared()").getString() == "shared shared")
|
|
56
|
+
#expect(shared.getString() == "shared")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
func `host function returns an object it keeps`() throws {
|
|
61
|
+
let kept = runtime.createObject()
|
|
62
|
+
kept.setProperty("answer", value: 42)
|
|
63
|
+
let fn = runtime.createFunction("kept") { _, _ in
|
|
64
|
+
return kept.asValue()
|
|
65
|
+
}
|
|
66
|
+
runtime.global().setProperty("kept", value: fn)
|
|
67
|
+
#expect(try runtime.eval("kept().answer + kept().answer").getInt() == 84)
|
|
68
|
+
#expect(kept.getProperty("answer").getInt() == 42)
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
@Test
|
|
38
72
|
func `evaluate script`() throws {
|
|
39
73
|
#expect(try runtime.eval("'hello' + ' ' + 'world'").getString() == "hello world")
|
|
@@ -58,6 +92,23 @@ struct JavaScriptRuntimeTests {
|
|
|
58
92
|
#expect(result.getString() == "hello")
|
|
59
93
|
}
|
|
60
94
|
|
|
95
|
+
@Test
|
|
96
|
+
func `evaluate to promise on the JavaScript thread when called from another thread`() async throws {
|
|
97
|
+
// The test body runs on a cooperative thread, not on the scheduler's thread. Evaluating there
|
|
98
|
+
// would run JavaScript concurrently with the work the scheduler runs, such as settling a promise.
|
|
99
|
+
let testRuntime = await TestRuntimeScheduler().makeRuntime()
|
|
100
|
+
let runtime = testRuntime.runtime
|
|
101
|
+
try await testRuntime.scheduler.runIsolated {
|
|
102
|
+
let isOnJavaScriptThread = runtime.createFunction("isOnJavaScriptThread") { _, _ in
|
|
103
|
+
return JavaScriptValue(runtime, runtime.isOnJavaScriptThread())
|
|
104
|
+
}
|
|
105
|
+
runtime.global().setProperty("isOnJavaScriptThread", value: isOnJavaScriptThread.asValue())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let result = try await runtime.evalAsync("Promise.resolve(isOnJavaScriptThread())")
|
|
109
|
+
#expect(result.getBool() == true)
|
|
110
|
+
}
|
|
111
|
+
|
|
61
112
|
@Test
|
|
62
113
|
func `is equatable`() {
|
|
63
114
|
#expect((runtime == runtime) == true)
|
|
@@ -136,11 +187,11 @@ struct JavaScriptRuntimeTests {
|
|
|
136
187
|
}
|
|
137
188
|
|
|
138
189
|
await scheduler.run {
|
|
139
|
-
|
|
190
|
+
let didRunInline = InlineRunFlag()
|
|
140
191
|
runtime.runOrSchedule {
|
|
141
|
-
didRunInline = true
|
|
192
|
+
didRunInline.value = true
|
|
142
193
|
}
|
|
143
|
-
#expect(didRunInline)
|
|
194
|
+
#expect(didRunInline.value)
|
|
144
195
|
}
|
|
145
196
|
|
|
146
197
|
await withCheckedContinuation { continuation in
|
|
@@ -217,6 +268,31 @@ struct JavaScriptRuntimeTests {
|
|
|
217
268
|
#expect(hostObject.getProperty("unknown").isUndefined())
|
|
218
269
|
}
|
|
219
270
|
|
|
271
|
+
@Test
|
|
272
|
+
func `host object getter returns strings`() throws {
|
|
273
|
+
// `fresh` is created per access and gets moved into the result; `shared` is the same instance on
|
|
274
|
+
// every access and has to be copied so it stays valid.
|
|
275
|
+
let shared = JavaScriptValue(runtime, "shared")
|
|
276
|
+
let hostObject = runtime.createHostObject(
|
|
277
|
+
get: { [runtime] name in
|
|
278
|
+
switch name {
|
|
279
|
+
case "fresh":
|
|
280
|
+
return JavaScriptValue(runtime, "fresh")
|
|
281
|
+
case "shared":
|
|
282
|
+
return shared
|
|
283
|
+
default:
|
|
284
|
+
return .undefined
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
getPropertyNames: { ["fresh", "shared"] }
|
|
288
|
+
)
|
|
289
|
+
runtime.global().setProperty("host", value: hostObject)
|
|
290
|
+
|
|
291
|
+
#expect(try runtime.eval("host.fresh + host.fresh").getString() == "freshfresh")
|
|
292
|
+
#expect(try runtime.eval("host.shared + host.shared").getString() == "sharedshared")
|
|
293
|
+
#expect(shared.getString() == "shared")
|
|
294
|
+
}
|
|
295
|
+
|
|
220
296
|
@Test
|
|
221
297
|
func `host object set property`() {
|
|
222
298
|
var storedValue: Int?
|
|
@@ -1089,6 +1165,13 @@ struct JavaScriptRuntimeTests {
|
|
|
1089
1165
|
}
|
|
1090
1166
|
}
|
|
1091
1167
|
|
|
1168
|
+
/// Records whether a `runOrSchedule` block ran. A class instead of a `nonisolated(unsafe) var` captured
|
|
1169
|
+
/// by the block, which Swift 6.2 rejects as a data race. Safe without synchronization: the test only
|
|
1170
|
+
/// reads it after the block ran inline on the same thread.
|
|
1171
|
+
private final class InlineRunFlag: @unchecked Sendable {
|
|
1172
|
+
var value = false
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1092
1175
|
/// Tasks captured by `holdSchedulerTask` instead of being executed, emulating a React
|
|
1093
1176
|
/// `RuntimeScheduler` that is torn down with work still queued (the #47716 reload scenario).
|
|
1094
1177
|
/// Safe without synchronization: the dispatch always runs synchronously on the test's thread.
|