Skip to content
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

Implement runTransaction and batch support #28

Merged
merged 14 commits into from
Feb 16, 2024
57 changes: 57 additions & 0 deletions Sources/FirebaseFirestore/Firestore+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,63 @@ extension Firestore {
public func collection(_ collectionPath: String) -> CollectionReference {
swift_firebase.swift_cxx_shims.firebase.firestore.firestore_collection(self, std.string(collectionPath))
}

public func runTransaction(_ updateBlock: @escaping (Transaction, UnsafePointer<NSError?>?) -> Any?, completion: @escaping (Any?, Error?) -> Void) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this need changes from https://github.com/thebrowsercompany/swift-firebase/pull/30/files for the error pointer?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that PR builds on top of this one. I wrote that one once I was able to try building against Arc. Rather than complicate things by folding it into this PR, I just left them as separate. Hope that works!

runTransaction(with: nil, block: updateBlock, completion: completion)
}

public func runTransaction(
with options: TransactionOptions?,
block updateBlock: @escaping (Transaction, UnsafePointer<NSError?>?) -> Any?,
completion: @escaping (Any?, Error?) -> Void
) {
let context = TransactionContext(updateBlock: updateBlock)
let boxed = Unmanaged.passRetained(context as AnyObject)
let future = swift_firebase.swift_cxx_shims.firebase.firestore.firestore_run_transaction(
self, options ?? .init(), { transaction, pErrorMessage, pvUpdateBlock in
let context = Unmanaged<AnyObject>.fromOpaque(pvUpdateBlock!).takeUnretainedValue() as! TransactionContext

// Instead of trying to relay the generated `NSError` through firebase's `Error` type
// and error message string, just store the `NSError` on `context` and access it later.
// We then only need to tell firebase if the update block succeeded or failed and can
// just not bother setting `pErrorMessage`.

// It is expected to run `updateBlock` on whatever thread this happens to be. This is
// consistent with the behavior of the ObjC API as well.

// Since we could run `updateBlock` multiple times, we need to take care to reset any
// residue from previous runs. That means clearing out this error field.
context.error = nil

withUnsafePointer(to: context.error) { pError in
context.result = context.updateBlock(transaction!.pointee, pError)
}

return context.error != nil ? firebase.firestore.kErrorNone : firebase.firestore.kErrorCancelled
},
boxed.toOpaque()
)
future.setCompletion({
darinf marked this conversation as resolved.
Show resolved Hide resolved
completion(context.result, context.error)
boxed.release()
})
}

public func batch() -> WriteBatch {
swift_firebase.swift_cxx_shims.firebase.firestore.firestore_batch(self)
}

private class TransactionContext {
typealias UpdateBlock = (Transaction, UnsafePointer<NSError?>?) -> Any?
darinf marked this conversation as resolved.
Show resolved Hide resolved

let updateBlock: UpdateBlock
var result: Any?
var error: NSError?

init(updateBlock: @escaping UpdateBlock) {
self.updateBlock = updateBlock
}
}
}

// An extension that adds the encoder and decoder functions required
Expand Down
13 changes: 10 additions & 3 deletions Sources/FirebaseFirestore/NSError+FirestoreError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ extension NSError {
internal static func firestore(_ error: firebase.firestore.Error?, errorMessage: UnsafePointer<CChar>? = nil) -> NSError? {
guard let actualError = error, actualError.rawValue != 0 else { return nil }

var userInfo = [String: Any]()
var errorMessageString: String?
if let errorMessage {
userInfo[NSLocalizedDescriptionKey] = String(cString: errorMessage)
errorMessageString = .init(cString: errorMessage)
}
return firestore(actualError, errorMessage: errorMessageString)
}

return NSError(domain: "firebase.firestore", code: Int(actualError.rawValue), userInfo: userInfo)
internal static func firestore(_ error: firebase.firestore.Error, errorMessage: String?) -> NSError {
var userInfo = [String: Any]()
if let errorMessage {
userInfo[NSLocalizedDescriptionKey] = errorMessage
}
return NSError(domain: "firebase.firestore", code: Int(error.rawValue), userInfo: userInfo)
}
}
53 changes: 53 additions & 0 deletions Sources/FirebaseFirestore/Transaction+Swift.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: BSD-3-Clause

@_exported
import firebase

