Skip to content

Commit 97aad00

Browse files
committedMar 5, 2025
Concurrency: Fix sendability errors around JSClosure.async
1 parent fa77908 commit 97aad00

File tree

2 files changed

+34
-20
lines changed

2 files changed

+34
-20
lines changed
 

‎Sources/JavaScriptKit/BasicObjects/JSPromise.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public final class JSPromise: JSBridgedClass {
9090
/// Schedules the `success` closure to be invoked on successful completion of `self`.
9191
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
9292
@discardableResult
93-
public func then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
93+
public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
9494
let closure = JSOneshotClosure.async {
9595
try await success($0[0]).jsValue
9696
}
@@ -101,8 +101,8 @@ public final class JSPromise: JSBridgedClass {
101101
/// Schedules the `success` closure to be invoked on successful completion of `self`.
102102
@discardableResult
103103
public func then(
104-
success: @escaping (JSValue) -> ConvertibleToJSValue,
105-
failure: @escaping (JSValue) -> ConvertibleToJSValue
104+
success: @escaping (sending JSValue) -> ConvertibleToJSValue,
105+
failure: @escaping (sending JSValue) -> ConvertibleToJSValue
106106
) -> JSPromise {
107107
let successClosure = JSOneshotClosure {
108108
success($0[0]).jsValue
@@ -117,8 +117,8 @@ public final class JSPromise: JSBridgedClass {
117117
/// Schedules the `success` closure to be invoked on successful completion of `self`.
118118
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
119119
@discardableResult
120-
public func then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue,
121-
failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise
120+
public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue,
121+
failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise
122122
{
123123
let successClosure = JSOneshotClosure.async {
124124
try await success($0[0]).jsValue
@@ -132,7 +132,7 @@ public final class JSPromise: JSBridgedClass {
132132

133133
/// Schedules the `failure` closure to be invoked on rejected completion of `self`.
134134
@discardableResult
135-
public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
135+
public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise {
136136
let closure = JSOneshotClosure {
137137
failure($0[0]).jsValue
138138
}
@@ -143,7 +143,7 @@ public final class JSPromise: JSBridgedClass {
143143
/// Schedules the `failure` closure to be invoked on rejected completion of `self`.
144144
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
145145
@discardableResult
146-
public func `catch`(failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
146+
public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
147147
let closure = JSOneshotClosure.async {
148148
try await failure($0[0]).jsValue
149149
}

‎Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift

+27-13
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public protocol JSClosureProtocol: JSValueCompatible {
1515
public class JSOneshotClosure: JSObject, JSClosureProtocol {
1616
private var hostFuncRef: JavaScriptHostFuncRef = 0
1717

18-
public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
18+
public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
1919
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
2020
super.init(id: 0)
2121

@@ -34,7 +34,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
3434

3535
#if compiler(>=5.5) && !hasFeature(Embedded)
3636
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
37-
public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure {
37+
public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure {
3838
JSOneshotClosure(makeAsyncClosure(body))
3939
}
4040
#endif
@@ -64,10 +64,10 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
6464
public class JSClosure: JSFunction, JSClosureProtocol {
6565

6666
class SharedJSClosure {
67-
private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:]
67+
private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: (sending [JSValue]) -> JSValue)] = [:]
6868
init() {}
6969

70-
subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: ([JSValue]) -> JSValue)? {
70+
subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: (sending [JSValue]) -> JSValue)? {
7171
get { storage[key] }
7272
set { storage[key] = newValue }
7373
}
@@ -93,7 +93,7 @@ public class JSClosure: JSFunction, JSClosureProtocol {
9393
})
9494
}
9595

96-
public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
96+
public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
9797
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
9898
super.init(id: 0)
9999

@@ -109,7 +109,7 @@ public class JSClosure: JSFunction, JSClosureProtocol {
109109

110110
#if compiler(>=5.5) && !hasFeature(Embedded)
111111
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
112-
public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure {
112+
public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure {
113113
JSClosure(makeAsyncClosure(body))
114114
}
115115
#endif
@@ -125,18 +125,29 @@ public class JSClosure: JSFunction, JSClosureProtocol {
125125

126126
#if compiler(>=5.5) && !hasFeature(Embedded)
127127
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
128-
private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) {
128+
private func makeAsyncClosure(
129+
_ body: sending @escaping (sending [JSValue]) async throws -> JSValue
130+
) -> ((sending [JSValue]) -> JSValue) {
129131
{ arguments in
130132
JSPromise { resolver in
133+
// NOTE: The context is fully transferred to the unstructured task
134+
// isolation but the compiler can't prove it yet, so we need to
135+
// use `@unchecked Sendable` to make it compile with the Swift 6 mode.
136+
struct Context: @unchecked Sendable {
137+
let resolver: (JSPromise.Result) -> Void
138+
let arguments: [JSValue]
139+
let body: (sending [JSValue]) async throws -> JSValue
140+
}
141+
let context = Context(resolver: resolver, arguments: arguments, body: body)
131142
Task {
132143
do {
133-
let result = try await body(arguments)
134-
resolver(.success(result))
144+
let result = try await context.body(context.arguments)
145+
context.resolver(.success(result))
135146
} catch {
136147
if let jsError = error as? JSError {
137-
resolver(.failure(jsError.jsValue))
148+
context.resolver(.failure(jsError.jsValue))
138149
} else {
139-
resolver(.failure(JSError(message: String(describing: error)).jsValue))
150+
context.resolver(.failure(JSError(message: String(describing: error)).jsValue))
140151
}
141152
}
142153
}
@@ -183,13 +194,16 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa
183194
@_cdecl("_call_host_function_impl")
184195
func _call_host_function_impl(
185196
_ hostFuncRef: JavaScriptHostFuncRef,
186-
_ argv: UnsafePointer<RawJSValue>, _ argc: Int32,
197+
_ argv: sending UnsafePointer<RawJSValue>, _ argc: Int32,
187198
_ callbackFuncRef: JavaScriptObjectRef
188199
) -> Bool {
189200
guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else {
190201
return true
191202
}
192-
let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { $0.jsValue}
203+
var arguments: [JSValue] = []
204+
for i in 0..<Int(argc) {
205+
arguments.append(argv[i].jsValue)
206+
}
193207
let result = hostFunc(arguments)
194208
let callbackFuncRef = JSFunction(id: callbackFuncRef)
195209
_ = callbackFuncRef(result)

0 commit comments

Comments
 (0)
Please sign in to comment.