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
@@ -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
  }
@@ -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
  }
@@ -146,7 +146,7 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
146
146
  // when the last reference is gone. `deinit` is `nonisolated`, so it can touch the actor-isolated
147
147
  // registry directly given that exclusive access.
148
148
  propNameIdsRegistry.removeAll()
149
- cachedDeferredPromiseFactory = nil
149
+ cache.clear()
150
150
  expo.destroyRuntime(runtimePointee)
151
151
  }
152
152
 
@@ -203,7 +203,8 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
203
203
  let propertyName = String(jsiPropNameID: propertyName.pointee, in: runtime.pointee)
204
204
  return JavaScriptActor.assumeIsolated {
205
205
  return forwardingSwiftErrorsToJS(runtime: runtime) {
206
- try context.get(propertyName).writeJSIValue(to: resultPtr)
206
+ var result = try context.get(propertyName)
207
+ JavaScriptValue.write(&result, to: resultPtr)
207
208
  }
208
209
  }
209
210
  }
@@ -789,12 +790,14 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
789
790
  @JavaScriptActor
790
791
  internal var propNameIdsRegistry: [String: JavaScriptPropNameID] = [:]
791
792
 
792
- // MARK: - Deferred promise factory
793
+ // MARK: - Cache
793
794
 
794
- /// The JavaScript function ``JavaScriptPromise`` uses to create deferred promises, built on first
795
- /// use and released with the runtime. See `JavaScriptPromise.init(_:)` for why it exists.
795
+ /// Values cached with ``cached(_:_:)``. Unchecked exclusivity skips the dynamic access checks on
796
+ /// every lookup: the cache is only used on the JavaScript thread, and `cached(_:_:)` never keeps an
797
+ /// access open while it calls out, so accesses can't overlap.
796
798
  @JavaScriptActor
797
- internal var cachedDeferredPromiseFactory: JavaScriptValue?
799
+ @exclusivity(unchecked)
800
+ internal var cache = Cache()
798
801
 
799
802
  // MARK: - Long-lived objects
800
803
 
@@ -826,12 +829,12 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
826
829
  // hop back to the JavaScript thread first.
827
830
  JavaScriptActor.assumeIsolated {
828
831
  longLivedObjects.clear()
829
- // Also flush the cached `jsi::PropNameID`s and the deferred-promise factory: a non-owning
830
- // wrapper can outlive its runtime (e.g. captured by a task abandoned on reload) and would
831
- // otherwise destroy them against the freed runtime when it deallocates. `self` is weak so
832
- // the teardown object doesn't retain the wrapper; the owning wrapper clears both in `deinit`.
832
+ // Also flush the cached `jsi::PropNameID`s and the cache: a non-owning wrapper can outlive its
833
+ // runtime (e.g. captured by a task abandoned on reload) and would otherwise destroy them against
834
+ // the freed runtime when it deallocates. `self` is weak so the teardown object doesn't retain the
835
+ // wrapper; the owning wrapper clears both in `deinit`.
833
836
  self?.propNameIdsRegistry.removeAll()
834
- self?.cachedDeferredPromiseFactory = nil
837
+ self?.cache.clear()
835
838
  }
836
839
  }
837
840
  let object = createObject()
@@ -883,7 +886,8 @@ private func createFunctionClosure(
883
886
  let this = UnsafeMutablePointer(mutating: thisPtr).move()
884
887
  let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
885
888
  let thisValue = JavaScriptValue(runtime, this)
886
- try context.call(thisValue, consume arguments).writeJSIValue(to: resultPtr)
889
+ var result = try context.call(thisValue, consume arguments)
890
+ JavaScriptValue.write(&result, to: resultPtr)
887
891
  }
888
892
  }
889
893
  }
@@ -926,7 +930,8 @@ private func createFunctionClosure(
926
930
  return forwardingSwiftErrorsToJS(runtime: runtime) {
927
931
  let arguments = JavaScriptValuesBuffer(runtime, start: argumentsPtr, count: argumentsCount)
928
932
  let thisValue = JavaScriptUnownedValue(runtime.pointee, thisPtr)
929
- try context.call(thisValue, consume arguments).writeJSIValue(to: resultPtr)
933
+ var result = try context.call(thisValue, consume arguments)
934
+ JavaScriptValue.write(&result, to: resultPtr)
930
935
  }
931
936
  }
932
937
  }
