Repository navigation
Support cancellation of tasks with FetchHTTPClient #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,13 @@ import JavaScriptKit | |
| /// # Javascript Imports | ||
| /// This file defines the Javascript classes and functions imported into Swift. | ||
|
|
||
| // https://developer.mozilla.org/en-US/docs/Web/API/AbortController | ||
| @JSClass(from: .global) struct AbortController: @unchecked Sendable { | ||
| @JSGetter var signal: JSObject? | ||
| @JSFunction init() throws(JSException) | ||
| @JSFunction func abort() throws(JSException) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these functions safe to call from any threads?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm no expert on how this translates to JS, but isn't JS single-threaded, making WASM single-threaded, making all the asynchronous contexts in Swift also single-threaded?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that multithreading is supported in WASM: https://webassembly.org/features/
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Today I learned :) @MaxDesiatov might know more about this specific question...
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our Swift SDK for Wasm is single-threaded by default. Multi-threading is available as an option, so I don't think
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does JS thread map to Swift concurrency? There is no guarantee of the thread that an async method runs on since threads can switch arbitrarily after an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MainActor-isolated code is consistently mapped to the browser main thread in any case, and there is no guarantee that any other async code runs on. However, practically speaking, JavaScriptKit does not provide a global concurrent executor even under a multithreading environment, and it just provides I took a closer look on the code, and I can say After swiftwasm/JavaScriptKit#747 lands, we can fix the potential thread issue by the following patch. Patchdiff --git a/Sources/FetchHTTPClient/FetchHTTPClient.swift b/Sources/FetchHTTPClient/FetchHTTPClient.swift
index cb6825e..d5c4bc4 100644
--- a/Sources/FetchHTTPClient/FetchHTTPClient.swift
+++ b/Sources/FetchHTTPClient/FetchHTTPClient.swift
@@ -120,7 +120,7 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient {
return try await responseHandler(
HTTPResponse(status: .init(code: responseStatus, reasonPhrase: responseStatusText), headerFields: responseHeaders),
- ResponseReader(reader: reader)
+ ResponseReader(reader: JSRemote(reader))
)
}
@@ -145,7 +145,7 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient {
}
public struct ResponseReader: ConcludingAsyncReader, ~Copyable {
- let reader: ReadableStreamDefaultReader
+ let reader: JSRemote<ReadableStreamDefaultReader>
public consuming func consumeAndConclude<Return, Failure>(
body: nonisolated(nonsending) (consuming sending FetchHTTPClient.ResponseBodyReader) async throws(Failure) -> Return
@@ -159,7 +159,7 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient {
public typealias ReadFailure = any Error
public typealias Buffer = UniqueArray<UInt8>
- let reader: ReadableStreamDefaultReader
+ let reader: JSRemote<ReadableStreamDefaultReader>
var buffer = UniqueArray<UInt8>()
public mutating func read<Return: ~Copyable, Failure>(
@@ -167,26 +167,35 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient {
+ let reader: JSRemote<ReadableStreamDefaultReader>
var buffer = UniqueArray<UInt8>()
public mutating func read<Return: ~Copyable, Failure>(
@@ -167,26 +167,35 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient {
) async throws(AsyncStreaming.EitherError<any Error, Failure>) -> Return where Failure: Error {
let chunk: Chunk
do {
- chunk = try await self.reader.read()
+ chunk = try await reader.withJSObject { reader in
+ try await reader.read()
+ }
+ } catch {
+ throw .first(error)
+ }
+ do {
+ try self.append(chunk: chunk)
} catch {
throw .first(error)
}
+ do {
+ return try await body(&self.buffer)
+ } catch {
+ throw .second(error)
+ }
+ }
+
+ private mutating func append(chunk: Chunk) throws {
if !chunk.done {
guard let bytes = chunk.value, !bytes.isEmpty else {
// If not done, there must be bytes that can be read
- throw .first(FetchError.BadAssumptionJS)
+ throw FetchError.BadAssumptionJS
}
buffer.reserveCapacity(bytes.count)
for b in bytes {
self.buffer.append(b)
}
}
-
- do {
- return try await body(&self.buffer)
- } catch {
- throw .second(error)
- }
}
}
}
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hope to understand a bit more how this works. Is Wondering if there is an efficiency difference between wrapping the reader in JSRemote or just slapping MainActor on everything to let Swift handle thread hopping.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we get around by making it non-Sendable in this PR (
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we can So I think this comes down to the concurrency threads that are used by the executors here. Not sure what |
||
| } | ||
|
|
||
| // https://developer.mozilla.org/en-US/docs/Web/API/Headers | ||
| @JSClass(from: .global) struct Headers { | ||
| @JSFunction init() throws(JSException) | ||
|
|
@@ -58,11 +65,13 @@ import JavaScriptKit | |
| let body: JSObject? | ||
| let method: String? | ||
| let headers: Headers? | ||
| let signal: JSObject? | ||
|
|
||
| init(body: JSObject?, method: String?, headers: Headers?) { | ||
| init(body: JSObject?, method: String?, headers: Headers?, signal: JSObject?) { | ||
| self.body = body | ||
| self.method = method | ||
| self.headers = headers | ||
| self.signal = signal | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -76,6 +85,7 @@ import JavaScriptKit | |
| // TODO: Find a way to remove the @unchecked. This object has to be moved through the different Swift reader types. | ||
| @JSClass(from: .global) struct ReadableStreamDefaultReader: @unchecked Sendable { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto, |
||
| @JSFunction func read() async throws(JSException) -> Chunk | ||
| @JSFunction func releaseLock() throws(JSException) | ||
| } | ||
|
|
||
| // https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we re-write this example to avoid top level code and use
@maininstead. Then we avoid the unstructured tasks as well.