Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 5 additions & 0 deletions FirebaseFunctions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Unreleased
- [changed] **Breaking Change**: Mark `HTTPSCallable` and `HTTPSCallableOptions`
as `final` classes for Swift clients. This was to achieve Swift 6 checked
`Sendable` support.

# 11.12.0
- [fixed] Fix regression from 11.6.0 where `HTTPSCallable` did not invoke
completion block on main thread (#14653).
Expand Down
123 changes: 41 additions & 82 deletions FirebaseFunctions/Sources/HTTPSCallable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,34 @@ open class HTTPSCallableResult: NSObject {

/// A `HTTPSCallable` is a reference to a particular Callable HTTPS trigger in Cloud Functions.
@objc(FIRHTTPSCallable)
open class HTTPSCallable: NSObject, @unchecked Sendable {
public final class HTTPSCallable: NSObject, Sendable {
// MARK: - Private Properties

/// Until this class can be marked *checked* `Sendable`, it's implementation
/// is delegated to an auxiliary class that is checked Sendable.
private let sendableCallable: SendableHTTPSCallable
// The functions client to use for making calls.
private let functions: Functions

private let url: URL

private let options: HTTPSCallableOptions?

private let _timeoutInterval: AtomicBox<TimeInterval> = .init(70)

// MARK: - Public Properties

/// The timeout to use when calling the function. Defaults to 70 seconds.
@objc open var timeoutInterval: TimeInterval {
get { sendableCallable.timeoutInterval }
set { sendableCallable.timeoutInterval = newValue }
@objc public var timeoutInterval: TimeInterval {
get { _timeoutInterval.value() }
set {
_timeoutInterval.withLock { timeoutInterval in
timeoutInterval = newValue
}
}
}

init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) {
sendableCallable = SendableHTTPSCallable(functions: functions, url: url, options: options)
self.functions = functions
self.url = url
self.options = options
}

/// Executes this Callable HTTPS trigger asynchronously.
Expand All @@ -72,11 +83,11 @@ open class HTTPSCallable: NSObject, @unchecked Sendable {
/// - data: Parameters to pass to the trigger.
/// - completion: The block to call when the HTTPS request has completed.
@available(swift 1000.0) // Objective-C only API
@objc(callWithObject:completion:) open func call(_ data: Any? = nil,
completion: @escaping @MainActor (HTTPSCallableResult?,
Error?)
-> Void) {
sendableCallable.call(SendableWrapper(value: data as Any), completion: completion)
@objc(callWithObject:completion:) public func call(_ data: Any? = nil,
completion: @escaping @MainActor (HTTPSCallableResult?,
Error?)
-> Void) {
call(SendableWrapper(value: data as Any), completion: completion)
}

/// Executes this Callable HTTPS trigger asynchronously.
Expand All @@ -100,11 +111,19 @@ open class HTTPSCallable: NSObject, @unchecked Sendable {
/// - Parameters:
/// - data: Parameters to pass to the trigger.
/// - completion: The block to call when the HTTPS request has completed.
@nonobjc open func call(_ data: sending Any? = nil,
completion: @escaping @MainActor (HTTPSCallableResult?,
Error?)
-> Void) {
sendableCallable.call(data, completion: completion)
@nonobjc public func call(_ data: sending Any? = nil,
completion: @escaping @MainActor (HTTPSCallableResult?,
Error?)
-> Void) {
let data = (data as? SendableWrapper)?.value ?? data
Task {
do {
let result = try await call(data)
await completion(result, nil)
} catch {
await completion(nil, error)
}
}
}

/// Executes this Callable HTTPS trigger asynchronously. This API should only be used from
Expand Down Expand Up @@ -140,73 +159,13 @@ open class HTTPSCallable: NSObject, @unchecked Sendable {
/// - Throws: An error if the Cloud Functions invocation failed.
/// - Returns: The result of the call.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
open func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult {
try await sendableCallable.call(data)
public func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult {
try await functions
.callFunction(at: url, withObject: data, options: options, timeout: timeoutInterval)
}

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
func stream(_ data: SendableWrapper? = nil) -> AsyncThrowingStream<JSONStreamResponse, Error> {
sendableCallable.stream(data)
}
}

private extension HTTPSCallable {
final class SendableHTTPSCallable: Sendable {
// MARK: - Private Properties

// The functions client to use for making calls.
private let functions: Functions

private let url: URL

private let options: HTTPSCallableOptions?

// MARK: - Public Properties

let _timeoutInterval: AtomicBox<TimeInterval> = .init(70)

/// The timeout to use when calling the function. Defaults to 70 seconds.
var timeoutInterval: TimeInterval {
get { _timeoutInterval.value() }
set {
_timeoutInterval.withLock { timeoutInterval in
timeoutInterval = newValue
}
}
}

init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) {
self.functions = functions
self.url = url
self.options = options
}

func call(_ data: sending Any? = nil,
completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) {
let data = (data as? SendableWrapper)?.value ?? data
Task {
do {
let result = try await call(data)
await completion(result, nil)
} catch {
await completion(nil, error)
}
}
}

func __call(completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) {
call(nil, completion: completion)
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult {
try await functions
.callFunction(at: url, withObject: data, options: options, timeout: timeoutInterval)
}

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
func stream(_ data: SendableWrapper? = nil) -> AsyncThrowingStream<JSONStreamResponse, Error> {
functions.stream(at: url, data: data, options: options, timeout: timeoutInterval)
}
functions.stream(at: url, data: data, options: options, timeout: timeoutInterval)
}
}
2 changes: 1 addition & 1 deletion FirebaseFunctions/Sources/HTTPSCallableOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import Foundation

/// Configuration options for a ``HTTPSCallable`` instance.
@objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject, @unchecked Sendable {
@objc(FIRHTTPSCallableOptions) public final class HTTPSCallableOptions: NSObject, Sendable {
/// Whether or not to protect the callable function with a limited-use App Check token.
@objc public let requireLimitedUseAppCheckTokens: Bool

Expand Down
12 changes: 2 additions & 10 deletions FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,6 @@ class MockFunctions: Functions, @unchecked Sendable {
}
}

public class HTTPSCallableResultFake: HTTPSCallableResult {
let fakeData: String
init(data: String) {
fakeData = data
super.init(data: data)
}
}

@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *)
class HTTPSCallableTests: XCTestCase {
func testCallWithoutParametersSuccess() {
Expand All @@ -73,7 +65,7 @@ class HTTPSCallableTests: XCTestCase {

let functions = MockFunctions {
httpsFunctionWasCalledExpectation.fulfill()
return HTTPSCallableResultFake(data: expectedResult)
return HTTPSCallableResult(data: expectedResult)
}

let dummyFunction = functions.httpsCallable("dummyFunction")
Expand Down Expand Up @@ -115,7 +107,7 @@ class HTTPSCallableTests: XCTestCase {
let expectedResult = "mockResult w/ parameters: \(inputParameter)"
let functions = MockFunctions {
httpsFunctionWasCalledExpectation.fulfill()
return HTTPSCallableResultFake(data: expectedResult)
return HTTPSCallableResult(data: expectedResult)
}
functions.verifyParameters = { url, data, timeout in
XCTAssertEqual(
Expand Down
Loading