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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 58.0.9
4
+
5
+ ### Patch Changes
6
+
7
+ - [iOS] Add a `JavaScriptCodable` conformance for `Set`: it encodes to a JS `Set` and decodes from a JS `Set` or an array, collapsing duplicate elements. ([#50859](https://github.com/expo/expo/pull/50859) by [@tsapeta](https://github.com/tsapeta))
8
+ - `JavaScriptRuntime.evalAsync` now evaluates on the JavaScript thread when called from another thread on a runtime with a scheduler, instead of on the calling thread. ([#51015](https://github.com/expo/expo/pull/51015) by [@tsapeta](https://github.com/tsapeta))
9
+ - [iOS] Speed up `JavaScriptObject.defineProperty` by looking up `Object.defineProperty` once per runtime and building the descriptor in C++. ([#51004](https://github.com/expo/expo/pull/51004) by [@tsapeta](https://github.com/tsapeta))
10
+ - [iOS] Fix the xcframework failing to build with Xcode 26 (Swift 6.2): `RuntimeScheduler` constructors annotated with `SWIFT_RETURNS_RETAINED` were rejected, and host function and host object getter callbacks failed with `sending '...' risks causing data races`. ([#51040](https://github.com/expo/expo/pull/51040) by [@tsapeta](https://github.com/tsapeta))
11
+ - [iOS] Speed up integer arguments and results of native functions by running the integer conversions specialized inside `ExpoModulesJSI` instead of unspecialized in the calling module. A host function that adds two `Int`s runs about 7× faster. ([#51055](https://github.com/expo/expo/pull/51055) by [@tsapeta](https://github.com/tsapeta))
12
+
13
+ ## 58.0.8
14
+
15
+ ### Patch Changes
16
+
17
+ - [iOS] Add `decodableKinds` to `JavaScriptDecodable`: the kinds of JavaScript value (`JavaScriptValueKinds`) that `decode` can accept, so code that picks between several types can skip the ones that can't match. ([#50905](https://github.com/expo/expo/pull/50905) by [@tsapeta](https://github.com/tsapeta))
18
+ - [iOS] Fix crash reports symbolicated on the device showing no function names for `ExpoModulesJSI` frames. ([#50698](https://github.com/expo/expo/pull/50698) by [@tsapeta](https://github.com/tsapeta))
19
+ - Return strings, objects and arrays from host functions and host object getters without cloning the engine handle, and build short ASCII strings from JS inline. ([#50937](https://github.com/expo/expo/pull/50937) by [@tsapeta](https://github.com/tsapeta))
20
+ - [iOS] Add `JavaScriptRuntime.cached(_:_:)` with typed `JavaScriptRuntime.Cache.Key`s, to create a value once per runtime and reuse it, for example a JavaScript constructor or a property name. A lookup reads one array slot, about 5× faster than the string-keyed `JavaScriptPropNameID.cached(_:_:)`. ([#50888](https://github.com/expo/expo/pull/50888) by [@tsapeta](https://github.com/tsapeta))
21
+ - [iOS] Add `JavaScriptValue.withUnownedValue(in:_:)`, and give the owning `JavaScriptDecodable.decode` a default that borrows the value and decodes it through the `JavaScriptUnownedValue` overload, so a conformer can implement only that one. Arrays, dictionaries, dates, records and enums now decode unowned values without copying them first. ([#50960](https://github.com/expo/expo/pull/50960) by [@tsapeta](https://github.com/tsapeta))
22
+ - [iOS] Add `JavaScriptArray.mapUnowned(_:)`, `JavaScriptObject.withUnownedProperty(_:_:)`, and `isArray()` and `getArray(in:)` on `JavaScriptUnownedValue`. Arrays, dictionaries and dates now decode through their unowned overload without copying the value or wrapping each element in a `JavaScriptValue`, and their owning decodes forward to it. ([#50980](https://github.com/expo/expo/pull/50980) by [@tsapeta](https://github.com/tsapeta))
23
+ - [iOS] Keep checkout paths out of the Swift compilation cache key so modules importing ExpoModulesCore can reuse cached compilation results across checkouts and worktrees. ([#50354](https://github.com/expo/expo/pull/50354) by [@janicduplessis](https://github.com/janicduplessis))
24
+
3
25
  ## 58.0.7
4
26
 
5
27
  ### Patch Changes
@@ -0,0 +1,74 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Testing
5
+
6
+ /// Benchmarks comparing the `Array` and `Set` `JavaScriptCodable` conformances. `Set` decoding from a
7
+ /// JS array shares the `Array` fast path and adds only the hashing, while decoding from a JS `Set` and
8
+ /// encoding to one go through the global `Array.from` and `Set` constructor, so each case is measured
9
+ /// against its `Array` counterpart at a small and a large element count.
10
+ extension JSIBenchmarks {
11
+ // MARK: - Decode
12
+
13
+ @Test(arguments: [10, 1000])
14
+ func `decode an array of ints as Array`(count: Int) async throws {
15
+ try await benchmarkCase { runtime in
16
+ let value = try runtime.eval("Array.from({ length: \(count) }, (_, i) => i)")
17
+ try benchmark("[Int].decode: JS array of \(count)", runtime: runtime) { iterations in
18
+ for _ in 0..<iterations {
19
+ _ = try [Int].decode(value, in: runtime)
20
+ }
21
+ }
22
+ }
23
+ }
24
+
25
+ @Test(arguments: [10, 1000])
26
+ func `decode an array of ints as Set`(count: Int) async throws {
27
+ try await benchmarkCase { runtime in
28
+ let value = try runtime.eval("Array.from({ length: \(count) }, (_, i) => i)")
29
+ try benchmark("Set<Int>.decode: JS array of \(count)", runtime: runtime) { iterations in
30
+ for _ in 0..<iterations {
31
+ _ = try Set<Int>.decode(value, in: runtime)
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ @Test(arguments: [10, 1000])
38
+ func `decode a set of ints as Set`(count: Int) async throws {
39
+ try await benchmarkCase { runtime in
40
+ let value = try runtime.eval("new Set(Array.from({ length: \(count) }, (_, i) => i))")
41
+ try benchmark("Set<Int>.decode: JS Set of \(count)", runtime: runtime) { iterations in
42
+ for _ in 0..<iterations {
43
+ _ = try Set<Int>.decode(value, in: runtime)
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ // MARK: - Encode
50
+
51
+ @Test(arguments: [10, 1000])
52
+ func `encode an Array of ints`(count: Int) async throws {
53
+ try await benchmarkCase { runtime in
54
+ let array = Array(0..<count)
55
+ try benchmark("[Int].encode: \(count) elements", runtime: runtime) { iterations in
56
+ for _ in 0..<iterations {
57
+ _ = try [Int].encode(array, in: runtime)
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ @Test(arguments: [10, 1000])
64
+ func `encode a Set of ints`(count: Int) async throws {
65
+ try await benchmarkCase { runtime in
66
+ let set = Set(0..<count)
67
+ try benchmark("Set<Int>.encode: \(count) elements", runtime: runtime) { iterations in
68
+ for _ in 0..<iterations {
69
+ _ = try Set<Int>.encode(set, in: runtime)
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
@@ -87,6 +87,47 @@ extension JSIBenchmarks {
87
87
  }
88
88
  }
89
89
 
90
+ // The integer codables, as bindings call them: `Int.decode` for an `Int` argument and `Int?.decode`
91
+ // for an optional one.
92
+
93
+ @Test
94
+ func `host function adding two Ints`() async throws {
95
+ try await benchmarkCase { runtime in
96
+ let fn = runtime.createFunction("add") {
97
+ [runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
98
+ let a = try Int.decode(arguments.unownedValue(at: 0), in: runtime)
99
+ let b = try Int.decode(arguments.unownedValue(at: 1), in: runtime)
100
+ return try Int.encode(a + b, in: runtime)
101
+ }
102
+ runtime.global().setProperty("benchFn", value: fn)
103
+ let driver = try runtime.eval(
104
+ "(function(n) { for (var i = 0; i < n; i++) benchFn(3, 38); })"
105
+ ).getFunction()
106
+ try benchmark("host function: add two Ints", runtime: runtime) { iterations in
107
+ _ = try driver.call(arguments: iterations)
108
+ }
109
+ }
110
+ }
111
+
112
+ @Test
113
+ func `host function adding two optional Ints`() async throws {
114
+ try await benchmarkCase { runtime in
115
+ let fn = runtime.createFunction("add") {
116
+ [runtime] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
117
+ let a = try Int?.decode(arguments.unownedValue(at: 0), in: runtime) ?? 0
118
+ let b = try Int?.decode(arguments.unownedValue(at: 1), in: runtime) ?? 0
119
+ return try Int.encode(a + b, in: runtime)
120
+ }
121
+ runtime.global().setProperty("benchFn", value: fn)
122
+ let driver = try runtime.eval(
123
+ "(function(n) { for (var i = 0; i < n; i++) benchFn(3, 38); })"
124
+ ).getFunction()
125
+ try benchmark("host function: add two optional Ints", runtime: runtime) { iterations in
126
+ _ = try driver.call(arguments: iterations)
127
+ }
128
+ }
129
+ }
130
+
90
131
  // MARK: - Unowned-this form with string arguments
91
132
 
92
133
  @Test
@@ -0,0 +1,71 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Testing
5
+
6
+ private let answerPropNameKey = JavaScriptRuntime.Cache.Key<JavaScriptPropNameID>()
7
+
8
+ /// Benchmarks for values cached per runtime with ``JavaScriptRuntime/cached(_:_:)``, read against
9
+ /// the string-keyed ``JavaScriptPropNameID/cached(_:_:)`` registry.
10
+ extension JSIBenchmarks {
11
+ @Test
12
+ func `runtime cache hit`() async throws {
13
+ try await benchmarkCase { runtime in
14
+ _ = runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") }
15
+ try benchmark("JavaScriptRuntime.cached(_:_:): hit", runtime: runtime) { iterations in
16
+ for _ in 0..<iterations {
17
+ _ = runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") }
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ @Test
24
+ func `object property by runtime-cached PropNameID`() async throws {
25
+ try await benchmarkCase { runtime in
26
+ let object = try runtime.eval("({ answer: 42 })").getObject()
27
+ try benchmark("JavaScriptObject.getProperty(_:): runtime-cached PropNameID", runtime: runtime) { iterations in
28
+ for _ in 0..<iterations {
29
+ _ = object.getProperty(runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") })
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ @Test
36
+ func `string-keyed PropNameID cache hit`() async throws {
37
+ try await benchmarkCase { runtime in
38
+ try benchmark("JavaScriptPropNameID.cached(_:_:): hit", runtime: runtime) { iterations in
39
+ for _ in 0..<iterations {
40
+ _ = JavaScriptPropNameID.cached(runtime, "answer")
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ @Test
47
+ func `object property by string-keyed cached PropNameID`() async throws {
48
+ try await benchmarkCase { runtime in
49
+ let object = try runtime.eval("({ answer: 42 })").getObject()
50
+ try benchmark("JavaScriptObject.getProperty(_:): string-cached PropNameID", runtime: runtime) { iterations in
51
+ for _ in 0..<iterations {
52
+ _ = object.getProperty(.cached(runtime, "answer"))
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ @Test
59
+ func `object property by prebuilt PropNameID`() async throws {
60
+ try await benchmarkCase { runtime in
61
+ let object = try runtime.eval("({ answer: 42 })").getObject()
62
+ let propName = JavaScriptPropNameID(runtime, string: "answer")
63
+ try benchmark("JavaScriptObject.getProperty(_:): prebuilt PropNameID", runtime: runtime) { iterations in
64
+ for _ in 0..<iterations {
65
+ _ = object.getProperty(propName)
66
+ }
67
+ }
68
+ withExtendedLifetime(propName) {}
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,121 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Foundation
5
+ import Testing
6
+
7
+ /// Decoding from a borrowed `JavaScriptUnownedValue`, the path a `@JS` argument takes, and from an owning
8
+ /// `JavaScriptValue`, the path a nested value takes. A type without its own unowned decode copies the
9
+ /// value into an owning `JavaScriptValue` first.
10
+ extension JSIBenchmarks {
11
+ @Test
12
+ func `unowned decode of an array`() async throws {
13
+ try await benchmarkCase { runtime in
14
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("[1, 2, 3, 4, 5, 6, 7, 8]"))
15
+ try benchmark("[Double].decode(unowned): 8 elements", runtime: runtime) { iterations in
16
+ for _ in 0..<iterations {
17
+ _ = try [Double].decode(buffer.unownedValue(at: 0), in: runtime)
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ @Test
24
+ func `unowned decode of a dictionary`() async throws {
25
+ try await benchmarkCase { runtime in
26
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("({ a: 1, b: 2, c: 3, d: 4 })"))
27
+ try benchmark("[String: Double].decode(unowned): 4 entries", runtime: runtime) { iterations in
28
+ for _ in 0..<iterations {
29
+ _ = try [String: Double].decode(buffer.unownedValue(at: 0), in: runtime)
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ @Test
36
+ func `unowned decode of a date`() async throws {
37
+ try await benchmarkCase { runtime in
38
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("1700000000000"))
39
+ try benchmark("Date.decode(unowned): number", runtime: runtime) { iterations in
40
+ for _ in 0..<iterations {
41
+ _ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ @Test
48
+ func `owning decode of an array`() async throws {
49
+ try await benchmarkCase { runtime in
50
+ let value = try runtime.eval("[1, 2, 3, 4, 5, 6, 7, 8]")
51
+ try benchmark("[Double].decode(owning): 8 elements", runtime: runtime) { iterations in
52
+ for _ in 0..<iterations {
53
+ _ = try [Double].decode(value, in: runtime)
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ @Test
60
+ func `owning decode of a dictionary`() async throws {
61
+ try await benchmarkCase { runtime in
62
+ let value = try runtime.eval("({ a: 1, b: 2, c: 3, d: 4 })")
63
+ try benchmark("[String: Double].decode(owning): 4 entries", runtime: runtime) { iterations in
64
+ for _ in 0..<iterations {
65
+ _ = try [String: Double].decode(value, in: runtime)
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ @Test
72
+ func `owning decode of nested arrays`() async throws {
73
+ try await benchmarkCase { runtime in
74
+ let value = try runtime.eval("[[1, 2], [3, 4], [5, 6], [7, 8]]")
75
+ try benchmark("[[Double]].decode(owning): 4 x 2 elements", runtime: runtime) { iterations in
76
+ for _ in 0..<iterations {
77
+ _ = try [[Double]].decode(value, in: runtime)
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ @Test
84
+ func `unowned decode of nested arrays`() async throws {
85
+ try await benchmarkCase { runtime in
86
+ let buffer = JavaScriptValuesBuffer.allocate(
87
+ in: runtime,
88
+ with: try runtime.eval("[[1, 2], [3, 4], [5, 6], [7, 8]]")
89
+ )
90
+ try benchmark("[[Double]].decode(unowned): 4 x 2 elements", runtime: runtime) { iterations in
91
+ for _ in 0..<iterations {
92
+ _ = try [[Double]].decode(buffer.unownedValue(at: 0), in: runtime)
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ @Test
99
+ func `unowned decode of a date string`() async throws {
100
+ try await benchmarkCase { runtime in
101
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("'2026-10-02T12:00:00Z'"))
102
+ try benchmark("Date.decode(unowned): ISO string", runtime: runtime) { iterations in
103
+ for _ in 0..<iterations {
104
+ _ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ @Test
111
+ func `unowned decode of a date object`() async throws {
112
+ try await benchmarkCase { runtime in
113
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("new Date(1700000000000)"))
114
+ try benchmark("Date.decode(unowned): Date object", runtime: runtime) { iterations in
115
+ for _ in 0..<iterations {
116
+ _ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
@@ -129,4 +129,27 @@ extension JSIBenchmarks {
129
129
  }
130
130
  }
131
131
  }
132
+
133
+ @Test
134
+ func `define a property`() async throws {
135
+ try await benchmarkCase { runtime in
136
+ try benchmark("JavaScriptObject.defineProperty(_:value:options:)", runtime: runtime) { iterations in
137
+ for index in 0..<iterations {
138
+ let object = runtime.createObject()
139
+ object.defineProperty("__expo_shared_object_id__", value: index, options: [.writable])
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ @Test
146
+ func `create an object`() async throws {
147
+ try await benchmarkCase { runtime in
148
+ try benchmark("JavaScriptRuntime.createObject()", runtime: runtime) { iterations in
149
+ for _ in 0..<iterations {
150
+ _ = runtime.createObject()
151
+ }
152
+ }
153
+ }
154
+ }
132
155
  }
@@ -10,19 +10,36 @@
10
10
  // MARK: - Array
11
11
 
12
12
  extension Array: JavaScriptDecodable where Element: JavaScriptDecodable {
13
+ // A non-array value is decoded as a single-element array, so it's accepted when the element is.
14
+ @inlinable
15
+ public static var decodableKinds: JavaScriptValueKinds {
16
+ return Element.decodableKinds.union(.object)
17
+ }
18
+
13
19
  @JavaScriptActor
14
20
  @inlinable
15
21
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
16
22
  -> [Element]
23
+ {
24
+ // Forwards to the unowned overload, which holds the implementation.
25
+ let runtime = copy runtime
26
+ return try value.withUnownedValue(in: runtime) { unownedValue in
27
+ return try decode(unownedValue, in: runtime)
28
+ }
29
+ }
30
+
31
+ @JavaScriptActor
32
+ @inlinable
33
+ public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws
34
+ -> [Element]
17
35
  {
18
36
  // A non-array value is "arrayized" into a single-element array, so a caller that passes a
19
37
  // scalar where an array is expected still works.
20
38
  guard value.isArray() else {
21
39
  return [try Element.decode(value, in: runtime)]
22
40
  }
23
- // `map` reads the length once and uses the unchecked element accessor, avoiding a
24
- // per-element weak-runtime load and bounds check on this hot path.
25
- return try value.getArray().map { element in
41
+ // Each element is lent out unowned, so it's decoded without a `JavaScriptValue` per element.
42
+ return try value.getArray(in: runtime).mapUnowned { element in
26
43
  return try Element.decode(element, in: runtime)
27
44
  }
28
45
  }
@@ -45,6 +62,11 @@ extension Array: JavaScriptEncodable where Element: JavaScriptEncodable {
45
62
  // MARK: - Optional
46
63
 
47
64
  extension Optional: JavaScriptDecodable where Wrapped: JavaScriptDecodable {
65
+ @inlinable
66
+ public static var decodableKinds: JavaScriptValueKinds {
67
+ return Wrapped.decodableKinds.union([.null, .undefined])
68
+ }
69
+
48
70
  // Optional copies nothing itself, so it overrides the zero-copy overload too and forwards the
49
71
  // borrowed value straight through — a wrapped primitive argument stays fully zero-copy.
50
72
  @JavaScriptActor
@@ -87,22 +109,42 @@ extension Optional: JavaScriptEncodable where Wrapped: JavaScriptEncodable {
87
109
  // MARK: - Dictionary
88
110
 
89
111
  extension Dictionary: JavaScriptDecodable where Key == String, Value: JavaScriptDecodable {
112
+ @inlinable
113
+ public static var decodableKinds: JavaScriptValueKinds {
114
+ return .object
115
+ }
116
+
90
117
  @JavaScriptActor
91
118
  @inlinable
92
119
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
93
120
  -> [String: Value]
94
121
  {
95
- let object = try value.asObject()
122
+ // Forwards to the unowned overload, which holds the implementation.
123
+ let runtime = copy runtime
124
+ return try value.withUnownedValue(in: runtime) { unownedValue in
125
+ return try decode(unownedValue, in: runtime)
126
+ }
127
+ }
128
+
129
+ @JavaScriptActor
130
+ @inlinable
131
+ public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws
132
+ -> [String: Value]
133
+ {
134
+ // Reads the object straight from the borrowed value and lends each property out unowned, so it's
135
+ // decoded without a `JavaScriptValue` per property.
136
+ let object = try value.asObject(in: runtime)
96
137
  let keys = object.getPropertyNames()
97
138
  var result = [String: Value](minimumCapacity: keys.count)
98
139
  for key in keys {
99
- let property = object.getProperty(key)
100
- // Treat an `undefined`-valued property as an absent entry. Without this a non-optional
101
- // `Value` would reject an object that simply omits the property as `undefined`.
102
- if property.isUndefined() {
103
- continue
140
+ let decoded: Value? = try object.withUnownedProperty(key) { property in
141
+ // Treat an `undefined`-valued property as an absent entry. Without this a non-optional
142
+ // `Value` would reject an object that simply omits the property as `undefined`.
143
+ return property.isUndefined() ? nil : try Value.decode(property, in: runtime)
144
+ }
145
+ if let decoded {
146
+ result[key] = decoded
104
147
  }
105
- result[key] = try Value.decode(property, in: runtime)
106
148
  }
107
149
  return result
108
150
  }
@@ -9,6 +9,11 @@ import Foundation
9
9
  // read/write rather than a single accessor.
10
10
 
11
11
  extension Data: JavaScriptCodable {
12
+ @inlinable
13
+ public static var decodableKinds: JavaScriptValueKinds {
14
+ return .object
15
+ }
16
+
12
17
  @JavaScriptActor
13
18
  @inlinable
14
19
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Data
@@ -9,22 +9,44 @@ import Foundation
9
9
  // A `Date` is an absolute instant with no timezone/calendar; resolution is milliseconds.
10
10
 
11
11
  extension Date: JavaScriptCodable {
12
+ @inlinable
13
+ public static var decodableKinds: JavaScriptValueKinds {
14
+ return [.number, .string, .object]
15
+ }
16
+
12
17
  @JavaScriptActor
13
18
  @inlinable
14
19
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Date
15
20
  {
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.
21
+ // Forwards to the unowned overload, which holds the implementation.
22
+ let runtime = copy runtime
23
+ return try value.withUnownedValue(in: runtime) { unownedValue in
24
+ return try decode(unownedValue, in: runtime)
25
+ }
26
+ }
27
+
28
+ @JavaScriptActor
29
+ @inlinable
30
+ public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws
31
+ -> Date
32
+ {
33
+ // The cheap tag checks come before the `instanceof` check, which needs a global lookup; a number or
34
+ // a string can't be a `Date`, so the order is behavior-neutral. Only the string branch copies the
35
+ // value, since the `Date` constructor takes owning arguments; a string is the rare input.
18
36
  if value.isNumber() {
19
37
  return try dateFromMilliseconds(value.getDouble())
20
38
  }
21
39
  if value.isString() {
22
40
  let dateConstructor = try runtime.global().getPropertyAsFunction("Date")
23
- let constructed = try dateConstructor.callAsConstructor(value.copy()).asObject()
41
+ let constructed = try dateConstructor.callAsConstructor(value.copied(in: runtime)).asObject()
24
42
  return try dateFromMilliseconds(constructed.callFunction("getTime").asDouble())
25
43
  }
26
- if value.is("Date") {
27
- return try dateFromMilliseconds(value.asObject().callFunction("getTime").asDouble())
44
+ if value.isObject() {
45
+ let dateConstructor = try runtime.global().getPropertyAsFunction("Date")
46
+ let object = value.getObject(in: runtime)
47
+ if object.instanceOf(dateConstructor) {
48
+ return try dateFromMilliseconds(object.callFunction("getTime").asDouble())
49
+ }
28
50
  }
29
51
  throw InvalidDateException()
30
52
  }