expo-modules-jsi 58.0.7 → 58.0.8

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 (26) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
  3. package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
  4. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
  5. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
  6. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
  7. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +75 -0
  8. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
  9. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
  10. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
  11. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +18 -13
  12. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
  13. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
  14. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +22 -0
  15. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
  16. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
  17. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +54 -1
  18. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
  19. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +27 -0
  20. package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
  21. package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
  22. package/apple/Tests/JavaScriptRuntimeTests.swift +59 -0
  23. package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
  24. package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
  25. package/apple/scripts/build-xcframework.sh +15 -0
  26. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 58.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [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))
8
+ - [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))
9
+ - 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))
10
+ - [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))
11
+ - [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))
12
+ - [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))
13
+ - [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))
14
+
3
15
  ## 58.0.7
4
16
 
5
17
  ### Patch Changes
@@ -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
+ }
@@ -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
  }
@@ -15,6 +15,11 @@ import CoreGraphics
15
15
  // MARK: - Bool
16
16
 
17
17
  extension Bool: JavaScriptCodable {
18
+ @inlinable
19
+ public static var decodableKinds: JavaScriptValueKinds {
20
+ return .bool
21
+ }
22
+
18
23
  @JavaScriptActor
19
24
  @inlinable
20
25
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Bool
@@ -40,6 +45,11 @@ extension Bool: JavaScriptCodable {
40
45
  // MARK: - String
41
46
 
42
47
  extension String: JavaScriptCodable {
48
+ @inlinable
49
+ public static var decodableKinds: JavaScriptValueKinds {
50
+ return .string
51
+ }
52
+
43
53
  @JavaScriptActor
44
54
  @inlinable
45
55
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
@@ -74,6 +84,11 @@ extension String: JavaScriptCodable {
74
84
  // undefined behavior — a native crash, not a catchable error.
75
85
 
76
86
  extension Double: JavaScriptCodable {
87
+ @inlinable
88
+ public static var decodableKinds: JavaScriptValueKinds {
89
+ return .number
90
+ }
91
+
77
92
  @JavaScriptActor
78
93
  @inlinable
79
94
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
@@ -100,6 +115,11 @@ extension Double: JavaScriptCodable {
100
115
  }
101
116
 
102
117
  extension Float: JavaScriptCodable {
118
+ @inlinable
119
+ public static var decodableKinds: JavaScriptValueKinds {
120
+ return .number
121
+ }
122
+
103
123
  @JavaScriptActor
104
124
  @inlinable
105
125
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Float
@@ -123,6 +143,11 @@ extension Float: JavaScriptCodable {
123
143
  }
124
144
 
125
145
  extension CGFloat: JavaScriptCodable {
146
+ @inlinable
147
+ public static var decodableKinds: JavaScriptValueKinds {
148
+ return .number
149
+ }
150
+
126
151
  @JavaScriptActor
127
152
  @inlinable
128
153
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
@@ -170,6 +195,11 @@ extension CGFloat: JavaScriptCodable {
170
195
  // back, and a JS caller may pass either form.
171
196
 
172
197
  extension Int: JavaScriptCodable {
198
+ @inlinable
199
+ public static var decodableKinds: JavaScriptValueKinds {
200
+ return [.number, .bigint]
201
+ }
202
+
173
203
  @JavaScriptActor
174
204
  @inlinable
175
205
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int {
@@ -192,6 +222,11 @@ extension Int: JavaScriptCodable {
192
222
  }
193
223
 
194
224
  extension Int8: JavaScriptCodable {
225
+ @inlinable
226
+ public static var decodableKinds: JavaScriptValueKinds {
227
+ return .number
228
+ }
229
+
195
230
  @JavaScriptActor
196
231
  @inlinable
197
232
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int8
@@ -215,6 +250,11 @@ extension Int8: JavaScriptCodable {
215
250
  }
216
251
 
217
252
  extension Int16: JavaScriptCodable {
253
+ @inlinable
254
+ public static var decodableKinds: JavaScriptValueKinds {
255
+ return .number
256
+ }
257
+
218
258
  @JavaScriptActor
219
259
  @inlinable
220
260
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int16
@@ -238,6 +278,11 @@ extension Int16: JavaScriptCodable {
238
278
  }
239
279
 
240
280
  extension Int32: JavaScriptCodable {
281
+ @inlinable
282
+ public static var decodableKinds: JavaScriptValueKinds {
283
+ return .number
284
+ }
285
+
241
286
  @JavaScriptActor
242
287
  @inlinable
243
288
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int32
@@ -261,6 +306,11 @@ extension Int32: JavaScriptCodable {
261
306
  }
262
307
 
263
308
  extension Int64: JavaScriptCodable {
309
+ @inlinable
310
+ public static var decodableKinds: JavaScriptValueKinds {
311
+ return [.number, .bigint]
312
+ }
313
+
264
314
  @JavaScriptActor
265
315
  @inlinable
266
316
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Int64
@@ -284,6 +334,11 @@ extension Int64: JavaScriptCodable {
284
334
  }
285
335
 
286
336
  extension UInt: JavaScriptCodable {
337
+ @inlinable
338
+ public static var decodableKinds: JavaScriptValueKinds {
339
+ return [.number, .bigint]
340
+ }
341
+
287
342
  @JavaScriptActor
288
343
  @inlinable
289
344
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> UInt
@@ -307,6 +362,11 @@ extension UInt: JavaScriptCodable {
307
362
  }
308
363
 
309
364
  extension UInt8: JavaScriptCodable {
365
+ @inlinable
366
+ public static var decodableKinds: JavaScriptValueKinds {
367
+ return .number
368
+ }
369
+
310
370
  @JavaScriptActor
311
371
  @inlinable
312
372
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> UInt8
@@ -330,6 +390,11 @@ extension UInt8: JavaScriptCodable {
330
390
  }
331
391
 
332
392
  extension UInt16: JavaScriptCodable {
393
+ @inlinable
394
+ public static var decodableKinds: JavaScriptValueKinds {
395
+ return .number
396
+ }
397
+
333
398
  @JavaScriptActor
334
399
  @inlinable
335
400
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
@@ -356,6 +421,11 @@ extension UInt16: JavaScriptCodable {
356
421
  }
357
422
 
358
423
  extension UInt32: JavaScriptCodable {
424
+ @inlinable
425
+ public static var decodableKinds: JavaScriptValueKinds {
426
+ return .number
427
+ }
428
+
359
429
  @JavaScriptActor
360
430
  @inlinable
361
431
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
@@ -382,6 +452,11 @@ extension UInt32: JavaScriptCodable {
382
452
  }
383
453
 
384
454
  extension UInt64: JavaScriptCodable {
455
+ @inlinable
456
+ public static var decodableKinds: JavaScriptValueKinds {
457
+ return [.number, .bigint]
458
+ }
459
+
385
460
  @JavaScriptActor
386
461
  @inlinable
387
462
  public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
@@ -13,9 +13,10 @@
13
13
  public protocol JavaScriptDecodable {
14
14
  /// Decodes an owning `JavaScriptValue` into `Self`.
15
15
  ///
16
- /// This overload is the protocol requirement: every conformer provides it. It is also the
17
- /// correct entry point for any value that must outlive the call (stored, captured, handed to a
18
- /// `Promise`).
16
+ /// Defaulted (see the extension below) to borrow the value and decode it through the
17
+ /// `JavaScriptUnownedValue` overload, so a conformer may implement just that one. A conformer must
18
+ /// implement at least one of the two overloads: each default forwards to the other, so with neither
19
+ /// implemented a decode recurses until the stack overflows.
19
20
  @JavaScriptActor
20
21
  static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Self
21
22
 
@@ -24,13 +25,35 @@ public protocol JavaScriptDecodable {
24
25
  ///
25
26
  /// This is the argument-decode fast path: the value borrows the argument the
26
27
  /// `JavaScriptValuesBuffer` already owns for the duration of the call. It is defaulted (see the
27
- /// extension below), so conformers get it for free by materializing an owning value; types that
28
- /// can read straight from the borrowed value override it to stay zero-copy.
28
+ /// extension below) to materialize an owning value and decode that, for conformers that implement
29
+ /// only the owning overload; types that can read straight from the borrowed value implement it to
30
+ /// stay zero-copy.
29
31
  @JavaScriptActor
30
32
  static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws -> Self
33
+
34
+ /// The kinds of JavaScript value `decode` can accept.
35
+ ///
36
+ /// A kind outside the set is definitive: `decode` would throw for a value of that kind. A kind in the
37
+ /// set only means the type may decode it, so `decode` can still throw, for example for an
38
+ /// out-of-range number or a missing record field. Code that picks between several types, like a
39
+ /// union, reads it to skip the ones that can't match without paying for a thrown error. Defaults to
40
+ /// `.all`, so a type that doesn't declare it is always tried.
41
+ static var decodableKinds: JavaScriptValueKinds { get }
31
42
  }
32
43
 
33
44
  extension JavaScriptDecodable {
45
+ /// Default owning decode: borrow the value and forward to the unowned overload, so a conformer that
46
+ /// implements only that one decodes owning values without a copy.
47
+ @JavaScriptActor
48
+ @inlinable
49
+ public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws -> Self
50
+ {
51
+ let runtime = copy runtime
52
+ return try value.withUnownedValue(in: runtime) { unownedValue in
53
+ return try decode(unownedValue, in: runtime)
54
+ }
55
+ }
56
+
34
57
  /// Default fast-path implementation: materialize an owning value and forward to the requirement.
35
58
  /// This is the "explicitly copy" behavior for types that cannot (or need not) read directly from
36
59
  /// the borrowed value.
@@ -44,4 +67,10 @@ extension JavaScriptDecodable {
44
67
  {
45
68
  return try decode(value.copied(in: runtime), in: runtime)
46
69
  }
70
+
71
+ /// Default: every kind, so a conformer that doesn't declare its kinds is always tried.
72
+ @inlinable
73
+ public static var decodableKinds: JavaScriptValueKinds {
74
+ return .all
75
+ }
47
76
  }