Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 26 additions & 32 deletions Sources/Functions/FunctionsClient.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ConcurrencyExtras
import Foundation
import HTTPTypes

Expand All @@ -9,11 +8,12 @@ import HTTPTypes
let version = Helpers.version

/// An actor representing a client for invoking functions.
public final class FunctionsClient: Sendable {
public actor FunctionsClient {
/// Fetch handler used to make requests.
public typealias FetchHandler = @Sendable (_ request: URLRequest) async throws -> (
Data, URLResponse
)
public typealias FetchHandler =
@Sendable (_ request: URLRequest) async throws -> (
Data, URLResponse
)

/// Request idle timeout: 150s (If an Edge Function doesn't send a response before the timeout, 504 Gateway Timeout will be returned)
///
Expand All @@ -26,19 +26,12 @@ public final class FunctionsClient: Sendable {
/// The Region to invoke the functions in.
let region: FunctionRegion?

struct MutableState {
/// Headers to be included in the requests.
var headers = HTTPFields()
}
/// Headers to be included in the requests.
var headers = HTTPFields()

private let http: any HTTPClientType
private let mutableState = LockIsolated(MutableState())
private let sessionConfiguration: URLSessionConfiguration

var headers: HTTPFields {
mutableState.headers
}

/// Initializes a new instance of `FunctionsClient`.
///
/// - Parameters:
Expand All @@ -47,8 +40,7 @@ public final class FunctionsClient: Sendable {
/// - region: The Region to invoke the functions in.
/// - logger: SupabaseLogger instance to use.
/// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
@_disfavoredOverload
public convenience init(
public init(
url: URL,
headers: [String: String] = [:],
region: FunctionRegion? = nil,
Expand All @@ -65,7 +57,7 @@ public final class FunctionsClient: Sendable {
)
}

convenience init(
init(
url: URL,
headers: [String: String] = [:],
region: FunctionRegion? = nil,
Expand Down Expand Up @@ -101,24 +93,20 @@ public final class FunctionsClient: Sendable {
self.http = http
self.sessionConfiguration = sessionConfiguration

mutableState.withValue {
$0.headers = HTTPFields(headers)
if $0.headers[.xClientInfo] == nil {
$0.headers[.xClientInfo] = "functions-swift/\(version)"
}
self.headers = HTTPFields(headers)
if self.headers[.xClientInfo] == nil {
self.headers[.xClientInfo] = "functions-swift/\(version)"
}
}

/// Updates the authorization header.
///
/// - Parameter token: The new JWT token sent in the authorization header.
public func setAuth(token: String?) {
mutableState.withValue {
if let token {
$0.headers[.authorization] = "Bearer \(token)"
} else {
$0.headers[.authorization] = nil
}
if let token {
headers[.authorization] = "Bearer \(token)"
} else {
headers[.authorization] = nil
}
}

Expand All @@ -136,7 +124,8 @@ public final class FunctionsClient: Sendable {
decode: (Data, HTTPURLResponse) throws -> Response
) async throws -> Response {
let response = try await rawInvoke(
functionName: functionName, invokeOptions: options
functionName: functionName,
invokeOptions: options
)
return try decode(response.data, response.underlyingResponse)
}
Expand Down Expand Up @@ -208,7 +197,10 @@ public final class FunctionsClient: Sendable {
let delegate = StreamResponseDelegate(continuation: continuation)

let session = URLSession(
configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
configuration: sessionConfiguration,
delegate: delegate,
delegateQueue: nil
)

let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest

Expand All @@ -233,7 +225,7 @@ public final class FunctionsClient: Sendable {
url: url.appendingPathComponent(functionName),
method: FunctionInvokeOptions.httpMethod(options.method) ?? .post,
query: query,
headers: mutableState.headers.merging(with: options.headers),
headers: headers.merging(with: options.headers),
body: options.body,
timeoutInterval: FunctionsClient.requestIdleTimeout
)
Expand Down Expand Up @@ -264,7 +256,9 @@ final class StreamResponseDelegate: NSObject, URLSessionDataDelegate, Sendable {
}

func urlSession(
_: URLSession, dataTask _: URLSessionDataTask, didReceive response: URLResponse,
_: URLSession,
dataTask _: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
) {
defer {
Expand Down
10 changes: 10 additions & 0 deletions Supabase.xcworkspace/xcshareddata/xcschemes/Functions.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FunctionsTests"
BuildableName = "FunctionsTests"
BlueprintName = "FunctionsTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
34 changes: 17 additions & 17 deletions Tests/FunctionsTests/FunctionsClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ final class FunctionsClientTests: XCTestCase {
headers: ["apikey": apiKey],
region: .saEast1
)
XCTAssertEqual(client.region?.rawValue, "sa-east-1")

XCTAssertEqual(client.headers[.init("apikey")!], apiKey)
XCTAssertNotNil(client.headers[.init("X-Client-Info")!])
let region = await client.region
XCTAssertEqual(region?.rawValue, "sa-east-1")

let headers = await client.headers
XCTAssertEqual(headers[.init("apikey")!], apiKey)
XCTAssertNotNil(headers[.init("X-Client-Info")!])
}

func testInvoke() async throws {
Expand Down Expand Up @@ -333,12 +336,15 @@ final class FunctionsClientTests: XCTestCase {
}
}

func test_setAuth() {
sut.setAuth(token: "access.token")
XCTAssertEqual(sut.headers[.authorization], "Bearer access.token")
func test_setAuth() async {
await sut.setAuth(token: "access.token")

var headers = await sut.headers
XCTAssertEqual(headers[.authorization], "Bearer access.token")

sut.setAuth(token: nil)
XCTAssertNil(sut.headers[.authorization])
await sut.setAuth(token: nil)
headers = await sut.headers
XCTAssertNil(headers[.authorization])
}

func testInvokeWithStreamedResponse() async throws {
Expand All @@ -358,9 +364,7 @@ final class FunctionsClientTests: XCTestCase {
}
.register()

let stream = sut._invokeWithStreamedResponse("stream")

for try await value in stream {
for try await value in await sut._invokeWithStreamedResponse("stream") {
XCTAssertEqual(String(decoding: value, as: UTF8.self), "hello world")
}
}
Expand All @@ -382,10 +386,8 @@ final class FunctionsClientTests: XCTestCase {
}
.register()

let stream = sut._invokeWithStreamedResponse("stream")

do {
for try await _ in stream {
for try await _ in await sut._invokeWithStreamedResponse("stream") {
XCTFail("should throw error")
}
} catch let FunctionsError.httpError(code, _) {
Expand Down Expand Up @@ -413,10 +415,8 @@ final class FunctionsClientTests: XCTestCase {
}
.register()

let stream = sut._invokeWithStreamedResponse("stream")

do {
for try await _ in stream {
for try await _ in await sut._invokeWithStreamedResponse("stream") {
XCTFail("should throw error")
}
} catch FunctionsError.relayError {
Expand Down
7 changes: 5 additions & 2 deletions Tests/SupabaseTests/SupabaseClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,14 @@ final class SupabaseClientTests: XCTestCase {
"""
}
expectNoDifference(client.headers, client.auth.configuration.headers)
expectNoDifference(client.headers, client.functions.headers.dictionary)

let functionsHeaders = await client.functions.headers
expectNoDifference(client.headers, functionsHeaders.dictionary)
expectNoDifference(client.headers, client.storage.configuration.headers)
expectNoDifference(client.headers, client.rest.configuration.headers)

XCTAssertEqual(client.functions.region?.rawValue, "ap-northeast-1")
let functionsRegion = await client.functions.region
XCTAssertEqual(functionsRegion?.rawValue, "ap-northeast-1")

let realtimeURL = client.realtimeV2.url
XCTAssertEqual(realtimeURL.absoluteString, "https://project-ref.supabase.co/realtime/v1")
Expand Down
Loading