@@ -0,0 +1,74 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import os
4
+
5
+ extension JavaScriptRuntime {
6
+ /// Values cached by a runtime, stored by their keys' indices. Isolated to the JavaScript thread
7
+ /// together with the runtime that owns it. Values are read and created through
8
+ /// ``JavaScriptRuntime/cached(_:_:)``.
9
+ public struct Cache: ~Copyable {
10
+ /// A key for a value that a runtime creates once and then reuses, such as a JavaScript constructor
11
+ /// or a property name. Declare keys as `static let`s and pass them to ``JavaScriptRuntime/cached(_:_:)``.
12
+ ///
13
+ /// Each key gets a fixed index from a process-wide counter when it is created, so a lookup reads one
14
+ /// array slot instead of hashing a string. Every runtime keeps its own values for the same keys.
15
+ public struct Key<Value: AnyObject>: Sendable {
16
+ internal let index: Int
17
+
18
+ public init() {
19
+ self.index = nextCacheKeyIndex.withLock { index in
20
+ defer { index += 1 }
21
+ return index
22
+ }
23
+ }
24
+ }
25
+
26
+ // A slot per key index, up to the highest index used so far in this runtime. Slots of keys that
27
+ // haven't been used here yet are `nil`. `ContiguousArray` stores the references directly, without
28
+ // the bridging checks that `Array` can do for class elements on Apple platforms.
29
+ private var storage = ContiguousArray<AnyObject?>()
30
+
31
+ internal func value<Value>(for key: Key<Value>) -> Value? {
32
+ guard key.index < storage.count, let value = storage[key.index] else {
33
+ return nil
34
+ }
35
+ // The key's type guarantees the type of the value stored under it.
36
+ return unsafeDowncast(value, to: Value.self)
37
+ }
38
+
39
+ internal mutating func store<Value>(_ value: Value, for key: Key<Value>) {
40
+ if key.index >= storage.count {
41
+ storage.append(contentsOf: repeatElement(nil, count: key.index - storage.count + 1))
42
+ }
43
+ storage[key.index] = value
44
+ }
45
+
46
+ /// Releases all cached values. They often hold JSI objects, which must be destroyed before the
47
+ /// runtime they belong to.
48
+ internal mutating func clear() {
49
+ storage.removeAll()
50
+ }
51
+ }
52
+
53
+ /// Returns the value cached under `key`, creating it with `make` the first time the key is used in
54
+ /// this runtime. If `make` throws, nothing is cached and the next call tries again.
55
+ ///
56
+ /// `make` may look up other cached values, for example to build a prototype from a cached
57
+ /// constructor.
58
+ @JavaScriptActor
59
+ public func cached<Value>(_ key: Cache.Key<Value>, _ make: () throws -> Value) rethrows -> Value {
60
+ if let value = cache.value(for: key) {
61
+ return value
62
+ }
63
+ // Call `make` without accessing the cache, so that the lookups it does itself don't overlap with
64
+ // an access in progress here.
65
+ let value = try make()
66
+ cache.store(value, for: key)
67
+ return value
68
+ }
69
+ }
70
+
71
+ /// The index for the next created ``JavaScriptRuntime/Cache/Key``. Keys can be created on any thread,
72
+ /// for example by initializing a `static let`, so the counter is behind a lock. That costs nothing on
73
+ /// the lookup path, since each key takes an index only once.
74
+ private let nextCacheKeyIndex = OSAllocatedUnfairLock(initialState: 0)
@@ -460,6 +460,32 @@ public struct JavaScriptArray: JavaScriptType, ~Copyable {
460
460
  return result
461
461
  }
462
462
 
463
+ /// Transforms each element like `map(_:)`, but lends each element to `transform` as a
464
+ /// `JavaScriptUnownedValue` instead of wrapping it in a new `JavaScriptValue`. An element is valid only
465
+ /// for the duration of its `transform` call and must not be stored or escaped.
466
+ public func mapUnowned<T>(_ transform: (borrowing JavaScriptUnownedValue) throws -> T) rethrows -> [T] {
467
+ guard let jsiRuntime else {
468
+ FatalError.runtimeLost()
469
+ }
470
+ let count = self.length
471
+ var result: [T] = []
472
+ result.reserveCapacity(count)
473
+ for index in 0..<count {
474
+ let element = pointee.getValueAtIndex(jsiRuntime, index)
475
+ // `withUnsafeBytes(of:)` rather than `withUnsafePointer(to:)`; see `JavaScriptValue.withUnsafePointee(_:)`.
476
+ try withUnsafeBytes(of: element) { bytes in
477
+ guard let baseAddress = bytes.baseAddress else {
478
+ preconditionFailure(
479
+ "withUnsafeBytes(of:) gave an empty buffer for a jsi::Value, which can't happen for a non-zero-sized type"
480
+ )
481
+ }
482
+ let pointer = baseAddress.assumingMemoryBound(to: facebook.jsi.Value.self)
483
+ result.append(try transform(JavaScriptUnownedValue(jsiRuntime, pointer)))
484
+ }
485
+ }
486
+ return result
487
+ }
488
+
463
489
  /// Converts the JavaScript array to a `JavaScriptValue`.
464
490
  ///
465
491
  /// - Returns: A `JavaScriptValue` representing this array
@@ -92,6 +92,14 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
92
92
  return pointee.isArrayBuffer(jsiRuntime)
93
93
  }
94
94
 
95
+ /// Returns `true` if the object is a typed array, such as `Uint8Array`.
96
+ internal func isTypedArray() -> Bool {
97
+ guard let runtime else {
98
+ FatalError.runtimeLost()
99
+ }
100
+ return expo.isTypedArray(runtime.pointee, pointee)
101
+ }
102
+
95
103
  /// Returns the object as an array buffer, or asserts if not an array buffer.
