| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- /*
- * 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/Remote/FSTRemoteEvent.h"
- #include <map>
- #include <utility>
- #import "Firestore/Source/Local/FSTQueryData.h"
- #import "Firestore/Source/Model/FSTDocument.h"
- #import "Firestore/Source/Remote/FSTWatchChange.h"
- #import "Firestore/Source/Util/FSTClasses.h"
- #include "Firestore/core/src/firebase/firestore/model/document_key.h"
- #include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
- #include "Firestore/core/src/firebase/firestore/util/hashing.h"
- #include "Firestore/core/src/firebase/firestore/util/log.h"
- using firebase::firestore::model::DocumentKey;
- using firebase::firestore::model::SnapshotVersion;
- using firebase::firestore::util::Hash;
- using firebase::firestore::model::DocumentKeySet;
- NS_ASSUME_NONNULL_BEGIN
- #pragma mark - FSTTargetMapping
- @interface FSTTargetMapping ()
- /** Private mutator method to add a document key to the mapping */
- - (void)addDocumentKey:(const DocumentKey &)documentKey;
- /** Private mutator method to remove a document key from the mapping */
- - (void)removeDocumentKey:(const DocumentKey &)documentKey;
- @end
- @implementation FSTTargetMapping
- - (void)addDocumentKey:(const DocumentKey &)documentKey {
- @throw FSTAbstractMethodException(); // NOLINT
- }
- - (void)removeDocumentKey:(const DocumentKey &)documentKey {
- @throw FSTAbstractMethodException(); // NOLINT
- }
- - (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys {
- @throw FSTAbstractMethodException(); // NOLINT
- }
- @end
- #pragma mark - FSTResetMapping
- @implementation FSTResetMapping {
- DocumentKeySet _documents;
- }
- + (instancetype)mappingWithDocuments:(NSArray<FSTDocument *> *)documents {
- DocumentKeySet keys;
- for (FSTDocument *doc in documents) {
- keys = keys.insert(doc.key);
- }
- return [[FSTResetMapping alloc] initWithDocuments:std::move(keys)];
- }
- - (instancetype)initWithDocuments:(DocumentKeySet)documents {
- self = [super init];
- if (self) {
- _documents = std::move(documents);
- }
- return self;
- }
- - (const DocumentKeySet &)documents {
- return _documents;
- }
- - (BOOL)isEqual:(id)other {
- if (other == self) {
- return YES;
- }
- if (![other isMemberOfClass:[FSTResetMapping class]]) {
- return NO;
- }
- FSTResetMapping *otherMapping = (FSTResetMapping *)other;
- return _documents == otherMapping.documents;
- }
- - (NSUInteger)hash {
- return Hash(_documents);
- }
- - (void)addDocumentKey:(const DocumentKey &)documentKey {
- _documents = _documents.insert(documentKey);
- }
- - (void)removeDocumentKey:(const DocumentKey &)documentKey {
- _documents = _documents.erase(documentKey);
- }
- - (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys {
- // No-op. Resets are not filtered.
- }
- @end
- #pragma mark - FSTUpdateMapping
- @implementation FSTUpdateMapping {
- DocumentKeySet _addedDocuments;
- DocumentKeySet _removedDocuments;
- }
- + (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added
- removedDocuments:(NSArray<FSTDocument *> *)removed {
- DocumentKeySet addedDocuments;
- DocumentKeySet removedDocuments;
- for (FSTDocument *doc in added) {
- addedDocuments = addedDocuments.insert(doc.key);
- }
- for (FSTDocument *doc in removed) {
- removedDocuments = removedDocuments.insert(doc.key);
- }
- return [[FSTUpdateMapping alloc] initWithAddedDocuments:std::move(addedDocuments)
- removedDocuments:std::move(removedDocuments)];
- }
- - (instancetype)initWithAddedDocuments:(DocumentKeySet)addedDocuments
- removedDocuments:(DocumentKeySet)removedDocuments {
- self = [super init];
- if (self) {
- _addedDocuments = std::move(addedDocuments);
- _removedDocuments = std::move(removedDocuments);
- }
- return self;
- }
- - (const DocumentKeySet &)addedDocuments {
- return _addedDocuments;
- }
- - (const DocumentKeySet &)removedDocuments {
- return _removedDocuments;
- }
- - (BOOL)isEqual:(id)other {
- if (other == self) {
- return YES;
- }
- if (![other isMemberOfClass:[FSTUpdateMapping class]]) {
- return NO;
- }
- FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other;
- return _addedDocuments == otherMapping.addedDocuments &&
- _removedDocuments == otherMapping.removedDocuments;
- }
- - (NSUInteger)hash {
- return Hash(_addedDocuments, _removedDocuments);
- }
- - (DocumentKeySet)applyTo:(const DocumentKeySet &)keys {
- DocumentKeySet result = keys;
- for (const DocumentKey &key : _addedDocuments) {
- result = result.insert(key);
- }
- for (const DocumentKey &key : _removedDocuments) {
- result = result.erase(key);
- }
- return result;
- }
- - (void)addDocumentKey:(const DocumentKey &)documentKey {
- _addedDocuments = _addedDocuments.insert(documentKey);
- _removedDocuments = _removedDocuments.erase(documentKey);
- }
- - (void)removeDocumentKey:(const DocumentKey &)documentKey {
- _addedDocuments = _addedDocuments.erase(documentKey);
- _removedDocuments = _removedDocuments.insert(documentKey);
- }
- - (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys {
- DocumentKeySet result = _addedDocuments;
- for (const DocumentKey &key : _addedDocuments) {
- if (existingKeys.contains(key)) {
- result = result.erase(key);
- }
- }
- _addedDocuments = result;
- }
- @end
- #pragma mark - FSTTargetChange
- @interface FSTTargetChange ()
- @property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate;
- @property(nonatomic, strong, nullable) FSTTargetMapping *mapping;
- @property(nonatomic, strong) NSData *resumeToken;
- @end
- @implementation FSTTargetChange {
- SnapshotVersion _snapshotVersion;
- }
- - (instancetype)init {
- if (self = [super init]) {
- _currentStatusUpdate = FSTCurrentStatusUpdateNone;
- _resumeToken = [NSData data];
- }
- return self;
- }
- - (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion {
- if (self = [self init]) {
- _snapshotVersion = std::move(snapshotVersion);
- }
- return self;
- }
- - (const SnapshotVersion &)snapshotVersion {
- return _snapshotVersion;
- }
- + (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs
- currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate {
- DocumentKeySet addedDocuments;
- DocumentKeySet removedDocuments;
- for (FSTMaybeDocument *doc in docs) {
- if ([doc isKindOfClass:[FSTDeletedDocument class]]) {
- removedDocuments = removedDocuments.insert(doc.key);
- } else {
- addedDocuments = addedDocuments.insert(doc.key);
- }
- }
- FSTUpdateMapping *mapping =
- [[FSTUpdateMapping alloc] initWithAddedDocuments:std::move(addedDocuments)
- removedDocuments:std::move(removedDocuments)];
- FSTTargetChange *change = [[FSTTargetChange alloc] init];
- change.mapping = mapping;
- change.currentStatusUpdate = currentStatusUpdate;
- return change;
- }
- + (instancetype)changeWithMapping:(FSTTargetMapping *)mapping
- snapshotVersion:(SnapshotVersion)snapshotVersion
- currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate {
- FSTTargetChange *change = [[FSTTargetChange alloc] init];
- change.mapping = mapping;
- change->_snapshotVersion = std::move(snapshotVersion);
- change.currentStatusUpdate = currentStatusUpdate;
- return change;
- }
- - (FSTTargetMapping *)mapping {
- if (!_mapping) {
- // Create an FSTUpdateMapping by default, since resets are always explicit
- _mapping = [[FSTUpdateMapping alloc] init];
- }
- return _mapping;
- }
- /**
- * Sets the resume token but only when it has a new value. Empty resumeTokens are
- * discarded.
- */
- - (void)setResumeToken:(NSData *)resumeToken {
- if (resumeToken.length > 0) {
- _resumeToken = resumeToken;
- }
- }
- @end
- #pragma mark - FSTRemoteEvent
- @implementation FSTRemoteEvent {
- SnapshotVersion _snapshotVersion;
- NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges;
- std::map<DocumentKey, FSTMaybeDocument *> _documentUpdates;
- DocumentKeySet _limboDocumentChanges;
- }
- - (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion
- targetChanges:
- (NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges
- documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates
- limboDocuments:(DocumentKeySet)limboDocuments {
- self = [super init];
- if (self) {
- _snapshotVersion = std::move(snapshotVersion);
- _targetChanges = targetChanges;
- _documentUpdates = std::move(documentUpdates);
- _limboDocumentChanges = std::move(limboDocuments);
- }
- return self;
- }
- - (NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges {
- return _targetChanges;
- }
- - (const DocumentKeySet &)limboDocumentChanges {
- return _limboDocumentChanges;
- }
- - (const std::map<DocumentKey, FSTMaybeDocument *> &)documentUpdates {
- return _documentUpdates;
- }
- - (const SnapshotVersion &)snapshotVersion {
- return _snapshotVersion;
- }
- - (void)synthesizeDeleteForLimboTargetChange:(FSTTargetChange *)targetChange
- key:(const DocumentKey &)key {
- if (targetChange.currentStatusUpdate == FSTCurrentStatusUpdateMarkCurrent &&
- _documentUpdates.find(key) == _documentUpdates.end()) {
- // When listening to a query the server responds with a snapshot containing documents
- // matching the query and a current marker telling us we're now in sync. It's possible for
- // these to arrive as separate remote events or as a single remote event. For a document
- // query, there will be no documents sent in the response if the document doesn't exist.
- //
- // If the snapshot arrives separately from the current marker, we handle it normally and
- // updateTrackedLimboDocumentsWithChanges:targetID: will resolve the limbo status of the
- // document, removing it from limboDocumentRefs. This works because clients only initiate
- // limbo resolution when a target is current and because all current targets are always at a
- // consistent snapshot.
- //
- // However, if the document doesn't exist and the current marker arrives, the document is
- // not present in the snapshot and our normal view handling would consider the document to
- // remain in limbo indefinitely because there are no updates to the document. To avoid this,
- // we specially handle this case here: synthesizing a delete.
- //
- // TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an
- // explicit delete message and we could remove this special logic.
- _documentUpdates[key] = [FSTDeletedDocument documentWithKey:key version:_snapshotVersion];
- _limboDocumentChanges = _limboDocumentChanges.insert(key);
- }
- }
- /** Adds a document update to this remote event */
- - (void)addDocumentUpdate:(FSTMaybeDocument *)document {
- _documentUpdates[document.key] = document;
- }
- /** Handles an existence filter mismatch */
- - (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID {
- // An existence filter mismatch will reset the query and we need to reset the mapping to contain
- // no documents and an empty resume token.
- //
- // Note:
- // * The reset mapping is empty, specifically forcing the consumer of the change to
- // forget all keys for this targetID;
- // * The resume snapshot for this target must be reset
- // * The target must be unacked because unwatching and rewatching introduces a race for
- // changes.
- //
- // TODO(dimond): keep track of reset targets not to raise.
- FSTTargetChange *targetChange =
- [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init]
- snapshotVersion:SnapshotVersion::None()
- currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent];
- _targetChanges[targetID] = targetChange;
- }
- @end
- #pragma mark - FSTWatchChangeAggregator
- @interface FSTWatchChangeAggregator ()
- /** Keeps track of the current target mappings */
- @property(nonatomic, strong, readonly)
- NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges;
- /** The set of open listens on the client */
- @property(nonatomic, strong, readonly)
- NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *listenTargets;
- /** Whether this aggregator was frozen and can no longer be modified */
- @property(nonatomic, assign) BOOL frozen;
- @end
- @implementation FSTWatchChangeAggregator {
- NSMutableDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *_existenceFilters;
- /** Keeps track of document to update */
- std::map<DocumentKey, FSTMaybeDocument *> _documentUpdates;
- DocumentKeySet _limboDocuments;
- /** The snapshot version for every target change this creates. */
- SnapshotVersion _snapshotVersion;
- }
- - (instancetype)
- initWithSnapshotVersion:(SnapshotVersion)snapshotVersion
- listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets
- pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses {
- self = [super init];
- if (self) {
- _snapshotVersion = std::move(snapshotVersion);
- _frozen = NO;
- _targetChanges = [NSMutableDictionary dictionary];
- _listenTargets = listenTargets;
- _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses];
- _limboDocuments = DocumentKeySet{};
- _existenceFilters = [NSMutableDictionary dictionary];
- }
- return self;
- }
- - (NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *)existenceFilters {
- return _existenceFilters;
- }
- - (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID {
- FSTTargetChange *change = self.targetChanges[targetID];
- if (!change) {
- change = [[FSTTargetChange alloc] initWithSnapshotVersion:_snapshotVersion];
- self.targetChanges[targetID] = change;
- }
- return change;
- }
- - (void)addWatchChanges:(NSArray<FSTWatchChange *> *)watchChanges {
- HARD_ASSERT(!self.frozen, "Trying to modify frozen FSTWatchChangeAggregator");
- for (FSTWatchChange *watchChange in watchChanges) {
- [self addWatchChange:watchChange];
- }
- }
- - (void)addWatchChange:(FSTWatchChange *)watchChange {
- HARD_ASSERT(!self.frozen, "Trying to modify frozen FSTWatchChangeAggregator");
- if ([watchChange isKindOfClass:[FSTDocumentWatchChange class]]) {
- [self addDocumentChange:(FSTDocumentWatchChange *)watchChange];
- } else if ([watchChange isKindOfClass:[FSTWatchTargetChange class]]) {
- [self addTargetChange:(FSTWatchTargetChange *)watchChange];
- } else if ([watchChange isKindOfClass:[FSTExistenceFilterWatchChange class]]) {
- [self addExistenceFilterChange:(FSTExistenceFilterWatchChange *)watchChange];
- } else {
- HARD_FAIL("Unknown watch change: %s", watchChange);
- }
- }
- /**
- * Updates limbo document tracking for a given target-document mapping change. If the target is a
- * limbo target, and the change for the document has only seen limbo targets so far, and we are not
- * already tracking a change for this document, then consider this document a limbo document update.
- * Otherwise, ensure that we don't consider this document a limbo document. Returns true if the
- * change still has only seen limbo resolution changes.
- */
- - (BOOL)updateLimboDocumentsForKey:(const DocumentKey &)documentKey
- queryData:(FSTQueryData *)queryData
- isOnlyLimbo:(BOOL)isOnlyLimbo {
- if (!isOnlyLimbo) {
- // It wasn't a limbo doc before, so it definitely isn't now.
- return NO;
- }
- if (_documentUpdates.find(documentKey) == _documentUpdates.end()) {
- // We haven't seen a document update for this key yet.
- if (queryData.purpose == FSTQueryPurposeLimboResolution) {
- // We haven't seen this document before, and this target is a limbo target.
- _limboDocuments = _limboDocuments.insert(documentKey);
- return YES;
- } else {
- // We haven't seen the document before, but this is a non-limbo target.
- // Since we haven't seen it, we know it's not in our set of limbo docs. Return NO to ensure
- // that this key is marked as non-limbo.
- return NO;
- }
- } else if (queryData.purpose == FSTQueryPurposeLimboResolution) {
- // We have only seen limbo targets so far for this document, and this is another limbo target.
- return YES;
- } else {
- // We haven't marked this as non-limbo yet, but this target is not a limbo target.
- // Mark the key as non-limbo and make sure it isn't in our set.
- _limboDocuments = _limboDocuments.erase(documentKey);
- return NO;
- }
- }
- - (void)addDocumentChange:(FSTDocumentWatchChange *)docChange {
- BOOL relevant = NO;
- BOOL isOnlyLimbo = YES;
- for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) {
- FSTQueryData *queryData = [self queryDataForActiveTarget:targetID];
- if (queryData) {
- FSTTargetChange *change = [self targetChangeForTargetID:targetID];
- isOnlyLimbo = [self updateLimboDocumentsForKey:docChange.documentKey
- queryData:queryData
- isOnlyLimbo:isOnlyLimbo];
- [change.mapping addDocumentKey:docChange.documentKey];
- relevant = YES;
- }
- }
- for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) {
- FSTQueryData *queryData = [self queryDataForActiveTarget:targetID];
- if (queryData) {
- FSTTargetChange *change = [self targetChangeForTargetID:targetID];
- isOnlyLimbo = [self updateLimboDocumentsForKey:docChange.documentKey
- queryData:queryData
- isOnlyLimbo:isOnlyLimbo];
- [change.mapping removeDocumentKey:docChange.documentKey];
- relevant = YES;
- }
- }
- // Only update the document if there is a new document to replace, this might be just a target
- // update instead.
- if (docChange.document && relevant) {
- _documentUpdates[docChange.documentKey] = docChange.document;
- }
- }
- - (void)addTargetChange:(FSTWatchTargetChange *)targetChange {
- for (FSTBoxedTargetID *targetID in targetChange.targetIDs) {
- FSTTargetChange *change = [self targetChangeForTargetID:targetID];
- switch (targetChange.state) {
- case FSTWatchTargetChangeStateNoChange:
- if ([self isActiveTarget:targetID]) {
- // Creating the change above satisfies the semantics of no-change.
- change.resumeToken = targetChange.resumeToken;
- }
- break;
- case FSTWatchTargetChangeStateAdded:
- [self recordResponseForTargetID:targetID];
- if (!self.pendingTargetResponses[targetID]) {
- // We have a freshly added target, so we need to reset any state that we had previously
- // This can happen e.g. when remove and add back a target for existence filter
- // mismatches.
- change.mapping = nil;
- change.currentStatusUpdate = FSTCurrentStatusUpdateNone;
- [_existenceFilters removeObjectForKey:targetID];
- }
- change.resumeToken = targetChange.resumeToken;
- break;
- case FSTWatchTargetChangeStateRemoved:
- // We need to keep track of removed targets to we can post-filter and remove any target
- // changes.
- [self recordResponseForTargetID:targetID];
- HARD_ASSERT(!targetChange.cause, "WatchChangeAggregator does not handle errored targets.");
- break;
- case FSTWatchTargetChangeStateCurrent:
- if ([self isActiveTarget:targetID]) {
- change.currentStatusUpdate = FSTCurrentStatusUpdateMarkCurrent;
- change.resumeToken = targetChange.resumeToken;
- }
- break;
- case FSTWatchTargetChangeStateReset:
- if ([self isActiveTarget:targetID]) {
- // Overwrite any existing target mapping with a reset mapping. Every subsequent update
- // will modify the reset mapping, not an update mapping.
- change.mapping = [[FSTResetMapping alloc] init];
- change.resumeToken = targetChange.resumeToken;
- }
- break;
- default:
- LOG_WARN("Unknown target watch change type: %s", targetChange.state);
- }
- }
- }
- /**
- * Records that we got a watch target add/remove by decrementing the number of pending target
- * responses that we have.
- */
- - (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID {
- NSNumber *count = self.pendingTargetResponses[targetID];
- int newCount = count ? [count intValue] - 1 : -1;
- if (newCount == 0) {
- [self.pendingTargetResponses removeObjectForKey:targetID];
- } else {
- self.pendingTargetResponses[targetID] = @(newCount);
- }
- }
- /**
- * Returns true if the given targetId is active. Active targets are those for which there are no
- * pending requests to add a listen and are in the current list of targets the client cares about.
- *
- * Clients can repeatedly listen and stop listening to targets, so this check is useful in
- * preventing in preventing race conditions for a target where events arrive but the server hasn't
- * yet acknowledged the intended change in state.
- */
- - (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID {
- return [self queryDataForActiveTarget:targetID] != nil;
- }
- - (FSTQueryData *_Nullable)queryDataForActiveTarget:(FSTBoxedTargetID *)targetID {
- FSTQueryData *queryData = self.listenTargets[targetID];
- return (queryData && !self.pendingTargetResponses[targetID]) ? queryData : nil;
- }
- - (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange {
- FSTBoxedTargetID *targetID = @(existenceFilterChange.targetID);
- if ([self isActiveTarget:targetID]) {
- _existenceFilters[targetID] = existenceFilterChange.filter;
- }
- }
- - (FSTRemoteEvent *)remoteEvent {
- NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges = self.targetChanges;
- NSMutableArray *targetsToRemove = [NSMutableArray array];
- // Apply any inactive targets.
- for (FSTBoxedTargetID *targetID in [targetChanges keyEnumerator]) {
- if (![self isActiveTarget:targetID]) {
- [targetsToRemove addObject:targetID];
- }
- }
- [targetChanges removeObjectsForKeys:targetsToRemove];
- // Mark this aggregator as frozen so no further modifications are made.
- self.frozen = YES;
- return [[FSTRemoteEvent alloc] initWithSnapshotVersion:_snapshotVersion
- targetChanges:targetChanges
- documentUpdates:_documentUpdates
- limboDocuments:_limboDocuments];
- }
- @end
- NS_ASSUME_NONNULL_END
|