forked from swiftwasm/JavaScriptKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJSRemote.swift
More file actions
155 lines (145 loc) · 5.6 KB
/
JSRemote.swift
File metadata and controls
155 lines (145 loc) · 5.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import _Concurrency
@_spi(JSObject_id) import JavaScriptKit
import _CJavaScriptKit
/// A sendable handle for temporarily accessing a `JSObject` on its owning thread.
///
/// `JSRemote` lets you share a reference to a JavaScript object across Swift concurrency
/// domains without transferring or cloning the object itself. Instead, the object stays
/// owned by its original JavaScript thread, and `withJSObject(_:)` schedules a closure to
/// run on that owner when needed.
///
/// This is useful when you need occasional coordinated access to a JavaScript object from
/// another thread, but cannot or should not move the object with `JSSending`.
///
/// - Note: `JSRemote` does not make the underlying `JSObject` itself thread-safe. The object
/// may only be touched inside `withJSObject(_:)`.
///
/// ## Example
///
/// ```swift
/// let document = JSObject.global.document.object!
/// let remoteDocument = JSRemote(document)
///
/// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
/// let title = try await Task(executorPreference: executor) {
/// try await remoteDocument.withJSObject { document in
/// document.title.string ?? ""
/// }
/// }.value
/// ```
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct JSRemote<T>: @unchecked Sendable {
private final class Storage {
let sourceObject: JSObject
let sourceTid: Int32
init(sourceObject: JSObject, sourceTid: Int32) {
self.sourceObject = sourceObject
self.sourceTid = sourceTid
}
}
private let storage: Storage
fileprivate init(sourceObject: JSObject, sourceTid: Int32) {
self.storage = Storage(sourceObject: sourceObject, sourceTid: sourceTid)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension JSRemote where T == JSObject {
/// Creates a remote handle for a `JSObject`.
///
/// The object remains owned by its current JavaScript thread. Access it later by calling
/// `withJSObject(_:)`, which executes the closure on the owning thread when necessary.
///
/// ## Example
///
/// ```swift
/// let remoteWindow = JSRemote(JSObject.global)
/// ```
///
/// - Parameter object: The JavaScript object to reference remotely.
public init(_ object: JSObject) {
#if compiler(>=6.1) && _runtime(_multithreaded)
self.init(sourceObject: object, sourceTid: object.ownerTid)
#else
self.init(sourceObject: object, sourceTid: -1)
#endif
}
/// Performs an operation with the underlying `JSObject` on its owning thread.
///
/// If the caller is already running on the thread that owns the object, `body` executes
/// immediately. Otherwise, this method asynchronously requests execution on the owner and
/// resumes when the closure completes.
///
/// Use this API when the object must stay on its original thread but a result derived from
/// that object needs to be produced in another Swift concurrency context.
///
/// ## Example
///
/// ```swift
/// let location = try await remoteWindow.withJSObject { window in
/// window.location.href.string ?? ""
/// }
/// ```
///
/// - Parameter body: A sendable closure that receives the owned `JSObject`.
/// - Returns: The value produced by `body`.
/// - Throws: Any error thrown by `body`.
public func withJSObject<R: Sendable, E: Error>(
_ body: @Sendable @escaping (JSObject) throws(E) -> R
) async throws(E) -> sending R {
#if compiler(>=6.1) && _runtime(_multithreaded)
if storage.sourceTid == swjs_get_worker_thread_id_cached() {
return try body(storage.sourceObject)
}
let result: Result<R, E> = await withCheckedContinuation { continuation in
let context = _JSRemoteContext(
sourceObject: storage.sourceObject,
body: body,
continuation: continuation
)
swjs_request_remote_jsobject_body(
storage.sourceTid,
Unmanaged.passRetained(context).toOpaque()
)
}
return try result.get()
#else
return try body(storage.sourceObject)
#endif
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
private final class _JSRemoteContext: @unchecked Sendable {
let invokeBody: () -> Bool
init<R: Sendable, E: Error>(
sourceObject: JSObject,
body: @escaping @Sendable (JSObject) throws(E) -> R,
continuation: CheckedContinuation<Result<R, E>, Never>
) {
self.invokeBody = {
// NOTE: Sendability violation here for `sourceObject`
// Even though `JSObject` is not Sendable, it is safe to access it here
// because this invokeBody closure will only be executed on the owning thread.
do throws(E) {
continuation.resume(returning: .success(try body(sourceObject)))
} catch {
continuation.resume(returning: .failure(error))
}
return false
}
}
}
#if compiler(>=6.1)
@_expose(wasm, "swjs_invoke_remote_jsobject_body")
@_cdecl("swjs_invoke_remote_jsobject_body")
#endif
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func _swjs_invoke_remote_jsobject_body(_ contextPtr: UnsafeRawPointer?) -> Bool {
#if compiler(>=6.1) && _runtime(_multithreaded)
guard let contextPtr else { return true }
let context = Unmanaged<_JSRemoteContext>.fromOpaque(contextPtr).takeRetainedValue()
return context.invokeBody()
#else
_ = contextPtr
return true
#endif
}