96
104
  public func getArrayBuffer() -> JavaScriptArrayBuffer {
97
105
  guard let runtime else {
@@ -138,6 +146,28 @@ public struct JavaScriptObject: JavaScriptType, Sendable, ~Copyable {
138
146
  return JavaScriptValue(runtimeHandle, pointee.getProperty(jsiRuntime, name.toJSIPropNameID(in: jsiRuntime)))
139
147
  }
140
148
 
149
+ /// Calls `body` with the property of the object with the given name, or `undefined` if there is no
150
+ /// such property, lent as a `JavaScriptUnownedValue` instead of wrapped in a new `JavaScriptValue`.
151
+ /// The value is valid only for the duration of the closure and must not be stored or escaped.
152
+ public func withUnownedProperty<R>(_ name: String, _ body: (borrowing JavaScriptUnownedValue) throws -> R) rethrows
153
+ -> R
154
+ {
155
+ guard let jsiRuntime else {
156
+ FatalError.runtimeLost()
157
+ }
158
+ let property = pointee.getProperty(jsiRuntime, name.toJSIPropNameID(in: jsiRuntime))
159
+ // `withUnsafeBytes(of:)` rather than `withUnsafePointer(to:)`; see `JavaScriptValue.withUnsafePointee(_:)`.
160
+ return try withUnsafeBytes(of: property) { bytes in
161
+ guard let baseAddress = bytes.baseAddress else {
162
+ preconditionFailure(
163
+ "withUnsafeBytes(of:) gave an empty buffer for a jsi::Value, which can't happen for a non-zero-sized type"
164
+ )
165
+ }
166
+ let pointer = baseAddress.assumingMemoryBound(to: facebook.jsi.Value.self)
167
+ return try body(JavaScriptUnownedValue(jsiRuntime, pointer))
168
+ }
169
+ }
170
+
141
171
  /// Returns the property of the object with the given prop name id,
142
172
  /// or `undefined` value if the name is not a property of the object.
143
173
  public func getProperty(_ propName: JavaScriptPropNameID) -> JavaScriptValue {
@@ -284,20 +284,20 @@ extension JavaScriptRuntime {
284
284
  /// optimize like any other closure.
285
285
  @JavaScriptActor
286
286
  fileprivate func deferredPromiseFactory() throws -> JavaScriptValue {
287
- if let factory = cachedDeferredPromiseFactory {
288
- return factory
287
+ return try cached(deferredPromiseFactoryKey) {
288
+ return try eval(
289
+ label: "expo-modules-jsi/deferred-promise.js",
290
+ """
291
+ (function () {
292
+ let resolve, reject;
293
+ const promise = new Promise(function (a, b) { resolve = a; reject = b; });
294
+ return [promise, resolve, reject];
295
+ })
296
+ """
297
+ )
289
298
  }
290
- let factory = try eval(
291
- label: "expo-modules-jsi/deferred-promise.js",
292
- """
293
- (function () {
294
- let resolve, reject;
295
- const promise = new Promise(function (a, b) { resolve = a; reject = b; });
296
- return [promise, resolve, reject];
297
- })
298
- """
299
- )
300
- cachedDeferredPromiseFactory = factory
301
- return factory
302
299
  }
303
300
  }
301
+
302
+ /// Key of the deferred promise factory in each runtime's cache.
303
+ private let deferredPromiseFactoryKey = JavaScriptRuntime.Cache.Key<JavaScriptValue>()
@@ -130,6 +130,23 @@ public struct JavaScriptUnownedValue: ~Copyable {
130
130
  return JavaScriptObject(runtime, pointer.pointee.getObject(self.runtime))
131
131
  }
132
132
 
133
+ /// Whether the value is an array. The zero-copy counterpart of ``JavaScriptValue/isArray()``.
134
+ public func isArray() -> Bool {
135
+ return pointer.pointee.isObject() && pointer.pointee.getObject(runtime).isArray(runtime)
136
+ }
137
+
138
+ /// Returns the value as a ``JavaScriptArray``, or asserts if it is not an array. The zero-copy
139
+ /// counterpart of ``JavaScriptValue/getArray()``, with the same runtime contract as
140
+ /// ``getObject(in:)``.
141
+ public func getArray(in runtime: JavaScriptRuntime) -> JavaScriptArray {
142
+ assert(isArray(), "Value is not an array")
143
+ assert(
144
+ Unmanaged.passUnretained(runtime.pointee).toOpaque() == Unmanaged.passUnretained(self.runtime).toOpaque(),
145
+ "`getArray(in:)` must be passed the runtime that owns the borrowed value"
146
+ )
147
+ return JavaScriptArray(runtime, pointer.pointee.getObject(self.runtime).getArray(self.runtime))
148
+ }
149
+
133
150
  // MARK: - Throwing conversions ("as functions")
134
151
 
135
152
  /// Returns the value as a boolean, or throws `TypeError` if it is not a boolean.