import CxxShim
import Foundation

public typealias Transaction = swift_firebase.swift_cxx_shims.firebase.firestore.TransactionWeakReference

extension Transaction {
public mutating func setData(_ data: [String : Any], forDocument document: DocumentReference) -> Transaction {
setData(data, forDocument: document, merge: false)
}

public mutating func setData(_ data: [String : Any], forDocument document: DocumentReference, merge: Bool) -> Transaction {
assert(is_valid())
darinf marked this conversation as resolved.
Show resolved Hide resolved
self.Set(document, FirestoreDataConverter.firestoreValue(document: data), merge ? .Merge() : .init())
return self
}

/* TODO: implement
public mutating func setData(_ data: [String : Any], forDocument document: DocumentReference, mergeFields: [Any]) -> Transaction {
}
*/

public mutating func updateData(_ fields: [String : Any], forDocument document: DocumentReference) -> Transaction {
assert(is_valid())
self.Update(document, FirestoreDataConverter.firestoreValue(document: fields))
return self
}

public mutating func deleteDocument(_ document: DocumentReference) -> Transaction {
assert(is_valid())
Delete(document)
return self
}

public mutating func getDocument(_ document: DocumentReference) throws -> DocumentSnapshot {
assert(is_valid())

var error = firebase.firestore.kErrorNone
var errorMessage = std.string()

let snapshot = Get(document, &error, &errorMessage)

if error != firebase.firestore.kErrorNone {
throw NSError.firestore(error, errorMessage: String(errorMessage))
}

return snapshot
}
}
17 changes: 17 additions & 0 deletions Sources/FirebaseFirestore/TransactionOptions+Swift.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: BSD-3-Clause

@_exported
import firebase

public typealias TransactionOptions = firebase.firestore.TransactionOptions

extension TransactionOptions {
public var maxAttempts: Int {
get {
Int(max_attempts())
}
set {
set_max_attempts(Int32(newValue))
}
}
}
67 changes: 67 additions & 0 deletions Sources/FirebaseFirestore/WriteBatch+Swift.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: BSD-3-Clause

@_exported
import firebase
@_spi(FirebaseInternal)
import FirebaseCore

import CxxShim
import Foundation

public typealias WriteBatch = firebase.firestore.WriteBatch

extension WriteBatch {
public mutating func setData(_ data: [String : Any], forDocument document: DocumentReference) -> WriteBatch {
setData(data, forDocument: document, merge: false)
}

public mutating func setData(_ data: [String : Any], forDocument document: DocumentReference, merge: Bool) -> WriteBatch {
_ = swift_firebase.swift_cxx_shims.firebase.firestore.write_batch_set(
self, document, FirestoreDataConverter.firestoreValue(document: data), merge ? .Merge() : .init()
)
return self
}

/* TODO: implement
public mutating func setData(_ data: [String : Any], forDocument document: DocumentReference, mergeFields: [Any]) -> WriteBatch {
}
*/

public mutating func updateData(_ fields: [String : Any], forDocument document: DocumentReference) -> WriteBatch {
_ = swift_firebase.swift_cxx_shims.firebase.firestore.write_batch_update(
self, document, FirestoreDataConverter.firestoreValue(document: fields)
)
return self
}

public mutating func deleteDocument(_ document: DocumentReference) -> WriteBatch {
_ = swift_firebase.swift_cxx_shims.firebase.firestore.write_batch_delete(
self, document
)
return self
}

public mutating func commit(completion: @escaping ((Error?) -> Void) = { _ in }) {
let future = swift_firebase.swift_cxx_shims.firebase.firestore.write_batch_commit(self)
darinf marked this conversation as resolved.
Show resolved Hide resolved
future.setCompletion({
let (_, error) = future.resultAndError
DispatchQueue.main.async {
completion(error)
}
})
}

public mutating func commit() async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
let future = swift_firebase.swift_cxx_shims.firebase.firestore.write_batch_commit(self)
future.setCompletion({
let (_, error) = future.resultAndError
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
})
}
}
}
10 changes: 10 additions & 0 deletions Sources/firebase/include/FirebaseCore.hh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class __attribute__((swift_attr("conforms_to:FirebaseCore.FutureProtocol")))

Future(const ::firebase::Future<R>& rhs) : ::firebase::Future<R>(rhs) {}

