diff --git a/Examples/WASMClient/main.swift b/Examples/WASMClient/main.swift index 258bcbb..34f6a12 100644 --- a/Examples/WASMClient/main.swift +++ b/Examples/WASMClient/main.swift @@ -22,7 +22,6 @@ import JavaScriptKit typealias DefaultExecutorFactory = JavaScriptEventLoop JavaScriptEventLoop.installGlobalExecutor() -let client = FetchHTTPClient() let status = Status() // Ask the user for the URL string. @@ -52,9 +51,10 @@ if method == .post || method == .put { } } -status.set("⏳ Making \(method) request to \(url)") +func makeRequest(url: URL, method: HTTPRequest.Method, body: HTTPClientRequestBody?) async throws { + let client = FetchHTTPClient() + status.set("⏳ Making \(method) request to \(url)") -do { try await client.perform( request: .init( method: method, @@ -117,6 +117,21 @@ do { div("") } } +} + +let requestTask = Task { + try await makeRequest(url: url, method: method, body: body) +} + +let sleepTask = Task { + status.set("💤 Waiting for 10 seconds") + try await Task.sleep(for: .seconds(10)) + status.set("🚫 Cancelling task") + requestTask.cancel() +} + +do { + try await requestTask.value } catch { status.set("❌ Fetch failed: \(error)") } diff --git a/Sources/FetchHTTPClient/FetchHTTPClient.swift b/Sources/FetchHTTPClient/FetchHTTPClient.swift index 33bc7bf..e59a644 100644 --- a/Sources/FetchHTTPClient/FetchHTTPClient.swift +++ b/Sources/FetchHTTPClient/FetchHTTPClient.swift @@ -83,8 +83,21 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient { } // Perform the request - let requestInit = RequestInit(body: jsBody, method: request.method.rawValue, headers: requestHeaders) - let response = try await fetch(url.absoluteString, requestInit) + let abortController = try AbortController() + let signal = try abortController.signal + let requestInit = RequestInit(body: jsBody, method: request.method.rawValue, headers: requestHeaders, signal: signal) + + // No matter what, we should abort the request at the end of this function + defer { + try? abortController.abort() + } + + let response = try await withTaskCancellationHandler { + return try await fetch(url.absoluteString, requestInit) + } onCancel: { + try? abortController.abort() + } + let responseStatus = try response.status let responseStatusText = try response.statusText let stream = try response.body @@ -163,8 +176,16 @@ public final class FetchHTTPClient: HTTPAPIs.HTTPClient { if buffer.isEmpty { // Read more data in from JS let chunk: Chunk + + // TODO: Find a way to make this safe. + nonisolated(unsafe) let reader = reader + do { - chunk = try await reader.read() + chunk = try await withTaskCancellationHandler { + try await reader.read() + } onCancel: { + try? reader.releaseLock() + } } catch { throw .first(error) } diff --git a/Sources/FetchHTTPClient/JSImports.swift b/Sources/FetchHTTPClient/JSImports.swift index c474484..48c1559 100644 --- a/Sources/FetchHTTPClient/JSImports.swift +++ b/Sources/FetchHTTPClient/JSImports.swift @@ -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) +} + // 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 { @JSFunction func read() async throws(JSException) -> Chunk + @JSFunction func releaseLock() throws(JSException) } // https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream