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
@@ -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
@@ -407,13 +482,134 @@ extension UInt64: JavaScriptCodable {
407
482
  }
408
483
  }
409
484
 
485
+ // MARK: - Integer conversion entry points
486
+
487
+ // The inlinable integer witnesses call these non-generic overloads rather than the generic
488
+ // implementations below. A client can devirtualize a witness and inline it only when its body calls
489
+ // non-generic functions; through a generic, non-inlinable call, it falls back to dynamic dispatch, and a
490
+ // direct call runs the generic implementation unspecialized. Each overload runs a copy of the generic
491
+ // implementation specialized inside the framework.
492
+
493
+ @usableFromInline
494
+ @JavaScriptActor
495
+ func decodeInteger(_ number: Double, as type: Int8.Type) throws -> Int8 {
496
+ return try genericDecodeInteger(number, as: Int8.self)
497
+ }
498
+
499
+ @usableFromInline
500
+ @JavaScriptActor
501
+ func decodeInteger(_ number: Double, as type: Int16.Type) throws -> Int16 {
502
+ return try genericDecodeInteger(number, as: Int16.self)
503
+ }
504
+
505
+ @usableFromInline
506
+ @JavaScriptActor
507
+ func decodeInteger(_ number: Double, as type: Int32.Type) throws -> Int32 {
508
+ return try genericDecodeInteger(number, as: Int32.self)
509
+ }
510
+
511
+ @usableFromInline
512
+ @JavaScriptActor
513
+ func decodeInteger(_ number: Double, as type: UInt8.Type) throws -> UInt8 {
514
+ return try genericDecodeInteger(number, as: UInt8.self)
515
+ }
516
+
517
+ @usableFromInline
518
+ @JavaScriptActor
519
+ func decodeInteger(_ number: Double, as type: UInt16.Type) throws -> UInt16 {
520
+ return try genericDecodeInteger(number, as: UInt16.self)
521
+ }
522
+
523
+ @usableFromInline
524
+ @JavaScriptActor
525
+ func decodeInteger(_ number: Double, as type: UInt32.Type) throws -> UInt32 {
526
+ return try genericDecodeInteger(number, as: UInt32.self)
527
+ }
528
+
529
+ @usableFromInline
530
+ @JavaScriptActor
531
+ func decodeWideInteger(_ value: borrowing JavaScriptValue, as type: Int.Type) throws -> Int {
532
+ return try genericDecodeWideInteger(value, as: Int.self)
533
+ }
534
+
535
+ @usableFromInline
536
+ @JavaScriptActor
537
+ func decodeWideInteger(
538
+ _ value: borrowing JavaScriptUnownedValue,
539
+ as type: Int.Type,
540
+ runtime: borrowing JavaScriptRuntime
541
+ ) throws -> Int {
542
+ return try genericDecodeWideInteger(value, as: Int.self, runtime: runtime)
543
+ }
544
+
545
+ @usableFromInline
546
+ @JavaScriptActor
547
+ func decodeWideInteger(_ value: borrowing JavaScriptValue, as type: Int64.Type) throws -> Int64 {
548
+ return try genericDecodeWideInteger(value, as: Int64.self)
549
+ }
550
+
551
+ @usableFromInline
552
+ @JavaScriptActor
553
+ func decodeWideInteger(
554
+ _ value: borrowing JavaScriptUnownedValue,
555
+ as type: Int64.Type,
556
+ runtime: borrowing JavaScriptRuntime
557
+ ) throws -> Int64 {
558
+ return try genericDecodeWideInteger(value, as: Int64.self, runtime: runtime)
559
+ }
560
+
561
+ @usableFromInline
562
+ @JavaScriptActor
563
+ func decodeWideInteger(_ value: borrowing JavaScriptValue, as type: UInt.Type) throws -> UInt {
564
+ return try genericDecodeWideInteger(value, as: UInt.self)
565
+ }
566
+
567
+ @usableFromInline
568
+ @JavaScriptActor
569
+ func decodeWideInteger(
570
+ _ value: borrowing JavaScriptUnownedValue,
571
+ as type: UInt.Type,
572
+ runtime: borrowing JavaScriptRuntime
573
+ ) throws -> UInt {
574
+ return try genericDecodeWideInteger(value, as: UInt.self, runtime: runtime)
575
+ }
576
+
577
+ @usableFromInline
578
+ @JavaScriptActor
579
+ func decodeWideInteger(_ value: borrowing JavaScriptValue, as type: UInt64.Type) throws -> UInt64 {
580
+ return try genericDecodeWideInteger(value, as: UInt64.self)
581
+ }
582
+
583
+ @usableFromInline
584
+ @JavaScriptActor
585
+ func decodeWideInteger(
586
+ _ value: borrowing JavaScriptUnownedValue,
587
+ as type: UInt64.Type,
588
+ runtime: borrowing JavaScriptRuntime
589
+ ) throws -> UInt64 {
590
+ return try genericDecodeWideInteger(value, as: UInt64.self, runtime: runtime)
591
+ }
592
+
593
+ @usableFromInline
594
+ @JavaScriptActor
595
+ func encodeSafeInteger(_ value: Int) throws -> JavaScriptValue {
596
+ return try genericEncodeSafeInteger(value)
597
+ }
598
+
599
+ @usableFromInline
600
+ @JavaScriptActor
601
+ func encodeSafeInteger(_ value: UInt) throws -> JavaScriptValue {
602
+ return try genericEncodeSafeInteger(value)
603
+ }
604
+
605
+ // MARK: - Generic integer conversions
606
+
410
607
  /// Rounds a JavaScript number to the nearest integer and narrows it to `T`, throwing instead of
