@@ -60,7 +60,7 @@ public struct JSRemote<T>: @unchecked Sendable {
6060 return try body ( storage. sourceObject)
6161 }
6262 let result : Result < R , E > = await withCheckedContinuation { continuation in
63- let context = _JSRemoteContext (
63+ let context = _JSRemoteSyncContext (
6464 sourceObject: storage. sourceObject,
6565 body: body,
6666 continuation: continuation
@@ -75,6 +75,33 @@ public struct JSRemote<T>: @unchecked Sendable {
7575 return try body ( storage. sourceObject)
7676 #endif
7777 }
78+
79+ #if compiler(>=6.1) && hasFeature(Embedded) && _runtime(_multithreaded)
80+ #else
81+ fileprivate func _withJSObject< R: Sendable , E: Error > (
82+ _ body: @Sendable @escaping ( JSObject) async throws ( E ) -> R
83+ ) async throws ( E) -> sending R {
84+ #if compiler(>=6.1) && _runtime(_multithreaded)
85+ if storage. sourceTid == swjs_get_worker_thread_id_cached ( ) {
86+ return try await body ( storage. sourceObject)
87+ }
88+ let result : Result < R , E > = await withCheckedContinuation { continuation in
89+ let context = _JSRemoteAsyncContext (
90+ sourceObject: storage. sourceObject,
91+ body: body,
92+ continuation: continuation
93+ )
94+ swjs_request_remote_jsobject_body (
95+ storage. sourceTid,
96+ Unmanaged . passRetained ( context) . toOpaque ( )
97+ )
98+ }
99+ return try result. get ( )
100+ #else
101+ return try await body ( storage. sourceObject)
102+ #endif
103+ }
104+ #endif
78105}
79106
80107@available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
@@ -120,6 +147,35 @@ extension JSRemote where T == JSObject {
120147 ) async throws ( E) -> sending R {
121148 try await _withJSObject ( body)
122149 }
150+
151+ #if compiler(>=6.1) && hasFeature(Embedded) && _runtime(_multithreaded)
152+ #else
153+ /// Performs an asynchronous operation with the underlying `JSObject` on its owning thread.
154+ ///
155+ /// If the caller is already running on the thread that owns the object, `body` executes
156+ /// immediately. Otherwise, this method asynchronously requests execution on the owner and
157+ /// resumes when the closure completes.
158+ ///
159+ /// Use this API when the object must stay on its original thread but producing a result
160+ /// requires suspending, such as awaiting a JavaScript promise.
161+ ///
162+ /// ## Example
163+ ///
164+ /// ```swift
165+ /// let value = try await remoteWindow.withJSObject { window in
166+ /// try await JSPromise(from: window.fetch!("/api").object!)!.value
167+ /// }
168+ /// ```
169+ ///
170+ /// - Parameter body: A sendable asynchronous closure that receives the owned `JSObject`.
171+ /// - Returns: The value produced by `body`.
172+ /// - Throws: Any error thrown by `body`.
173+ public func withJSObject< R: Sendable , E: Error > (
174+ _ body: @Sendable @escaping ( JSObject) async throws ( E ) -> R
175+ ) async throws ( E) -> sending R {
176+ try await _withJSObject ( body)
177+ }
178+ #endif
123179}
124180
125181@available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
@@ -167,39 +223,157 @@ extension JSRemote where T: _JSBridgedClass {
167223 /// - Parameter body: A sendable closure that receives the owned `T` object.
168224 /// - Returns: The value produced by `body`.
169225 /// - Throws: Any error thrown by `body`.
170- public func withJSObject< R: Sendable , E: Error > (
171- _ body: @Sendable @escaping ( T) throws( E ) -> R
172- ) async throws ( E) -> sending R where T: SendableMetatype {
173- try await _withJSObject { jsObject throws ( E) -> R in
174- let object = T ( unsafelyWrapping: jsObject)
175- return try body ( object)
226+ #if compiler(>=6.2)
227+ public func withJSObject< R: Sendable , E: Error > (
228+ _ body: @Sendable @escaping ( T) throws( E ) -> R
229+ ) async throws ( E) -> sending R where T: SendableMetatype {
230+ try await _withJSObject { jsObject throws ( E) -> R in
231+ let object = T ( unsafelyWrapping: jsObject)
232+ return try body ( object)
233+ }
176234 }
177- }
235+ #else
236+ public func withJSObject< R: Sendable , E: Error > (
237+ _ body: @Sendable @escaping ( T) throws( E ) -> R
238+ ) async throws ( E) -> sending R {
239+ try await _withJSObject { jsObject throws ( E) -> R in
240+ let object = T ( unsafelyWrapping: jsObject)
241+ return try body ( object)
242+ }
243+ }
244+ #endif
245+
246+ #if compiler(>=6.1) && hasFeature(Embedded) && _runtime(_multithreaded)
247+ #else
248+ /// Performs an asynchronous operation with the underlying `T` object on its owning thread.
249+ ///
250+ /// If the caller is already running on the thread that owns the object, `body` executes
251+ /// immediately. Otherwise, this method asynchronously requests execution on the owner and
252+ /// resumes when the closure completes.
253+ ///
254+ /// Use this API when the object must stay on its original thread but producing a result
255+ /// requires suspending, such as awaiting a JavaScript promise.
256+ ///
257+ /// ## Example
258+ ///
259+ /// ```swift
260+ /// let response = try await remoteWindow.withJSObject { window in
261+ /// try await window.fetch("/api").value
262+ /// }
263+ /// ```
264+ ///
265+ /// - Parameter body: A sendable asynchronous closure that receives the owned `T` object.
266+ /// - Returns: The value produced by `body`.
267+ /// - Throws: Any error thrown by `body`.
268+ #if compiler(>=6.2)
269+ public func withJSObject< R: Sendable , E: Error > (
270+ _ body: @Sendable @escaping ( T) async throws ( E ) -> R
271+ ) async throws ( E) -> sending R where T: SendableMetatype {
272+ try await _withJSObject { jsObject async throws ( E) -> R in
273+ let object = T ( unsafelyWrapping: jsObject)
274+ return try await body ( object)
275+ }
276+ }
277+ #else
278+ public func withJSObject< R: Sendable , E: Error > (
279+ _ body: @Sendable @escaping ( T) async throws ( E ) -> R
280+ ) async throws ( E) -> sending R {
281+ try await _withJSObject { jsObject async throws ( E) -> R in
282+ let object = T ( unsafelyWrapping: jsObject)
283+ return try await body ( object)
284+ }
285+ }
286+ #endif
287+ #endif
178288}
179289
180290
181291@available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
182- private final class _JSRemoteContext : @unchecked Sendable {
183- let invokeBody : ( ) -> Bool
292+ private class _JSRemoteContext : @unchecked Sendable {
293+ fileprivate func invoke( ) {
294+ preconditionFailure ( " JSRemote context subclasses must override invoke() " )
295+ }
296+ }
297+
298+ @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
299+ private final class _JSRemoteSyncContext < R: Sendable , E: Error > : _JSRemoteContext , @unchecked Sendable {
300+ let sourceObject : JSObject
301+ let body : @Sendable ( JSObject) throws( E ) -> R
302+ let continuation : CheckedContinuation < Result < R , E > , Never >
184303
185- init < R : Sendable , E : Error > (
304+ init (
186305 sourceObject: JSObject ,
187306 body: @escaping @Sendable ( JSObject) throws( E ) -> R ,
188307 continuation: CheckedContinuation < Result < R , E > , Never >
189308 ) {
190- self . invokeBody = {
191- // NOTE: Sendability violation here for `sourceObject`
309+ self . sourceObject = sourceObject
310+ self . body = body
311+ self . continuation = continuation
312+ }
313+
314+ override fileprivate func invoke( ) {
315+ // NOTE: Sendability violation here for `sourceObject`.
316+ // Even though `JSObject` is not Sendable, it is safe to access it here
317+ // because this method will only be executed on the owning thread.
318+ do throws ( E) {
319+ continuation. resume ( returning: . success( try body ( sourceObject) ) )
320+ } catch {
321+ continuation. resume ( returning: . failure( error) )
322+ }
323+ }
324+ }
325+
326+ #if compiler(>=6.1) && hasFeature(Embedded) && _runtime(_multithreaded)
327+ #else
328+ @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
329+ private final class _JSRemoteAsyncContext < R: Sendable , E: Error > : _JSRemoteContext , @unchecked Sendable {
330+ let sourceObject : JSObject
331+ let body : @Sendable ( JSObject) async throws ( E ) -> R
332+ let continuation : CheckedContinuation < Result < R , E > , Never >
333+
334+ init (
335+ sourceObject: JSObject ,
336+ body: @escaping @Sendable ( JSObject) async throws ( E ) -> R ,
337+ continuation: CheckedContinuation < Result < R , E > , Never >
338+ ) {
339+ self . sourceObject = sourceObject
340+ self . body = body
341+ self . continuation = continuation
342+ }
343+
344+ override fileprivate func invoke( ) {
345+ _runJSRemoteBody {
346+ await self . invokeAsync ( )
347+ }
348+ }
349+
350+ private func invokeAsync( ) async {
351+ // NOTE: Sendability violation here for `sourceObject`.
192352 // Even though `JSObject` is not Sendable, it is safe to access it here
193- // because this invokeBody closure will only be executed on the owning thread.
353+ // because this method will only be executed on the owning thread.
194354 do throws ( E) {
195- continuation. resume ( returning: . success( try body ( sourceObject) ) )
355+ continuation. resume ( returning: . success( try await body ( sourceObject) ) )
196356 } catch {
197357 continuation. resume ( returning: . failure( error) )
198358 }
199- return false
200359 }
201360 }
202- }
361+
362+ @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
363+ private func _runJSRemoteBody( _ body: @escaping @Sendable ( ) async -> Void ) {
364+ #if compiler(>=6.0) && !hasFeature(Embedded)
365+ if #available( macOS 15 . 0 , iOS 18 . 0 , watchOS 11 . 0 , tvOS 18 . 0 , visionOS 2 . 0 , * ) {
366+ Task ( executorPreference: WebWorkerTaskExecutor . currentExecutorPreference) {
367+ await body ( )
368+ }
369+ return
370+ }
371+ #endif
372+ Task {
373+ await body ( )
374+ }
375+ }
376+ #endif
203377
204378#if compiler(>=6.1)
205379@_expose ( wasm, " swjs_invoke_remote_jsobject_body " )
@@ -211,7 +385,8 @@ func _swjs_invoke_remote_jsobject_body(_ contextPtr: UnsafeRawPointer?) -> Bool
211385 guard let contextPtr else { return true }
212386 let context = Unmanaged < _JSRemoteContext > . fromOpaque ( contextPtr) . takeRetainedValue ( )
213387
214- return context. invokeBody ( )
388+ context. invoke ( )
389+ return false
215390 #else
216391 _ = contextPtr
217392 return true
0 commit comments