// Allow explicit conversion from `Future<void>` in support of `VoidFuture`.
static Future From(const ::firebase::Future<void>& other) {
static_assert(sizeof(::firebase::Future<void>) == sizeof(::firebase::Future<R>));
return Future(*reinterpret_cast<const ::firebase::Future<R>*>(&other));
}

void OnCompletion(
_Nonnull FutureCompletionType completion,
_Nullable void* user_data) const {
Expand All @@ -31,6 +37,10 @@ class __attribute__((swift_attr("conforms_to:FirebaseCore.FutureProtocol")))
}
};

// As a workaround, use `int` here instead of `void` for futures with no
// result. Swift is not able to handle a `ResultType` of `void`.
typedef Future<int> VoidFuture;

} // namespace swift_firebase::swift_cxx_shims::firebase

#endif
73 changes: 73 additions & 0 deletions Sources/firebase/include/FirebaseFirestore.hh
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
#ifndef firebase_include_FirebaseFirestore_hh
#define firebase_include_FirebaseFirestore_hh

#include <string>
#include <utility>

#include <firebase/firestore.h>

#include "FirebaseCore.hh"
#include "TransactionWeakReference.hh"

// Functions defined in this namespace are used to get around the lack of
// virtual function support currently in Swift. As that support changes
// these functions will go away whenever possible.
namespace swift_firebase::swift_cxx_shims::firebase::firestore {

inline ::firebase::firestore::Settings
firestore_settings(::firebase::firestore::Firestore *firestore) {
return firestore->settings();
Expand All @@ -28,6 +33,34 @@ firestore_collection(::firebase::firestore::Firestore *firestore,
return firestore->Collection(collection_path);
}

typedef ::firebase::firestore::Error (*FirebaseRunTransactionUpdateCallback)(
TransactionWeakReference *transaction,
std::string& error_message,
void *user_data);
inline VoidFuture
firestore_run_transaction(
::firebase::firestore::Firestore *firestore,
::firebase::firestore::TransactionOptions options,
FirebaseRunTransactionUpdateCallback update_callback,
void *user_data) {
return VoidFuture::From(
firestore->RunTransaction(options, [update_callback, user_data](
::firebase::firestore::Transaction& transaction,
std::string& error_message
) -> ::firebase::firestore::Error {
TransactionWeakReference transaction_ref(&transaction);
::firebase::firestore::Error error =
update_callback(&transaction_ref, error_message, user_data);
transaction_ref.reset();
return error;
}));
}

inline ::firebase::firestore::WriteBatch
firestore_batch(::firebase::firestore::Firestore *firestore) {
return firestore->batch();
}

// MARK: - DocumentReference

inline ::firebase::firestore::Firestore *
Expand Down Expand Up @@ -279,6 +312,46 @@ query_snapshot_size(const ::firebase::firestore::QuerySnapshot& snapshot) {
return snapshot.size();
}

// MARK: WriteBatch

inline ::firebase::firestore::WriteBatch&
write_batch_set(
::firebase::firestore::WriteBatch write_batch,
const ::firebase::firestore::DocumentReference& document,
const ::firebase::firestore::MapFieldValue& data,
const ::firebase::firestore::SetOptions& options =
::firebase::firestore::SetOptions()) {
return write_batch.Set(document, data, options);
}

inline ::firebase::firestore::WriteBatch&
write_batch_update(
::firebase::firestore::WriteBatch write_batch,
const ::firebase::firestore::DocumentReference& document,
const ::firebase::firestore::MapFieldValue& data) {
return write_batch.Update(document, data);
}

inline ::firebase::firestore::WriteBatch&
write_batch_update(
::firebase::firestore::WriteBatch write_batch,
const ::firebase::firestore::DocumentReference& document,
const ::firebase::firestore::MapFieldPathValue& data) {
return write_batch.Update(document, data);
}

inline ::firebase::firestore::WriteBatch&
write_batch_delete(
::firebase::firestore::WriteBatch write_batch,
const ::firebase::firestore::DocumentReference& document) {
return write_batch.Delete(document);
}

inline VoidFuture
write_batch_commit(::firebase::firestore::WriteBatch write_batch) {
return VoidFuture::From(write_batch.Commit());
}

} // namespace swift_firebase::swift_cxx_shims::firebase::firestore

#endif
Loading
Loading