mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
* Migrate FSTFail to HARD_FAIL * FSTCFail -> HARD_FAIL * FSTCAssert -> HARD_ASSERT * FSTAssert -> HARD_ASSERT * Replace FSTAssert with NSAssert in dead Objective-C code * Remove FSTAssert.h
260 lines
9.4 KiB
Plaintext
260 lines
9.4 KiB
Plaintext
/*
|
|
* 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"
|
|
|
|
#import <GRPCClient/GRPCCall.h>
|
|
|
|
#include <map>
|
|
#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/Remote/FSTDatastore.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/util/hard_assert.h"
|
|
|
|
using firebase::firestore::model::DocumentKey;
|
|
using firebase::firestore::model::Precondition;
|
|
using firebase::firestore::model::SnapshotVersion;
|
|
using firebase::firestore::model::DocumentKeySet;
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
#pragma mark - FSTTransaction
|
|
|
|
@interface FSTTransaction ()
|
|
@property(nonatomic, strong, readonly) FSTDatastore *datastore;
|
|
@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 {
|
|
std::map<DocumentKey, SnapshotVersion> _readVersions;
|
|
}
|
|
|
|
+ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore {
|
|
return [[FSTTransaction alloc] initWithDatastore:datastore];
|
|
}
|
|
|
|
- (instancetype)initWithDatastore:(FSTDatastore *)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 = doc.version;
|
|
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();
|
|
}
|
|
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.");
|
|
}
|
|
[self.datastore lookupDocuments:keys
|
|
completion:^(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:(FSTParsedSetData *)data forDocument:(const DocumentKey &)key {
|
|
[self writeMutations:[data mutationsWithKey:key
|
|
precondition:[self preconditionForDocumentKey:key]]];
|
|
}
|
|
|
|
- (void)updateData:(FSTParsedUpdateData *)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:[data mutationsWithKey:key precondition: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 {
|
|
[self.datastore commitMutations:self.mutations
|
|
completion:^(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
|