Skip to content

Commit d315b69

Browse files
Add @JSClass support to JSRemote
1 parent 403ae95 commit d315b69

1 file changed

Lines changed: 88 additions & 24 deletions

File tree

Sources/JavaScriptEventLoop/JSRemote.swift

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,39 @@ public struct JSRemote<T>: @unchecked Sendable {
4242

4343
private let storage: Storage
4444

45-
fileprivate init(sourceObject: JSObject, sourceTid: Int32) {
45+
fileprivate init(sourceObject: JSObject) {
46+
let sourceTid: Int32
47+
#if compiler(>=6.1) && _runtime(_multithreaded)
48+
sourceTid = sourceObject.ownerTid
49+
#else
50+
sourceTid = -1
51+
#endif
4652
self.storage = Storage(sourceObject: sourceObject, sourceTid: sourceTid)
4753
}
54+
55+
fileprivate func _withJSObject<R: Sendable, E: Error>(
56+
_ body: @Sendable @escaping (JSObject) throws(E) -> R
57+
) async throws(E) -> sending R {
58+
#if compiler(>=6.1) && _runtime(_multithreaded)
59+
if storage.sourceTid == swjs_get_worker_thread_id_cached() {
60+
return try body(storage.sourceObject)
61+
}
62+
let result: Result<R, E> = await withCheckedContinuation { continuation in
63+
let context = _JSRemoteContext(
64+
sourceObject: storage.sourceObject,
65+
body: body,
66+
continuation: continuation
67+
)
68+
swjs_request_remote_jsobject_body(
69+
storage.sourceTid,
70+
Unmanaged.passRetained(context).toOpaque()
71+
)
72+
}
73+
return try result.get()
74+
#else
75+
return try body(storage.sourceObject)
76+
#endif
77+
}
4878
}
4979

5080
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@@ -62,11 +92,7 @@ extension JSRemote where T == JSObject {
6292
///
6393
/// - Parameter object: The JavaScript object to reference remotely.
6494
public init(_ object: JSObject) {
65-
#if compiler(>=6.1) && _runtime(_multithreaded)
66-
self.init(sourceObject: object, sourceTid: object.ownerTid)
67-
#else
68-
self.init(sourceObject: object, sourceTid: -1)
69-
#endif
95+
self.init(sourceObject: object)
7096
}
7197

7298
/// Performs an operation with the underlying `JSObject` on its owning thread.
@@ -92,28 +118,66 @@ extension JSRemote where T == JSObject {
92118
public func withJSObject<R: Sendable, E: Error>(
93119
_ body: @Sendable @escaping (JSObject) throws(E) -> R
94120
) async throws(E) -> sending R {
95-
#if compiler(>=6.1) && _runtime(_multithreaded)
96-
if storage.sourceTid == swjs_get_worker_thread_id_cached() {
97-
return try body(storage.sourceObject)
98-
}
99-
let result: Result<R, E> = await withCheckedContinuation { continuation in
100-
let context = _JSRemoteContext(
101-
sourceObject: storage.sourceObject,
102-
body: body,
103-
continuation: continuation
104-
)
105-
swjs_request_remote_jsobject_body(
106-
storage.sourceTid,
107-
Unmanaged.passRetained(context).toOpaque()
108-
)
121+
try await _withJSObject(body)
122+
}
123+
}
124+
125+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
126+
extension JSRemote where T: _JSBridgedClass {
127+
/// Creates a remote handle for a `@JSClass`-imported object.
128+
///
129+
/// The object remains owned by its current JavaScript thread. Access it later by calling
130+
/// `withJSObject(_:)`, which executes the closure on the owning thread when necessary.
131+
///
132+
/// ## Example
133+
///
134+
/// ```swift
135+
/// @JSClass struct Window {
136+
/// @JSGetter var location: Location
137+
/// }
138+
/// let remoteWindow = JSRemote(Window(unsafelyWrapping: JSObject.global))
139+
/// remoteWindow.withJSObject { window in
140+
/// print(window.location.href.string ?? "")
141+
/// }
142+
/// ```
143+
///
144+
/// - Parameter object: The JavaScript object to reference remotely.
145+
public init(_ object: T) {
146+
self.init(sourceObject: object.jsObject)
147+
}
148+
149+
150+
/// Performs an operation with the underlying `T` object on its owning thread.
151+
///
152+
/// If the caller is already running on the thread that owns the object, `body` executes
153+
/// immediately. Otherwise, this method asynchronously requests execution on the owner and
154+
/// resumes when the closure completes.
155+
///
156+
/// Use this API when the object must stay on its original thread but a result derived from
157+
/// that object needs to be produced in another Swift concurrency context.
158+
///
159+
/// ## Example
160+
///
161+
/// ```swift
162+
/// let location = try await remoteWindow.withJSObject { window in
163+
/// window.location.href.string ?? ""
164+
/// }
165+
/// ```
166+
///
167+
/// - Parameter body: A sendable closure that receives the owned `T` object.
168+
/// - Returns: The value produced by `body`.
169+
/// - 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)
109176
}
110-
return try result.get()
111-
#else
112-
return try body(storage.sourceObject)
113-
#endif
114177
}
115178
}
116179

180+
117181
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
118182
private final class _JSRemoteContext: @unchecked Sendable {
119183
let invokeBody: () -> Bool

0 commit comments

Comments
 (0)