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.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/apple/Benchmarks/ContainerCodingBenchmarks.swift +74 -0
  3. package/apple/Benchmarks/HostFunctionBenchmarks.swift +41 -0
  4. package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
  5. package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
  6. package/apple/Benchmarks/ValueAccessBenchmarks.swift +23 -0
  7. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
  8. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
  9. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
  10. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +210 -21
  11. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Set.swift +79 -0
  12. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
  13. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
  14. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
  15. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +43 -31
  16. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
  17. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
  18. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +97 -7
  19. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
  20. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
  21. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +54 -1
  22. package/apple/Sources/ExpoModulesJSI/Utilities/Errors.swift +19 -0
  23. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
  24. package/apple/Sources/ExpoModulesJSI/Utilities/UncheckedSendable.swift +13 -0
  25. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +68 -0
  26. package/apple/Sources/ExpoModulesJSI-Cxx/include/RuntimeScheduler.h +18 -5
  27. package/apple/Tests/JavaScriptCodableSetTests.swift +115 -0
  28. package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
  29. package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
  30. package/apple/Tests/JavaScriptRuntimeTests.swift +86 -3
  31. package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
  32. package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
  33. package/apple/scripts/build-xcframework.sh +15 -0
  34. 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
- Constructs a scheduler bound to a host-provided native RuntimeScheduler.
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
- SWIFT_RETURNS_RETAINED RuntimeScheduler(void *scheduler, ScheduleFn fn) noexcept
54
- : nativeScheduler(scheduler), scheduleFn(fn) {}
63
+ static RuntimeScheduler *create(void *scheduler, ScheduleFn fn) noexcept SWIFT_RETURNS_RETAINED {
64
+ return new RuntimeScheduler(scheduler, fn);
65
+ }
55
66
 
56
67
  /**
57
- Constructs a no-op scheduler. Scheduled tasks run synchronously on the
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
- SWIFT_RETURNS_RETAINED RuntimeScheduler() {}
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
- nonisolated(unsafe) var didRunInline = false
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.