expo-modules-jsi 58.0.6 → 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 (29) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/apple/Benchmarks/FreeFormBenchmarks.swift +61 -0
  3. package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
  4. package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
  5. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Any.swift +500 -0
  6. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
  7. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
  8. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
  9. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +75 -0
  10. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
  11. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
  12. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
  13. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +18 -13
  14. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
  15. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
  16. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +30 -0
  17. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
  18. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
  19. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +64 -2
  20. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
  21. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +27 -0
  22. package/apple/Tests/JavaScriptCodableAnyTests.swift +531 -0
  23. package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
  24. package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
  25. package/apple/Tests/JavaScriptRuntimeTests.swift +59 -0
  26. package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
  27. package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
  28. package/apple/scripts/build-xcframework.sh +15 -0
  29. package/package.json +1 -1
@@ -0,0 +1,500 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import Foundation
4
+
5
+ // Conversions for free-form values: `Any`, `[Any]`, `[String: Any]`, and types nested from them.
6
+ //
7
+ // `Any` can't conform to `JavaScriptDecodable`/`JavaScriptEncodable` (it isn't a nominal type), and
8
+ // the container conformances require their elements to conform, so none of these types have a
9
+ // `decode`/`encode` of their own. These static methods fill that gap. The three common shapes each
10
+ // have a dedicated decode and encode pair that reads or writes the container directly. Any other
11
+ // shape that holds `Any` (`[[Any]]`, `[String: [Any]]?`, …) goes through the generic
12
+ // `decodeAny(_:as:in:)` and `encodeAny(_:in:)`, which convert the whole value and cast it.
13
+ //
14
+ // Decoding turns numbers into `Double`, drops `undefined` object properties, and turns `null` and
15
+ // `undefined` into `nil` boxed in `Any`, so a decoded value casts to optional types at any depth.
16
+ // A `Date` decodes as `Date`, a `Set` as an array of its elements and a `Map` with string keys as a
17
+ // dictionary. It differs from the deprecated `getAny()` in two ways: `getAny()` uses `NSNull` for
18
+ // `null`, and it silently turns values it can't represent into `NSNull`. Here, a bigint becomes
19
+ // `Int64` (throwing when it doesn't fit), and a function, symbol, typed array, `ArrayBuffer`,
20
+ // `DataView`, `WeakMap`, `WeakSet` or `Map` with a non-string key anywhere in the value throws
21
+ // `TypeError`, as does a value nested deeper than `maxFreeFormDepth` (usually a cycle). Any other
22
+ // object decodes from its enumerable own properties only, so data it keeps elsewhere is not
23
+ // decoded: an `Error` loses its `message` and `stack`, and a class instance loses values it exposes
24
+ // through getters.
25
+
26
+ extension JavaScriptValue {
27
+ // MARK: - Decoding
28
+
29
+ /// Decodes a JavaScript value into a free-form native value: `Bool`, `Double`, `String`, `Int64`
30
+ /// (from a bigint), `Date`, `[Any]`, `[String: Any]`, or `nil` (boxed in `Any`) for `null` and
31
+ /// `undefined`. A `Set` decodes as an array of its elements and a `Map` with string keys as a
32
+ /// dictionary. Any other object decodes from its enumerable own properties only, so an `Error`, for
33
+ /// example, decodes without its `message` and `stack`.
34
+ ///
35
+ /// Throws `TypeError` for a function, symbol, typed array, `ArrayBuffer`, `DataView`, `WeakMap`,
36
+ /// `WeakSet` or `Map` with a non-string key anywhere in the value, or for a value nested more than
37
+ /// 64 levels deep (such as one that contains itself). The error names the path to the failing value.
38
+ /// Throws `BigIntConversionError` for a bigint outside the `Int64` range.
39
+ @JavaScriptActor
40
+ public static func decodeAny(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
41
+ -> Any
42
+ {
43
+ return try freeFormValue(of: value, depth: 0, in: runtime)
44
+ }
45
+
46
+ /// Decodes a borrowed JavaScript value into a free-form native value. See ``decodeAny(_:in:)``.
47
+ @JavaScriptActor
48
+ public static func decodeAny(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime)
49
+ throws -> Any
50
+ {
51
+ return try freeFormValue(of: value, depth: 0, in: runtime)
52
+ }
53
+
54
+ /// Decodes a JavaScript array into an array of free-form native values. A `Set` decodes as an array
55
+ /// of its elements, and any other value that isn't an array is wrapped in a single-element array,
56
+ /// like `Array.decode` does.
57
+ @JavaScriptActor
58
+ public static func decodeAnyArray(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime)
59
+ throws -> [Any]
60
+ {
61
+ guard value.isArray() else {
62
+ return wrappingInArray(try freeFormValue(of: value, depth: 0, in: runtime))
63
+ }
64
+ return try freeFormElements(of: value.getArray(), depth: 0, in: runtime)
65
+ }
66
+
67
+ /// Decodes a borrowed JavaScript array into an array of free-form native values. See
68
+ /// ``decodeAnyArray(_:in:)``.
69
+ @JavaScriptActor
70
+ public static func decodeAnyArray(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime)
71
+ throws -> [Any]
72
+ {
73
+ guard value.isObject() else {
74
+ return [try freeFormValue(of: value, depth: 0, in: runtime)]
75
+ }
76
+ let object = value.getObject(in: copy runtime)
77
+ guard object.isArray() else {
78
+ return wrappingInArray(try freeFormValue(ofObject: object, depth: 0, in: runtime))
79
+ }
80
+ return try freeFormElements(of: object.getArray(), depth: 0, in: runtime)
81
+ }
82
+
83
+ /// Decodes a JavaScript object into a string-keyed dictionary of free-form native values, with the
84
+ /// same rules as a nested object in ``decodeAny(_:in:)`` (so a `Map` with string keys converts).
85
+ /// Throws `TypeError` if the value doesn't decode as a dictionary, such as a primitive, an array, a
86
+ /// `Date`, a `Set` or a function, or if anything inside it can't be decoded.
87
+ @JavaScriptActor
88
+ public static func decodeAnyDictionary(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime)
89
+ throws -> [String: Any]
90
+ {
91
+ return try freeFormDictionary(of: value.asObject(), in: runtime)
92
+ }
93
+
94
+ /// Decodes a borrowed JavaScript object into a string-keyed dictionary of free-form native values.
95
+ /// See ``decodeAnyDictionary(_:in:)``.
96
+ @JavaScriptActor
97
+ public static func decodeAnyDictionary(
98
+ _ value: borrowing JavaScriptUnownedValue,
99
+ in runtime: borrowing JavaScriptRuntime
100
+ ) throws -> [String: Any] {
101
+ return try freeFormDictionary(of: value.asObject(in: copy runtime), in: runtime)
102
+ }
103
+
104
+ /// Decodes a JavaScript value into any type built from free-form values, such as `[[Any]]` or
105
+ /// `[String: [Any]]?`. The value is decoded with ``decodeAny(_:in:)`` and then
106
+ /// cast to `type`. A `null` or `undefined` at any depth decodes as `nil`, so it fits an optional
107
+ /// in `type` at that position (`[String: [Any]?]` accepts `{ a: null }`).
108
+ ///
109
+ /// Throws `TypeError` if the decoded value can't be cast to `type`. Prefer ``decodeAny(_:in:)``,
110
+ /// ``decodeAnyArray(_:in:)`` or ``decodeAnyDictionary(_:in:)`` for the types they cover, because
111
+ /// they don't need the cast.
112
+ @JavaScriptActor
113
+ public static func decodeAny<T>(
114
+ _ value: borrowing JavaScriptValue,
115
+ as type: T.Type,
116
+ in runtime: borrowing JavaScriptRuntime
117
+ ) throws -> T {
118
+ guard let result = try decodeAny(value, in: runtime) as? T else {
119
+ throw TypeError(type: T.self, reason: castFailureReason(for: T.self))
120
+ }
121
+ return result
122
+ }
123
+
124
+ // MARK: - Encoding
125
+
126
+ /// Encodes a free-form native value into a JavaScript value.
127
+ ///
128
+ /// Supports strings, numbers (including `NSNumber`), booleans, `nil` and `NSNull` (encoded as
129
+ /// `null`), `[Any]` and `[String: Any]` with supported elements, and any `JavaScriptEncodable` or
130
+ /// `JavaScriptRepresentable` value. Throws `EncodingError` for any other value.
131
+ @JavaScriptActor
132
+ public static func encodeAny(_ value: Any, in runtime: borrowing JavaScriptRuntime) throws -> JavaScriptValue {
133
+ // Strings and the native number and boolean types are the most common leaf values, so they're
134
+ // checked first, before the slower protocol casts, and each converts through its own `encode` (so
135
+ // an `Int` outside the safe-integer range throws, as `Int.encode` does). The number and boolean
136
+ // checks compare the exact type rather than casting, since an `NSNumber` casts to both `Bool` and
137
+ // `Double` and has to be told apart below.
138
+ if let string = value as? String {
139
+ return try String.encode(string, in: runtime)
140
+ }
141
+ let valueType = type(of: value)
142
+ if valueType == Double.self {
143
+ return try Double.encode(value as! Double, in: runtime)
144
+ }
145
+ if valueType == Bool.self {
146
+ return try Bool.encode(value as! Bool, in: runtime)
147
+ }
148
+ if valueType == Int.self {
149
+ return try Int.encode(value as! Int, in: runtime)
150
+ }
151
+ if let dictionary = value as? [String: Any] {
152
+ return try encodeAnyDictionary(dictionary, in: runtime)
153
+ }
154
+ if let array = value as? [Any] {
155
+ return try encodeAnyArray(array, in: runtime)
156
+ }
157
+ if value is NSNull {
158
+ return .null
159
+ }
160
+ if let optional = value as? any FreeFormOptional {
161
+ guard let wrapped = optional.freeFormWrappedValue else {
162
+ return .null
163
+ }
164
+ return try encodeAny(wrapped, in: runtime)
165
+ }
166
+ if let encodable = value as? any JavaScriptEncodable {
167
+ return try encode(opening: encodable, in: runtime)
168
+ }
169
+ if let number = value as? NSNumber {
170
+ if CFGetTypeID(number) == CFBooleanGetTypeID() {
171
+ return number.boolValue ? .true() : .false()
172
+ }
173
+ return .number(number.doubleValue)
174
+ }
175
+ if let representable = value as? any JavaScriptRepresentable {
176
+ return representable.toJavaScriptValue(in: copy runtime)
177
+ }
178
+ throw EncodingError(valueType: valueType)
179
+ }
180
+
181
+ /// Encodes an array of free-form native values into a JavaScript array. Each element is encoded
182
+ /// with ``encodeAny(_:in:)``.
183
+ @JavaScriptActor
184
+ public static func encodeAnyArray(_ value: [Any], in runtime: borrowing JavaScriptRuntime) throws -> JavaScriptValue {
185
+ let array = runtime.createArray(length: value.count)
186
+ for (index, element) in value.enumerated() {
187
+ try array.set(value: encodeAny(element, in: runtime), at: index)
188
+ }
189
+ return array.asValue()
190
+ }
191
+
192
+ /// Encodes a string-keyed dictionary of free-form native values into a JavaScript object. Each
193
+ /// value is encoded with ``encodeAny(_:in:)``.
194
+ @JavaScriptActor
195
+ public static func encodeAnyDictionary(_ value: [String: Any], in runtime: borrowing JavaScriptRuntime) throws
196
+ -> JavaScriptValue
197
+ {
198
+ let object = runtime.createObject()
199
+ for (key, element) in value {
200
+ object.setProperty(key, value: try encodeAny(element, in: runtime))
201
+ }
202
+ return object.asValue()
203
+ }
204
+
205
+ // MARK: - Helpers
206
+
207
+ @JavaScriptActor
208
+ private static func freeFormValue(
209
+ of value: borrowing JavaScriptValue,
210
+ depth: Int,
211
+ in runtime: borrowing JavaScriptRuntime
212
+ ) throws -> Any {
213
+ if value.isString() {
214
+ return value.getString()
215
+ }
216
+ if value.isNumber() {
217
+ return value.getDouble()
218
+ }
219
+ if value.isBool() {
220
+ return value.getBool()
221
+ }
222
+ if value.isObject() {
223
+ return try freeFormValue(ofObject: value.getObject(), depth: depth, in: runtime)
224
+ }
225
+ if value.isNull() || value.isUndefined() {
226
+ return Optional<Any>.none as Any
227
+ }
228
+ if value.isBigInt() {
229
+ return try value.getBigInt().asInt64()
230
+ }
231
+ throw TypeError(type: Any.self, reason: unrepresentableValueReason)
232
+ }
233
+
234
+ @JavaScriptActor
235
+ private static func freeFormValue(
236
+ of value: borrowing JavaScriptUnownedValue,
237
+ depth: Int,
238
+ in runtime: borrowing JavaScriptRuntime
239
+ ) throws -> Any {
240
+ if value.isString() {
241
+ return value.getString()
242
+ }
243
+ if value.isNumber() {
244
+ return value.getDouble()
245
+ }
246
+ if value.isBool() {
247
+ return value.getBool()
248
+ }
249
+ if value.isObject() {
250
+ return try freeFormValue(ofObject: value.getObject(in: copy runtime), depth: depth, in: runtime)
251
+ }
252
+ if value.isNull() || value.isUndefined() {
253
+ return Optional<Any>.none as Any
254
+ }
255
+ if value.isBigInt() {
256
+ // The borrowed value has no bigint accessor, so this rare case goes through an owning copy.
257
+ return try value.copied(in: copy runtime).getBigInt().asInt64()
258
+ }
259
+ throw TypeError(type: Any.self, reason: unrepresentableValueReason)
260
+ }
261
+
262
+ @JavaScriptActor
263
+ private static func freeFormValue(
264
+ ofObject object: borrowing JavaScriptObject,
265
+ depth: Int,
266
+ in runtime: borrowing JavaScriptRuntime
267
+ ) throws -> Any {
268
+ if object.isArray() {
269
+ return try freeFormElements(of: object.getArray(), depth: depth, in: runtime)
270
+ }
271
+ if object.isFunction() {
272
+ throw TypeError(type: Any.self, reason: unrepresentableValueReason)
273
+ }
274
+ try rejectBinaryData(object)
275
+ let keys = object.getPropertyNames()
276
+ if keys.isEmpty {
277
+ // A `Date`, `Set`, `Map`, `WeakMap`, `WeakSet` or `DataView` has no enumerable properties, so only
278
+ // an object without any can be one. This keeps the global lookup and `instanceof` walk of `is(_:)`
279
+ // off objects that have properties.
280
+ let value = object.asValue()
281
+ if value.is("Date") {
282
+ return try Date.decode(value, in: runtime)
283
+ }
284
+ if value.is("Set") {
285
+ // A `Set` has no native free-form counterpart (a Swift `Set` needs hashable elements), so it
286
+ // decodes as an array of its elements, in insertion order.
287
+ let elements = try runtime.global()
288
+ .getPropertyAsObject("Array")
289
+ .getPropertyAsFunction("from")
290
+ .call(arguments: value)
291
+ return try freeFormElements(of: elements.asArray(), depth: depth, in: runtime)
292
+ }
293
+ if value.is("Map") {
294
+ // A `Map` keeps its entries outside its properties, so it's read through its `[key, value]`
295
+ // pairs. Only string keys have a dictionary counterpart; any other key throws.
296
+ let entries = try runtime.global()
297
+ .getPropertyAsObject("Array")
298
+ .getPropertyAsFunction("from")
299
+ .call(arguments: value)
300
+ return try freeFormMapEntries(of: entries.asArray(), depth: depth, in: runtime)
301
+ }
302
+ if value.is("WeakMap") || value.is("WeakSet") {
303
+ throw TypeError(type: Any.self, reason: weakCollectionReason)
304
+ }
305
+ if value.is("DataView") {
306
+ throw TypeError(type: Any.self, reason: binaryDataReason)
307
+ }
308
+ }
309
+ return try freeFormProperties(of: object, keys: keys, depth: depth, in: runtime)
310
+ }
311
+
312
+ /// Decodes a top-level object exactly as it would decode nested inside a value, then requires a
313
+ /// dictionary, so a top-level `Map` converts and a function, `Date`, `Set` or array throws, just as
314
+ /// through `decodeAny(_:as: [String: Any].self)`.
315
+ @JavaScriptActor
316
+ private static func freeFormDictionary(
317
+ of object: borrowing JavaScriptObject,
318
+ in runtime: borrowing JavaScriptRuntime
319
+ ) throws -> [String: Any] {
320
+ guard let dictionary = try freeFormValue(ofObject: object, depth: 0, in: runtime) as? [String: Any] else {
321
+ throw TypeError(type: [String: Any].self, reason: castFailureReason(for: [String: Any].self))
322
+ }
323
+ return dictionary
324
+ }
325
+
326
+ @JavaScriptActor
327
+ private static func freeFormElements(
328
+ of array: borrowing JavaScriptArray,
329
+ depth: Int,
330
+ in runtime: borrowing JavaScriptRuntime
331
+ ) throws -> [Any] {
332
+ try checkDepth(depth)
333
+ var index = 0
334
+ return try array.map { element in
335
+ defer {
336
+ index += 1
337
+ }
338
+ do {
339
+ return try freeFormValue(of: element, depth: depth + 1, in: runtime)
340
+ } catch let error as TypeError {
341
+ throw error.prependingPath("[\(index)]")
342
+ }
343
+ }
344
+ }
345
+
346
+ @JavaScriptActor
347
+ private static func freeFormProperties(
348
+ of object: borrowing JavaScriptObject,
349
+ keys: [String],
350
+ depth: Int,
351
+ in runtime: borrowing JavaScriptRuntime
352
+ ) throws -> [String: Any] {
353
+ try checkDepth(depth)
354
+ var result = [String: Any](minimumCapacity: keys.count)
355
+ for key in keys {
356
+ let property = object.getProperty(key)
357
+ if property.isUndefined() {
358
+ continue
359
+ }
360
+ do {
361
+ result[key] = try freeFormValue(of: property, depth: depth + 1, in: runtime)
362
+ } catch let error as TypeError {
363
+ throw error.prependingPath(".\(key)")
364
+ }
365
+ }
366
+ return result
367
+ }
368
+
369
+ /// Wraps a decoded non-array value in a single-element array, unless it already is an array, which
370
+ /// only a `Set` decodes to. Wrapping a `Set` would add a level that the generic path doesn't.
371
+ private static func wrappingInArray(_ decoded: Any) -> [Any] {
372
+ if let elements = decoded as? [Any] {
373
+ return elements
374
+ }
375
+ return [decoded]
376
+ }
377
+
378
+ /// Decodes the `[key, value]` pairs of a `Map` into a dictionary. Throws for a key that isn't a
379
+ /// string, since a dictionary key would have to be a string the entry doesn't have.
380
+ @JavaScriptActor
381
+ private static func freeFormMapEntries(
382
+ of entries: borrowing JavaScriptArray,
383
+ depth: Int,
384
+ in runtime: borrowing JavaScriptRuntime
385
+ ) throws -> [String: Any] {
386
+ try checkDepth(depth)
387
+ var result = [String: Any](minimumCapacity: entries.length)
388
+ for index in 0..<entries.length {
389
+ let entry = try entries.getValue(at: index).asArray()
390
+ let key = try entry.getValue(at: 0)
391
+ guard key.isString() else {
392
+ throw TypeError(type: Any.self, reason: nonStringMapKeyReason)
393
+ }
394
+ let name = key.getString()
395
+ let value = try entry.getValue(at: 1)
396
+ // An `undefined` value means "absent", as it does for an object property.
397
+ if value.isUndefined() {
398
+ continue
399
+ }
400
+ do {
401
+ result[name] = try freeFormValue(of: value, depth: depth + 1, in: runtime)
402
+ } catch let error as TypeError {
403
+ throw error.prependingPath(".\(name)")
404
+ }
405
+ }
406
+ return result
407
+ }
408
+
409
+ /// Throws for a typed array or `ArrayBuffer`, whose bytes would otherwise decode as a dictionary
410
+ /// keyed by index, or as an empty one.
411
+ @JavaScriptActor
412
+ private static func rejectBinaryData(_ object: borrowing JavaScriptObject) throws {
413
+ if object.isTypedArray() || object.isArrayBuffer() {
414
+ throw TypeError(type: Any.self, reason: binaryDataReason)
415
+ }
416
+ }
417
+
418
+ /// Throws once a value is nested deeper than `maxFreeFormDepth`, which stops a cycle from
419
+ /// recursing until the stack overflows.
420
+ private static func checkDepth(_ depth: Int) throws {
421
+ if depth >= maxFreeFormDepth {
422
+ throw TypeError(type: Any.self, reason: nestingTooDeepReason)
423
+ }
424
+ }
425
+
426
+ /// Opens the existential so the conforming type's own `encode` runs.
427
+ @JavaScriptActor
428
+ private static func encode<Value: JavaScriptEncodable>(
429
+ opening value: Value,
430
+ in runtime: borrowing JavaScriptRuntime
431
+ ) throws -> JavaScriptValue {
432
+ return try Value.encode(value, in: runtime)
433
+ }
434
+ }
435
+
436
+ // MARK: - Errors
437
+
438
+ /// The deepest nesting of arrays and objects that free-form decoding accepts.
439
+ private let maxFreeFormDepth = 64
440
+
441
+ /// The `TypeError` reason for a JavaScript value that has no free-form native form.
442
+ private let unrepresentableValueReason =
443
+ "Cannot convert a JavaScript function or symbol to a native value, because neither has a native representation. Remove it from the value before passing it to native code, or declare the native type as JavaScriptValue to receive it unconverted."
444
+
445
+ /// The `TypeError` reason for a typed array, `ArrayBuffer` or `DataView` inside a free-form value.
446
+ private let binaryDataReason =
447
+ "Cannot convert a typed array, ArrayBuffer or DataView to a free-form native value, because free-form values hold only primitives, plain objects, arrays and dates. Convert it to an array first (for example, with Array.from), or declare the native type as JavaScriptValue to receive it unconverted."
448
+
449
+ /// The `TypeError` reason for a `WeakMap` or `WeakSet` inside a free-form value.
450
+ private let weakCollectionReason =
451
+ "Cannot convert a WeakMap or WeakSet to a native value, because JavaScript doesn't allow reading their entries. Pass a Map or Set instead, or declare the native type as JavaScriptValue to receive it unconverted."
452
+
453
+ /// The `TypeError` reason for a `Map` with a key that isn't a string.
454
+ private let nonStringMapKeyReason =
455
+ "Cannot convert a Map with a key that isn't a string to a native value, because it decodes as a dictionary, whose keys are strings. Use only string keys, convert the Map to an array of entries first (for example, with Array.from), or declare the native type as JavaScriptValue to receive it unconverted."
456
+
457
+ /// The `TypeError` reason for a value nested deeper than `maxFreeFormDepth`.
458
+ private let nestingTooDeepReason =
459
+ "Cannot convert a JavaScript value nested more than \(maxFreeFormDepth) levels deep to a native value, most likely because it contains a reference to itself (a cycle). Remove the cycle before passing the value to native code, or declare the native type as JavaScriptValue to receive it unconverted."
460
+
461
+ /// The `TypeError` reason for a decoded free-form value that doesn't cast to the requested type.
462
+ private func castFailureReason(for type: Any.Type) -> String {
463
+ return
464
+ "Cannot convert the JavaScript value to '\(type)', because the decoded value has a different shape. Free-form decoding produces only Bool, Double (for every number), String, Int64, Date, [Any], [String: Any] (object keys are always String) and nil. Declare the type in terms of these, for example [String: Any] rather than [Int: Any], or pass a value of the expected shape."
465
+ }
466
+
467
+ extension JavaScriptValue.TypeError {
468
+ /// Returns the error with `component` (such as `.key` or `[2]`) added to the front of its path, as
469
+ /// the error passes up through each container on the way out.
470
+ fileprivate func prependingPath(_ component: String) -> Self {
471
+ var error = self
472
+ error.path = component + path
473
+ return error
474
+ }
475
+ }
476
+
477
+ extension JavaScriptValue {
478
+ /// Thrown by ``JavaScriptValue/encodeAny(_:in:)`` for a value it can't convert to JavaScript.
479
+ public struct EncodingError: Error, CustomStringConvertible {
480
+ let valueType: Any.Type
481
+
482
+ public var description: String {
483
+ return
484
+ "Cannot convert a value of type '\(valueType)' to a JavaScript value, because it isn't a string, number, boolean, null, array, dictionary, or a JavaScriptEncodable value. Convert it to one of these types first, or make '\(valueType)' conform to JavaScriptEncodable."
485
+ }
486
+ }
487
+ }
488
+
489
+ // MARK: - Optional support
490
+
491
+ /// Lets `encodeAny` unwrap an `Optional` found inside `Any`, whose wrapped type isn't known statically.
492
+ private protocol FreeFormOptional {
493
+ var freeFormWrappedValue: Any? { get }
494
+ }
495
+
496
+ extension Optional: FreeFormOptional {
497
+ var freeFormWrappedValue: Any? {
498
+ return self.map { $0 as Any }
499
+ }
500
+ }
@@ -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
  }