| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- /*
- * Copyright 2017 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- #import "Firestore/Source/Core/FSTTransaction.h"
- #include <map>
- #include <utility>
- #include <vector>
- #import "FIRFirestoreErrors.h"
- #import "Firestore/Source/API/FSTUserDataConverter.h"
- #import "Firestore/Source/Model/FSTDocument.h"
- #import "Firestore/Source/Model/FSTMutation.h"
- #import "Firestore/Source/Util/FSTUsageValidation.h"
- #include "Firestore/core/src/firebase/firestore/model/document_key.h"
- #include "Firestore/core/src/firebase/firestore/model/document_key_set.h"
- #include "Firestore/core/src/firebase/firestore/model/precondition.h"
- #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h"
- #include "Firestore/core/src/firebase/firestore/remote/datastore.h"
- #include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
- using firebase::firestore::core::ParsedSetData;
- using firebase::firestore::core::ParsedUpdateData;
- using firebase::firestore::model::DocumentKey;
- using firebase::firestore::model::Precondition;
- using firebase::firestore::model::SnapshotVersion;
- using firebase::firestore::model::DocumentKeySet;
- using firebase::firestore::remote::Datastore;
- NS_ASSUME_NONNULL_BEGIN
- #pragma mark - FSTTransaction
- @interface FSTTransaction ()
- @property(nonatomic, strong, readonly) NSMutableArray *mutations;
- @property(nonatomic, assign) BOOL commitCalled;
- /**
- * An error that may have occurred as a consequence of a write. If set, needs to be raised in the
- * completion handler instead of trying to commit.
- */
- @property(nonatomic, strong, nullable) NSError *lastWriteError;
- @end
- @implementation FSTTransaction {
- Datastore *_datastore;
- std::map<DocumentKey, SnapshotVersion> _readVersions;
- }
- + (instancetype)transactionWithDatastore:(Datastore *)datastore {
- return [[FSTTransaction alloc] initWithDatastore:datastore];
- }
- - (instancetype)initWithDatastore:(Datastore *)datastore {
- self = [super init];
- if (self) {
- _datastore = datastore;
- _mutations = [NSMutableArray array];
- _commitCalled = NO;
- }
- return self;
- }
- /**
- * Every time a document is read, this should be called to record its version. If we read two
- * different versions of the same document, this will return an error through its out parameter.
- * When the transaction is committed, the versions recorded will be set as preconditions on the
- * writes sent to the backend.
- */
- - (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error {
- HARD_ASSERT(error != nil, "nil error parameter");
- *error = nil;
- SnapshotVersion docVersion;
- if ([doc isKindOfClass:[FSTDocument class]]) {
- docVersion = doc.version;
- } else if ([doc isKindOfClass:[FSTDeletedDocument class]]) {
- // For deleted docs, we must record an explicit no version to build the right precondition
- // when writing.
- docVersion = SnapshotVersion::None();
- } else {
- HARD_FAIL("Unexpected document type in transaction: %s", NSStringFromClass([doc class]));
- }
- if (_readVersions.find(doc.key) == _readVersions.end()) {
- _readVersions[doc.key] = docVersion;
- return YES;
- } else {
- if (error) {
- *error = [NSError errorWithDomain:FIRFirestoreErrorDomain
- code:FIRFirestoreErrorCodeFailedPrecondition
- userInfo:@{
- NSLocalizedDescriptionKey :
- @"A document cannot be read twice within a single transaction."
- }];
- }
- return NO;
- }
- }
- - (void)lookupDocumentsForKeys:(const std::vector<DocumentKey> &)keys
- completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion {
- [self ensureCommitNotCalled];
- if (self.mutations.count) {
- FSTThrowInvalidUsage(@"FIRIllegalStateException",
- @"All reads in a transaction must be done before any writes.");
- }
- _datastore->LookupDocuments(
- keys, ^(NSArray<FSTMaybeDocument *> *_Nullable documents, NSError *_Nullable error) {
- if (error) {
- completion(nil, error);
- return;
- }
- for (FSTMaybeDocument *doc in documents) {
- NSError *recordError = nil;
- if (![self recordVersionForDocument:doc error:&recordError]) {
- completion(nil, recordError);
- return;
- }
- }
- completion(documents, nil);
- });
- }
- /** Stores mutations to be written when commitWithCompletion is called. */
- - (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
- [self ensureCommitNotCalled];
- [self.mutations addObjectsFromArray:mutations];
- }
- /**
- * Returns version of this doc when it was read in this transaction as a precondition, or no
- * precondition if it was not read.
- */
- - (Precondition)preconditionForDocumentKey:(const DocumentKey &)key {
- const auto iter = _readVersions.find(key);
- if (iter == _readVersions.end()) {
- return Precondition::None();
- } else {
- return Precondition::UpdateTime(iter->second);
- }
- }
- /**
- * Returns the precondition for a document if the operation is an update, based on the provided
- * UpdateOptions. Will return none precondition if an error occurred, in which case it sets the
- * error parameter.
- */
- - (Precondition)preconditionForUpdateWithDocumentKey:(const DocumentKey &)key
- error:(NSError **)error {
- const auto iter = _readVersions.find(key);
- if (iter == _readVersions.end()) {
- // Document was not read, so we just use the preconditions for an update.
- return Precondition::Exists(true);
- }
- const SnapshotVersion &version = iter->second;
- if (version == SnapshotVersion::None()) {
- // The document was read, but doesn't exist.
- // Return an error because the precondition is impossible
- if (error) {
- *error = [NSError
- errorWithDomain:FIRFirestoreErrorDomain
- code:FIRFirestoreErrorCodeAborted
- userInfo:@{
- NSLocalizedDescriptionKey : @"Can't update a document that doesn't exist."
- }];
- }
- return Precondition::None();
- } else {
- // Document exists, just base precondition on document update time.
- return Precondition::UpdateTime(version);
- }
- }
- - (void)setData:(ParsedSetData &&)data forDocument:(const DocumentKey &)key {
- [self writeMutations:std::move(data).ToMutations(key, [self preconditionForDocumentKey:key])];
- }
- - (void)updateData:(ParsedUpdateData &&)data forDocument:(const DocumentKey &)key {
- NSError *error = nil;
- const Precondition precondition = [self preconditionForUpdateWithDocumentKey:key error:&error];
- if (precondition.IsNone()) {
- HARD_ASSERT(error, "Got nil precondition, but error was not set");
- self.lastWriteError = error;
- } else {
- [self writeMutations:std::move(data).ToMutations(key, precondition)];
- }
- }
- - (void)deleteDocument:(const DocumentKey &)key {
- [self writeMutations:@[ [[FSTDeleteMutation alloc]
- initWithKey:key
- precondition:[self preconditionForDocumentKey:key]] ]];
- // Since the delete will be applied before all following writes, we need to ensure that the
- // precondition for the next write will be exists without timestamp.
- _readVersions[key] = SnapshotVersion::None();
- }
- - (void)commitWithCompletion:(FSTVoidErrorBlock)completion {
- [self ensureCommitNotCalled];
- // Once commitWithCompletion is called once, mark this object so it can't be used again.
- self.commitCalled = YES;
- // If there was an error writing, raise that error now
- if (self.lastWriteError) {
- completion(self.lastWriteError);
- return;
- }
- // Make a list of read documents that haven't been written.
- DocumentKeySet unwritten;
- for (const auto &kv : _readVersions) {
- unwritten = unwritten.insert(kv.first);
- };
- // For each mutation, note that the doc was written.
- for (FSTMutation *mutation in self.mutations) {
- unwritten = unwritten.erase(mutation.key);
- }
- if (!unwritten.empty()) {
- // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend.
- completion([NSError
- errorWithDomain:FIRFirestoreErrorDomain
- code:FIRFirestoreErrorCodeFailedPrecondition
- userInfo:@{
- NSLocalizedDescriptionKey : @"Every document read in a transaction must also be "
- @"written in that transaction."
- }]);
- } else {
- _datastore->CommitMutations(self.mutations, ^(NSError *_Nullable error) {
- if (error) {
- completion(error);
- } else {
- completion(nil);
- }
- });
- }
- }
- - (void)ensureCommitNotCalled {
- if (self.commitCalled) {
- FSTThrowInvalidUsage(
- @"FIRIllegalStateException",
- @"A transaction object cannot be used after its update block has completed.");
- }
- }
- @end
- NS_ASSUME_NONNULL_END
|