-
-
Notifications
You must be signed in to change notification settings - Fork 69
Description
Motivation
JSObject is intentionally thread-bound in JavaScriptKit. In multi-threaded WebAssembly builds, each worker owns its own JavaScript object space, and JSObject enforces that ownership at runtime via ownerTid.
This is the right default for safety, but it leaves a gap for cases where Swift code wants to keep a stable reference to a JavaScript object while interacting with it from another isolation domain. Today the only public cross-thread escape hatch is JSSending, which transfers or clones the object into the destination thread. That is useful when ownership should move, but it does not help when the caller wants to keep using the original object on its original owner thread.
We need a small, explicit abstraction for that use case: a sendable handle that can be passed across threads, while only exposing the underlying JSObject through an async hop back to the owner thread.
Overview
This proposal introduces JSRemote<T> in JavaScriptEventLoop.
The initial scope is intentionally narrow:
- The first version is effectively
JSRemote<JSObject>. - The handle is
Sendable. - It retains the original owner-side
JSObject. - It stores the owning thread ID.
- The wrapped object is only accessible through an async API that executes a closure on the owner thread.
The key design constraints are:
JSObjectitself should remain non-Sendable- the public API should make cross-thread access explicit
- the wrapped object should not be cloned or transferred just to perform a temporary access
- the implementation should reuse the existing ITC request-routing mechanism already used by
JSSending
API Design (draft)
The first public shape would be:
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct JSRemote<T>: @unchecked Sendable
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension JSRemote where T == JSObject {
public init(_ object: JSObject)
public func withJSObject<R: Sendable>(
_ body: (JSObject) -> R
) async -> R
}Usage:
let remote = JSRemote(document)
let title = await remote.withJSObject { document in
document.title.string ?? ""
}Semantics:
withJSObjectalways executesbodyon theJSObjectowner thread- the method is
asyncbecause it may need to hop threads - the closure itself is synchronous and cannot suspend while borrowing the object
- the return type is constrained to
Sendable, so raw JS references do not escape back across threads