411
608
  /// trapping when the value is non-finite or outside `T`'s representable range. `T(exactly:)` on the
412
609
  /// already-rounded value is nil only when out of range, sidestepping the lossy `Double(T.max)`
413
610
  /// boundary comparison for 64-bit widths.
414
- @usableFromInline
415
611
  @JavaScriptActor
416
- func decodeInteger<T: FixedWidthInteger>(_ number: Double, as type: T.Type) throws -> T {
612
+ func genericDecodeInteger<T: FixedWidthInteger>(_ number: Double, as type: T.Type) throws -> T {
417
613
  guard number.isFinite, let result = T(exactly: number.rounded()) else {
418
614
  throw IntegerOutOfRangeException(value: number, type: "\(T.self)")
419
615
  }
@@ -424,47 +620,41 @@ func decodeInteger<T: FixedWidthInteger>(_ number: Double, as type: T.Type) thro
424
620
  /// case is checked first since it's the common one: a single `isNumber()` tag check, then the
425
621
  /// assert-only `getDouble()` reads the value without re-checking the tag. A `bigint` is read losslessly
426
622
  /// through its 64-bit accessor; anything else throws the same `TypeError` the `number` read would.
427
- @usableFromInline
428
623
  @JavaScriptActor
429
- func decodeWideInteger<T: FixedWidthInteger>(_ value: borrowing JavaScriptValue, as type: T.Type) throws -> T {
624
+ func genericDecodeWideInteger<T: FixedWidthInteger>(_ value: borrowing JavaScriptValue, as type: T.Type) throws -> T {
430
625
  guard value.isNumber() else {
431
626
  guard value.isBigInt() else {
432
- // Neither a number nor a bigint: `asDouble()` throws the canonical `TypeError`. Throwing it
433
- // through the accessor (rather than constructing it here) keeps this inlinable helper from
434
- // referencing `TypeError`'s internal memberwise initializer.
435
- return try decodeInteger(value.asDouble(), as: T.self)
627
+ // Neither a number nor a bigint: `asDouble()` throws the canonical `TypeError`.
628
+ return try genericDecodeInteger(value.asDouble(), as: T.self)
436
629
  }
437
- return try decodeBigInt(value.getBigInt(), as: T.self)
630
+ return try genericDecodeBigInt(value.getBigInt(), as: T.self)
438
631
  }
439
- return try decodeInteger(value.getDouble(), as: T.self)
632
+ return try genericDecodeInteger(value.getDouble(), as: T.self)
440
633
  }
441
634
 
442
635
  /// `JavaScriptUnownedValue` overload of `decodeWideInteger`. The common `number` path stays zero-copy
443
636
  /// and pays a single tag check (see the owning overload); only a `bigint` materializes an owning value
444
637
  /// (the borrowed value exposes no BigInt accessor), which is acceptable on this rare branch.
445
- @usableFromInline
446
638
  @JavaScriptActor
447
- func decodeWideInteger<T: FixedWidthInteger>(
639
+ func genericDecodeWideInteger<T: FixedWidthInteger>(
448
640
  _ value: borrowing JavaScriptUnownedValue,
449
641
  as type: T.Type,
450
642
  runtime: borrowing JavaScriptRuntime
451
643
  ) throws -> T {
452
644
  guard value.isNumber() else {
453
645
  guard value.isBigInt() else {
454
- // See the owning overload: route the not-a-number throw through `asDouble()` rather than
455
- // constructing `TypeError` here, which this inlinable helper can't reference.
456
- return try decodeInteger(value.asDouble(), as: T.self)
646
+ // Neither a number nor a bigint: `asDouble()` throws the canonical `TypeError`.
647
+ return try genericDecodeInteger(value.asDouble(), as: T.self)
457
648
  }
458
- return try decodeBigInt(value.copied(in: runtime).getBigInt(), as: T.self)
649
+ return try genericDecodeBigInt(value.copied(in: runtime).getBigInt(), as: T.self)
459
650
  }
460
- return try decodeInteger(value.getDouble(), as: T.self)
651
+ return try genericDecodeInteger(value.getDouble(), as: T.self)
461
652
  }
462
653
 
463
654
  /// Narrows a `JavaScriptBigInt` to `T`, reading it through the signed or unsigned 64-bit accessor to
464
655
  /// match `T`'s signedness and throwing rather than truncating when it falls outside `T`'s range.
465
- @usableFromInline
466
656
  @JavaScriptActor
467
- func decodeBigInt<T: FixedWidthInteger>(_ bigInt: borrowing JavaScriptBigInt, as type: T.Type) throws -> T {
657
+ func genericDecodeBigInt<T: FixedWidthInteger>(_ bigInt: borrowing JavaScriptBigInt, as type: T.Type) throws -> T {
468
658
  let wide: T? =
469
659
  if T.isSigned {
470
660
  bigInt.isInt64() ? T(exactly: bigInt.getInt64()) : nil
@@ -480,9 +670,8 @@ func decodeBigInt<T: FixedWidthInteger>(_ bigInt: borrowing JavaScriptBigInt, as
480
670
  /// Encodes a 64-bit-wide integer to a JS `number`, throwing when it falls outside JavaScript's
481
671
  /// safe-integer range where a `Double` could no longer represent it exactly. Used by `Int`/`UInt`,
482
672
  /// whose JS mapping stays a `number`; the explicitly-sized 64-bit types encode as a `bigint` instead.
483
- @usableFromInline
484
673
  @JavaScriptActor
485
- func encodeSafeInteger<T: FixedWidthInteger>(_ value: T) throws -> JavaScriptValue {
674
+ func genericEncodeSafeInteger<T: FixedWidthInteger>(_ value: T) throws -> JavaScriptValue {
486
675
  // `Number.MAX_SAFE_INTEGER` (2^53 - 1): the largest magnitude where every integer up to it, and the
487
676
  // next one, is representable as a JS number. 2^53 itself is representable but unsafe (it collides with
488
677
  // 2^53 + 1), so the bound is exclusive above this. Used by `Int`/`UInt`, both 64-bit, so 2^53 fits.
@@ -0,0 +1,79 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ internal import ExpoModulesJSI_Cxx
4
+ internal import jsi
5
+
6
+ // `Set` encodes to a JS `Set` and decodes from a JS `Set` or, like `Array`, from an array or an arrayized
7
+ // scalar. JSI has no `Set` API, so the conversions go through the runtime's global `Set` and `Array`
8
+ // constructors. Equality differs between the two sides: a JS `Set` compares objects by reference while
9
+ // a Swift `Set` compares by `Hashable`, so two distinct JS objects that decode to equal values collapse
10
+ // into a single element.
11
+
12
+ extension Set: JavaScriptDecodable where Element: JavaScriptDecodable {
13
+ @JavaScriptActor
14
+ @inlinable
15
+ public static func decode(_ value: borrowing JavaScriptValue, in runtime: borrowing JavaScriptRuntime) throws
16
+ -> Set<Element>
17
+ {
18
+ // The cheap tag checks come first: an array takes the `Array` path directly, and a primitive
19
+ // can't be a JS `Set`, so both skip the JS call below. Any other value that is not a JS `Set` is
20
+ // arrayized, as in `Array`. Duplicates in an array collapse without an error.
21
+ guard !value.isArray(), value.isObject(), let entries = try runtime.setEntries(of: value) else {
22
+ return Set(try [Element].decode(value, in: runtime))
23
+ }
24
+ return Set(try [Element].decode(entries, in: runtime))
25
+ }
26
+ }
27
+
28
+ extension Set: JavaScriptEncodable where Element: JavaScriptEncodable {
29
+ @JavaScriptActor
30
+ @inlinable
31
+ public static func encode(_ value: Set<Element>, in runtime: borrowing JavaScriptRuntime) throws
32
+ -> JavaScriptValue
33
+ {
34
+ // Constructing from an array fills the set in a single call, rather than one `add` call per element.
35
+ let entries = try [Element].encode(Array(value), in: runtime)
36
+ return try runtime.setConstructor().callAsConstructor(entries)
37
+ }
38
+ }
39
+
40
+ extension JavaScriptRuntime {
41
+ /// Copies the entries of a JS `Set` out into a JS array, or returns `nil` when the value is not a
42
+ /// JS `Set`. The instance check and the copy run in a single call to a cached JS function, which
43
+ /// saves the global lookups and the separate `instanceof` call of `value.is("Set")` followed by
44
+ /// `Array.from`. For a small `Set` those fixed costs are most of the decode time.
45
+ @usableFromInline
46
+ @JavaScriptActor
47
+ func setEntries(of value: borrowing JavaScriptValue) throws -> JavaScriptValue? {
48
+ let function = try cached(setEntriesFunctionKey) {
49
+ return try eval(
50
+ label: "expo-modules-jsi/set-entries.js",
51
+ "(function (value) { return value instanceof Set ? Array.from(value) : undefined; })"
52
+ )
53
+ }
54
+ let setEntries = function.getFunction()
55
+ // Pass the borrowed `jsi::Value` straight to the call, rather than copying it into an owned value
56
+ // and again into an arguments buffer.
57
+ let entries = try withUnsafePointer(to: value.pointee) { argument in
58
+ return try capturingCppErrors {
59
+ return JavaScriptValue(self, expo.callFunction(pointee, setEntries.pointee, argument, 1))
60
+ }
61
+ }
62
+ return entries.isUndefined() ? nil : entries
63
+ }
64
+
65
+ /// Returns the global `Set` constructor, looked up once and cached, so an encode doesn't pay for
66
+ /// the global property lookup.
67
+ @usableFromInline
68
+ @JavaScriptActor
69
+ func setConstructor() throws -> JavaScriptFunction {
70
+ let constructor = try cached(setConstructorKey) {
71
+ return try global().getPropertyAsFunction("Set").asValue()
72
+ }
73
+ return constructor.getFunction()
74
+ }
75
+ }
76
+
77
+ /// Keys of the `Set` entries function and the `Set` constructor in each runtime's cache.
78
+ private let setEntriesFunctionKey = JavaScriptRuntime.Cache.Key<JavaScriptValue>()
79
+ private let setConstructorKey = JavaScriptRuntime.Cache.Key<JavaScriptValue>()
@@ -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
  }
@@ -0,0 +1,73 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ internal import jsi
4
+
5
+ /// A set of JavaScript value kinds, as told apart by the value's type tag alone.
6
+ ///
7
+ /// Unlike `JavaScriptValue.Kind`, a function is an `object` here: telling it apart needs a call into
8
+ /// the runtime, while every kind in this set is read from the tag without one. Used by
9
+ /// `JavaScriptDecodable.decodableKinds` to describe the kinds a type can decode from. Frozen, so a check
10
+ /// against it compiles to a mask test in the client.
11
+ @frozen
12
+ public struct JavaScriptValueKinds: OptionSet, Sendable {
13
+ public let rawValue: UInt16
14
+
15
+ // `UInt16` leaves room for more kinds without changing the frozen layout.
16
+ //
17
+ // The kinds are computed and inlinable rather than stored `static let`s, which a client would reach
18
+ // through a lazily initialized global, so a mask built from them folds to a constant.
19
+
20
+ @inlinable
21
+ public init(rawValue: UInt16) {
22
+ self.rawValue = rawValue
23
+ }
24
+
25
+ @inlinable public static var undefined: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 0) }
26
+ @inlinable public static var null: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 1) }
27
+ @inlinable public static var bool: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 2) }
28
+ @inlinable public static var number: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 3) }
29
+ @inlinable public static var bigint: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 4) }
30
+ @inlinable public static var string: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 5) }
31
+ @inlinable public static var symbol: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 6) }
32
+ /// Any object, including arrays, typed arrays and functions.
33
+ @inlinable public static var object: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: 1 << 7) }
34
+
35
+ /// Every kind, including any added later: the bits above the current kinds are set too, so a type that
36
+ /// accepts every kind keeps doing so.
37
+ @inlinable public static var all: JavaScriptValueKinds { JavaScriptValueKinds(rawValue: .max) }
38
+
39
+ /// The kind of `value`.
40
+ ///
41
+ /// Not inlinable on purpose: the type checks read the `jsi::Value`, which clients can't see, so an
42
+ /// inlined initializer would make one call into this module per check. Here it is a single call.
43
+ public init(of value: borrowing JavaScriptValue) {
44
+ self.init(of: value.pointee)
45
+ }
46
+
47
+ /// The kind of the borrowed `value`, read the same way as from an owning value.
48
+ public init(of value: borrowing JavaScriptUnownedValue) {
49
+ self.init(of: value.pointer.pointee)
50
+ }
51
+
52
+ /// Reads the kind from the `jsi::Value` both public initializers wrap. The checks go from the most to
53
+ /// the least common kind of an argument.
54
+ private init(of value: borrowing facebook.jsi.Value) {
55
+ if value.isNumber() {
56
+ self = .number
57
+ } else if value.isString() {
58
+ self = .string
59
+ } else if value.isObject() {
60
+ self = .object
61
+ } else if value.isBool() {
62
+ self = .bool
63
+ } else if value.isNull() {
64
+ self = .null
65
+ } else if value.isUndefined() {
66
+ self = .undefined
67
+ } else if value.isBigInt() {
68
+ self = .bigint
69
+ } else {
70
+ self = .symbol
71
+ }
72
+ }
73
+ }
@@ -93,14 +93,16 @@ extension String: JSIRepresentable {
93
93
  // allocation, copy and free per string. `withUTF8` is mutating (it makes a bridged string
94
94
  // contiguous first), hence the local copy; native strings are already contiguous and pay nothing.
95
95
  // The value is moved out through a local because `withUTF8` needs a `Copyable` closure result.
96
+ // The C++ helper moves the engine's `jsi::String` into the value, so this costs one engine handle;
97
+ // `jsi::Value(runtime, string)` would clone it and then release the original.
96
98
  var string = self
97
99
  var value = facebook.jsi.Value.undefined()
98
100
  string.withUTF8 { utf8 in
99
101
  guard let base = utf8.baseAddress else {
100
- value = facebook.jsi.Value(runtime, facebook.jsi.String.createFromAscii(runtime, "", 0))
102
+ value = expo.createStringValueFromAscii(runtime, "", 0)
101
103
  return
102
104
  }
103
- value = facebook.jsi.Value(runtime, facebook.jsi.String.createFromUtf8(runtime, base, utf8.count))
105
+ value = expo.createStringValueFromUtf8(runtime, base, utf8.count)
104
106
  }
105
107
  return value
106
108
  }