aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore
diff options
context:
space:
mode:
authorGravatar Sebastian Schmidt <mrschmidt@google.com>2018-06-12 10:58:35 -0700
committerGravatar GitHub <noreply@github.com>2018-06-12 10:58:35 -0700
commitafea8d5aacf474b57b4364feda08be9ca195594b (patch)
treed43c39ae9f71e88d256012f4467cd2b707fc7ddd /Firestore
parent0db8ef7dbe6c8c191252d33090dbb88b98735148 (diff)
Refactor Remote Event (#1396)
Diffstat (limited to 'Firestore')
-rw-r--r--Firestore/Example/Tests/Core/FSTQueryListenerTests.mm14
-rw-r--r--Firestore/Example/Tests/Core/FSTViewTests.mm46
-rw-r--r--Firestore/Example/Tests/Integration/FSTDatastoreTests.mm5
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalStoreTests.mm32
-rw-r--r--Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm906
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSpecTests.mm13
-rw-r--r--Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json780
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json411
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limit_spec_test.json20
-rw-r--r--Firestore/Example/Tests/SpecTests/json/listen_spec_test.json1075
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.h54
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.mm135
-rw-r--r--Firestore/Source/Core/FSTSyncEngine.mm45
-rw-r--r--Firestore/Source/Core/FSTView.mm30
-rw-r--r--Firestore/Source/Local/FSTLocalStore.mm38
-rw-r--r--Firestore/Source/Remote/FSTRemoteEvent.h199
-rw-r--r--Firestore/Source/Remote/FSTRemoteEvent.mm853
-rw-r--r--Firestore/Source/Remote/FSTRemoteStore.h10
-rw-r--r--Firestore/Source/Remote/FSTRemoteStore.mm204
-rw-r--r--Firestore/Source/Remote/FSTWatchChange.mm2
20 files changed, 3673 insertions, 1199 deletions
diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm
index 5629075..ddd831a 100644
--- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm
+++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm
@@ -118,11 +118,7 @@ NS_ASSUME_NONNULL_BEGIN
FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}];
FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil);
-
- FSTTargetChange *ackTarget =
- [FSTTargetChange changeWithDocuments:@[]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
- FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], FSTTestTargetChangeMarkCurrent());
[listener queryDidChangeViewSnapshot:snap1];
XCTAssertEqualObjects(accum, @[]);
@@ -188,9 +184,7 @@ NS_ASSUME_NONNULL_BEGIN
FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}];
FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
- FSTTargetChange *ackTarget =
- [FSTTargetChange changeWithDocuments:@[ doc1 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
+ FSTTargetChange *ackTarget = FSTTestTargetChangeAckDocuments({doc1.key});
FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget);
FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil);
@@ -342,9 +336,7 @@ NS_ASSUME_NONNULL_BEGIN
FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil);
FSTViewSnapshot *snap3 =
- FSTTestApplyChanges(view, @[],
- [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+ FSTTestApplyChanges(view, @[], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key}));
[listener applyChangedOnlineState:FSTOnlineStateOnline]; // no event
[listener queryDidChangeViewSnapshot:snap1];
diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm
index ec62d82..8c8d8a0 100644
--- a/Firestore/Example/Tests/Core/FSTViewTests.mm
+++ b/Firestore/Example/Tests/Core/FSTViewTests.mm
@@ -56,10 +56,8 @@ NS_ASSUME_NONNULL_BEGIN
FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
FSTDocument *doc3 = FSTTestDoc("rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO);
- FSTViewSnapshot *_Nullable snapshot =
- FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ],
- [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+ FSTViewSnapshot *_Nullable snapshot = FSTTestApplyChanges(
+ view, @[ doc1, doc2, doc3 ], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key}));
XCTAssertEqual(snapshot.query, query);
@@ -90,8 +88,7 @@ NS_ASSUME_NONNULL_BEGIN
// delete doc2, add doc3
FSTViewSnapshot *snapshot =
FSTTestApplyChanges(view, @[ FSTTestDeletedDoc("rooms/eros/messages/2", 0), doc3 ],
- [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+ FSTTestTargetChangeAckDocuments({doc1.key, doc3.key}));
XCTAssertEqual(snapshot.query, query);
@@ -216,10 +213,8 @@ NS_ASSUME_NONNULL_BEGIN
FSTTestApplyChanges(view, @[ doc1, doc3 ], nil);
// add doc2, which should push out doc3
- FSTViewSnapshot *snapshot =
- FSTTestApplyChanges(view, @[ doc2 ],
- [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(
+ view, @[ doc2 ], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key}));
XCTAssertEqual(snapshot.query, query);
@@ -263,9 +258,8 @@ NS_ASSUME_NONNULL_BEGIN
previousChanges:viewDocChanges];
FSTViewSnapshot *snapshot =
[view applyChangesToDocuments:viewDocChanges
- targetChange:[FSTTargetChange
- changeWithDocuments:@[ doc1, doc2, doc3, doc4 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]
+ targetChange:FSTTestTargetChangeAckDocuments(
+ {doc1.key, doc2.key, doc3.key, doc4.key})]
.snapshot;
XCTAssertEqual(snapshot.query, query);
@@ -294,27 +288,21 @@ NS_ASSUME_NONNULL_BEGIN
applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]];
XCTAssertEqualObjects(change.limboChanges, @[]);
- change =
- [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
- targetChange:[FSTTargetChange
- changeWithDocuments:@[]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]];
+ change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
+ targetChange:FSTTestTargetChangeMarkCurrent()];
XCTAssertEqualObjects(
change.limboChanges,
@[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]);
- change = [view
- applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
- targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ]
- currentStatusUpdate:FSTCurrentStatusUpdateNone]];
+ change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
+ targetChange:FSTTestTargetChangeAckDocuments({doc1.key})];
XCTAssertEqualObjects(
change.limboChanges,
@[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]);
- change = [view
- applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]
- targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ]
- currentStatusUpdate:FSTCurrentStatusUpdateNone]];
+ change =
+ [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]
+ targetChange:FSTTestTargetChangeAckDocuments({doc2.key})];
XCTAssertEqualObjects(change.limboChanges, @[]);
change = [view
@@ -343,11 +331,9 @@ NS_ASSUME_NONNULL_BEGIN
FSTView *view =
[[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{doc1.key, doc2.key}];
- FSTTargetChange *markCurrent =
- [FSTTargetChange changeWithDocuments:@[]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])];
- FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent];
+ FSTViewChange *change =
+ [view applyChangesToDocuments:changes targetChange:FSTTestTargetChangeMarkCurrent()];
XCTAssertEqualObjects(change.limboChanges, @[]);
}
diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm
index e6e1a19..cba9017 100644
--- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm
+++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm
@@ -48,6 +48,7 @@ namespace util = firebase::firestore::util;
using firebase::firestore::auth::EmptyCredentialsProvider;
using firebase::firestore::core::DatabaseInfo;
using firebase::firestore::model::DatabaseId;
+using firebase::firestore::model::DocumentKeySet;
using firebase::firestore::model::Precondition;
using firebase::firestore::model::TargetId;
@@ -121,6 +122,10 @@ NS_ASSUME_NONNULL_BEGIN
HARD_FAIL("Not implemented");
}
+- (DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetId {
+ return DocumentKeySet{};
+}
+
- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
[self.listenEvents addObject:remoteEvent];
XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0];
diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm
index e10fb12..8760571 100644
--- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm
+++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm
@@ -609,8 +609,8 @@ NS_ASSUME_NONNULL_BEGIN
FSTQuery *query = FSTTestQuery("foo");
FSTTargetID targetID = [self allocateQuery:query];
- [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO),
- @[ @(targetID) ], @[])];
+ [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO),
+ @[ @(targetID) ])];
[self collectGarbage];
FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO));
@@ -703,8 +703,8 @@ NS_ASSUME_NONNULL_BEGIN
FSTQuery *query = FSTTestQuery("foo");
FSTTargetID targetID = [self allocateQuery:query];
- [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO),
- @[ @(targetID) ], @[])];
+ [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO),
+ @[ @(targetID) ])];
[self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})];
[self collectGarbage];
FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO));
@@ -805,21 +805,19 @@ NS_ASSUME_NONNULL_BEGIN
FSTBoxedTargetID *targetID = @(queryData.targetID);
NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000);
- FSTWatchChange *watchChange =
+ FSTWatchTargetChange *watchChange =
[FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
targetIDs:@[ targetID ]
resumeToken:resumeToken];
NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listens =
[NSMutableDictionary dictionary];
listens[targetID] = queryData;
- NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses =
- [NSMutableDictionary dictionary];
- FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(1000)
- listenTargets:listens
- pendingTargetResponses:pendingResponses];
- [aggregator addWatchChanges:@[ watchChange ]];
- FSTRemoteEvent *remoteEvent = [aggregator remoteEvent];
+ FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc]
+ initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider
+ providerWithSingleResultForKey:testutil::Key("foo/bar")
+ targets:@[ targetID ]]];
+ [aggregator handleTargetChange:watchChange];
+ FSTRemoteEvent *remoteEvent = [aggregator remoteEventAtSnapshotVersion:testutil::Version(1000)];
[self applyRemoteEvent:remoteEvent];
// Stop listening so that the query should become inactive (but persistent)
@@ -842,10 +840,10 @@ NS_ASSUME_NONNULL_BEGIN
[self allocateQuery:query];
FSTAssertTargetID(2);
- [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO),
- @[ @2 ], @[])];
- [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO),
- @[ @2 ], @[])];
+ [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO),
+ @[ @2 ])];
+ [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO),
+ @[ @2 ])];
[self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]];
diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm
index 84d0fa1..c6936f7 100644
--- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm
+++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm
@@ -34,6 +34,7 @@
namespace testutil = firebase::firestore::testutil;
using firebase::firestore::model::DocumentKey;
using firebase::firestore::model::DocumentKeySet;
+using firebase::firestore::model::SnapshotVersion;
NS_ASSUME_NONNULL_BEGIN
@@ -42,86 +43,204 @@ NS_ASSUME_NONNULL_BEGIN
@implementation FSTRemoteEventTests {
NSData *_resumeToken1;
- NSMutableDictionary<NSNumber *, NSNumber *> *_noPendingResponses;
+ NSMutableDictionary<NSNumber *, NSNumber *> *_noOutstandingResponses;
+ FSTTestTargetMetadataProvider *_targetMetadataProvider;
}
- (void)setUp {
_resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding];
- _noPendingResponses = [NSMutableDictionary dictionary];
+ _noOutstandingResponses = [NSMutableDictionary dictionary];
+ _targetMetadataProvider = [FSTTestTargetMetadataProvider new];
}
-- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray<NSNumber *> *)targets
- outstanding:
- (NSDictionary<NSNumber *, NSNumber *> *)outstanding
- changes:(NSArray<FSTWatchChange *> *)watchChanges {
- NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
- FSTQueryData *dummyQueryData = [FSTQueryData alloc];
- for (NSNumber *targetID in targets) {
- listens[targetID] = dummyQueryData;
+/**
+ * Creates a map with query data for the provided target IDs. All targets are considered active
+ * and query a collection named "coll".
+ */
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)queryDataForTargets:
+ (NSArray<FSTBoxedTargetID *> *)targetIDs {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targets =
+ [NSMutableDictionary dictionary];
+ for (FSTBoxedTargetID *targetID in targetIDs) {
+ FSTQuery *query = FSTTestQuery("coll");
+ targets[targetID] = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID.intValue
+ listenSequenceNumber:0
+ purpose:FSTQueryPurposeListen];
+ }
+ return targets;
+}
+
+/**
+ * Creates a map with query data for the provided target IDs. All targets are marked as limbo
+ * queries for the document at "coll/limbo".
+ */
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)queryDataForLimboTargets:
+ (NSArray<FSTBoxedTargetID *> *)targetIDs {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targets =
+ [NSMutableDictionary dictionary];
+ for (FSTBoxedTargetID *targetID in targetIDs) {
+ FSTQuery *query = FSTTestQuery("coll/limbo");
+ targets[targetID] = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID.intValue
+ listenSequenceNumber:0
+ purpose:FSTQueryPurposeLimboResolution];
}
+ return targets;
+}
+
+/**
+ * Creates an aggregator initialized with the set of provided FSTWatchChanges. Tests can add further
+ * changes via `handleDocumentChange`, `handleTargetChange` and `handleExistenceFilterChange`.
+ *
+ * @param snapshotVersion The version at which to create the remote event. This corresponds to the
+ * snapshot version provided by a NO_CHANGE event.
+ * @param targetMap A map of query data for all active targets. The map must include an entry for
+ * every target referenced by any of the watch changes.
+ * @param outstandingResponses The number of outstanding ACKs a target has to receive before it is
+ * considered active, or `_noOutstandingResponses` if all targets are already active.
+ * @param existingKeys The set of documents that are considered synced with the test targets as
+ * part of a previous listen. To modify this set during test execution, invoke
+ * `[_targetMetadataProvider setSyncedKeys:forQueryData:]`.
+ * @param watchChanges The watch changes to apply before returning the aggregator. Supported
+ * changes are FSTDocumentWatchChange and FSTWatchTargetChange.
+ */
+- (FSTWatchChangeAggregator *)
+aggregatorWithTargetMap:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)targetMap
+ outstandingResponses:
+ (nullable NSDictionary<FSTBoxedTargetID *, NSNumber *> *)outstandingResponses
+ existingKeys:(DocumentKeySet)existingKeys
+ changes:(NSArray<FSTWatchChange *> *)watchChanges {
FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3)
- listenTargets:listens
- pendingTargetResponses:outstanding];
- [aggregator addWatchChanges:watchChanges];
+ [[FSTWatchChangeAggregator alloc] initWithTargetMetadataProvider:_targetMetadataProvider];
+
+ NSMutableArray<FSTBoxedTargetID *> *targetIDs = [NSMutableArray array];
+ [targetMap enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID,
+ FSTQueryData *queryData, BOOL *stop) {
+ [targetIDs addObject:targetID];
+ [_targetMetadataProvider setSyncedKeys:existingKeys forQueryData:queryData];
+ }];
+
+ [outstandingResponses
+ enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID, NSNumber *count, BOOL *stop) {
+ for (int i = 0; i < count.intValue; ++i) {
+ [aggregator recordTargetRequest:targetID];
+ }
+ }];
+
+ for (FSTWatchChange *change in watchChanges) {
+ if ([change isKindOfClass:[FSTDocumentWatchChange class]]) {
+ [aggregator handleDocumentChange:(FSTDocumentWatchChange *)change];
+ } else if ([change isKindOfClass:[FSTWatchTargetChange class]]) {
+ [aggregator handleTargetChange:(FSTWatchTargetChange *)change];
+ } else {
+ HARD_ASSERT("Encountered unexpected type of FSTWatchChange");
+ }
+ }
+
+ [aggregator handleTargetChange:[[FSTWatchTargetChange alloc]
+ initWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:targetIDs
+ resumeToken:_resumeToken1
+ cause:nil]];
+
return aggregator;
}
+/**
+ * Creates a single remote event that includes target changes for all provided FSTWatchChanges.
+ *
+ * @param snapshotVersion The version at which to create the remote event. This corresponds to the
+ * snapshot version provided by the NO_CHANGE event.
+ * @param targetMap A map of query data for all active targets. The map must include an entry for
+ * every target referenced by any of the watch changes.
+ * @param outstandingResponses The number of outstanding ACKs a target has to receive before it is
+ * considered active, or `_noOutstandingResponses` if all targets are already active.
+ * @param existingKeys The set of documents that are considered synced with the test targets as
+ * part of a previous listen.
+ * @param watchChanges The watch changes to apply before creating the remote event. Supported
+ * changes are FSTDocumentWatchChange and FSTWatchTargetChange.
+ */
+- (FSTRemoteEvent *)
+remoteEventAtSnapshotVersion:(FSTTestSnapshotVersion)snapshotVersion
+ targetMap:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)targetMap
+ outstandingResponses:
+ (nullable NSDictionary<FSTBoxedTargetID *, NSNumber *> *)outstandingResponses
+ existingKeys:(DocumentKeySet)existingKeys
+ changes:(NSArray<FSTWatchChange *> *)watchChanges {
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:outstandingResponses
+ existingKeys:existingKeys
+ changes:watchChanges];
+ return [aggregator remoteEventAtSnapshotVersion:testutil::Version(snapshotVersion)];
+}
+
- (void)testWillAccumulateDocumentAddedAndRemovedEvents {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
+ // The target map that contains an entry for every target in this test. If a target ID is omitted,
+ // the target is considered inactive and FSTTestTargetMetadataProvider will fail on access.
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2, @3, @4, @5, @6 ]];
+ FSTDocument *existingDoc = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ]
removedTargetIDs:@[ @4, @5, @6 ]
- documentKey:doc1.key
- document:doc1];
+ documentKey:existingDoc.key
+ document:existingDoc];
+ FSTDocument *newDoc = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ]
removedTargetIDs:@[ @2, @6 ]
- documentKey:doc2.key
- document:doc2];
-
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ documentKey:newDoc.key
+ document:newDoc];
+
+ // Create a remote event that includes both `change1` and `change2` as well as a NO_CHANGE event
+ // with the default resume token (`_resumeToken1`).
+ // As `existingDoc` is provided as an existing key, any updates to this document will be treated
+ // as modifications rather than adds.
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{existingDoc.key}
+ changes:@[ change1, change2 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 2);
- XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
- XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
+ XCTAssertEqualObjects(event.documentUpdates.at(existingDoc.key), existingDoc);
+ XCTAssertEqualObjects(event.documentUpdates.at(newDoc.key), newDoc);
- XCTAssertEqual(event.targetChanges.count, 6);
+ // 'change1' and 'change2' affect six different targets
+ XCTAssertEqual(event.targetChanges.size(), 6);
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ FSTTargetChange *targetChange1 =
+ FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key},
+ DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
- FSTUpdateMapping *mapping2 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]];
- XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2);
+ FSTTargetChange *targetChange2 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
- FSTUpdateMapping *mapping3 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3);
+ FSTTargetChange *targetChange3 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3);
- FSTUpdateMapping *mapping4 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]];
- XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4);
+ FSTTargetChange *targetChange4 =
+ FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{},
+ DocumentKeySet{existingDoc.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(4), targetChange4);
- FSTUpdateMapping *mapping5 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]];
- XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5);
+ FSTTargetChange *targetChange5 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(5), targetChange5);
- FSTUpdateMapping *mapping6 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]];
- XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6);
+ FSTTargetChange *targetChange6 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(6), targetChange6);
}
- (void)testWillIgnoreEventsForPendingTargets {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
@@ -135,31 +254,34 @@ NS_ASSUME_NONNULL_BEGIN
targetIDs:@[ @1 ]
cause:nil];
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc2.key
document:doc2];
// We're waiting for the unwatch and watch ack
- NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2 };
-
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ]
- outstanding:pendingResponses
- changes:@[ change1, change2, change3, change4 ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @2 };
+
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:outstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ change1, change2, change3, change4 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
// doc1 is ignored because it was part of an inactive target, but doc2 is in the changes
// because it become active.
XCTAssertEqual(event.documentUpdates.size(), 1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
}
- (void)testWillIgnoreEventsForRemovedTargets {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
@@ -170,25 +292,25 @@ NS_ASSUME_NONNULL_BEGIN
cause:nil];
// We're waiting for the unwatch ack
- NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @1 };
+ NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @1 };
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:outstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ change1, change2 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
// doc1 is ignored because it was part of an inactive target
XCTAssertEqual(event.documentUpdates.size(), 0);
// Target 1 is ignored because it was removed
- XCTAssertEqual(event.targetChanges.count, 0);
+ XCTAssertEqual(event.targetChanges.size(), 0);
}
- (void)testWillKeepResetMappingEvenWithUpdates {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
- FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
@@ -199,10 +321,13 @@ NS_ASSUME_NONNULL_BEGIN
cause:nil];
// Add doc2, doc3
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc2.key
document:doc2];
+
+ FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO);
FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc3.key
@@ -213,203 +338,208 @@ NS_ASSUME_NONNULL_BEGIN
removedTargetIDs:@[ @1 ]
documentKey:doc2.key
document:doc2];
-
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2, change3, change4, change5 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{doc1.key}
+ changes:@[ change1, change2, change3, change4, change5 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 3);
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3);
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
// Only doc3 is part of the new mapping
- FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]];
-
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping);
+ FSTTargetChange *expectedChange = FSTTestTargetChange(
+ DocumentKeySet{doc3.key}, DocumentKeySet{}, DocumentKeySet{doc1.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange);
}
- (void)testWillHandleSingleReset {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+
// Reset target
- FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
- targetIDs:@[ @1 ]
- cause:nil];
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1 ]
+ cause:nil];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
+ [aggregator handleTargetChange:change];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 0);
-
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
// Reset mapping is empty
- FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping);
+ FSTTargetChange *expectedChange =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange);
}
- (void)testWillHandleTargetAddAndRemovalInSameBatch {
- FSTDocument *doc1a = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{ @"value" : @2 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2 ]];
+ FSTDocument *doc1a = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[ @2 ]
documentKey:doc1a.key
document:doc1a];
+ FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{ @"value" : @2 }, NO);
FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ]
removedTargetIDs:@[ @1 ]
documentKey:doc1b.key
document:doc1b];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2 ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{doc1a.key}
+ changes:@[ change1, change2 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 1);
XCTAssertEqualObjects(event.documentUpdates.at(doc1b.key), doc1b);
- XCTAssertEqual(event.targetChanges.count, 2);
+ XCTAssertEqual(event.targetChanges.size(), 2);
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ FSTTargetChange *targetChange1 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1b.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
- FSTUpdateMapping *mapping2 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2);
+ FSTTargetChange *targetChange2 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc1b.key},
+ DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
}
- (void)testTargetCurrentChangeWillMarkTheTargetCurrent {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+
FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
targetIDs:@[ @1 ]
resumeToken:_resumeToken1];
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ change ]];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 0);
- XCTAssertEqual(event.targetChanges.count, 1);
- FSTTargetChange *targetChange = event.targetChanges[@1];
- XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]);
- XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
+
+ FSTTargetChange *targetChange =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange);
}
- (void)testTargetAddedChangeWillResetPreviousState {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @3 ]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ]
removedTargetIDs:@[ @2 ]
documentKey:doc1.key
document:doc1];
+
FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
targetIDs:@[ @1, @2, @3 ]
resumeToken:_resumeToken1];
+
FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
targetIDs:@[ @1 ]
cause:nil];
+
FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
targetIDs:@[ @2 ]
cause:nil];
+
FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
targetIDs:@[ @1 ]
cause:nil];
+
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[ @3 ]
documentKey:doc2.key
document:doc2];
- NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2, @2 : @1 };
+ NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @2, @2 : @1 };
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1, @3 ]
- outstanding:pendingResponses
- changes:@[ change1, change2, change3, change4, change5, change6 ]];
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:outstandingResponses
+ existingKeys:DocumentKeySet{doc2.key}
+ changes:@[ change1, change2, change3, change4, change5, change6 ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 2);
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
// target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove
- XCTAssertEqual(event.targetChanges.count, 2);
+ XCTAssertEqual(event.targetChanges.size(), 2);
- // doc1 was before the remove, so it does not show up in the mapping
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
- // Current was before the remove
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone);
+ // doc1 was before the remove, so it does not show up in the mapping.
+ // Current was before the remove.
+ FSTTargetChange *targetChange1 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc2.key},
+ DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
// Doc1 was before the remove
- FSTUpdateMapping *mapping3 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]];
- XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3);
// Current was before the remove
- XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1);
+ FSTTargetChange *targetChange3 = FSTTestTargetChange(
+ DocumentKeySet{doc1.key}, DocumentKeySet{}, DocumentKeySet{doc2.key}, _resumeToken1, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3);
}
- (void)testNoChangeWillStillMarkTheAffectedTargets {
- FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
- targetIDs:@[ @1 ]
- resumeToken:_resumeToken1];
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
- XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.documentUpdates.size(), 0);
- XCTAssertEqual(event.targetChanges.count, 1);
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]);
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone);
- XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
-}
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
-- (void)testExistenceFiltersWillReplacePreviousExistenceFilters {
- FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1];
- FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2];
- FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1];
- FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2];
- // replace filter1 for target 2
- FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2];
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ [aggregator handleTargetChange:change];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1, @2 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2, change3 ]];
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 0);
- XCTAssertEqual(event.targetChanges.count, 0);
- XCTAssertEqual(aggregator.existenceFilters.count, 2);
- XCTAssertEqual(aggregator.existenceFilters[@1], filter1);
- XCTAssertEqual(aggregator.existenceFilters[@2], filter2);
+ XCTAssertEqual(event.targetChanges.size(), 1);
+
+ FSTTargetChange *targetChange =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange);
}
-- (void)testExistenceFilterMismatchResetsTarget {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
+- (void)testExistenceFilterMismatchClearsTarget {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2 ]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
document:doc1];
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc2.key
@@ -420,270 +550,353 @@ NS_ASSUME_NONNULL_BEGIN
resumeToken:_resumeToken1];
FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2, change3 ]];
+ [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{doc1.key, doc2.key}
+ changes:@[ change1, change2, change3 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 2);
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 2);
+
+ FSTTargetChange *targetChange1 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}, _resumeToken1, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
+
+ FSTTargetChange *targetChange2 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
+
+ // The existence filter mismatch will remove the document from target 1,
+ // but not synthesize a document delete.
+ FSTExistenceFilterWatchChange *change4 =
+ [FSTExistenceFilterWatchChange changeWithFilter:[FSTExistenceFilter filterWithCount:1]
+ targetID:1];
+ [aggregator handleExistenceFilter:change4];
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
+ event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(4)];
- [event handleExistenceFilterMismatchForTargetID:@1];
+ FSTTargetChange *targetChange3 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, [NSData data], NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange3);
- // Mapping is reset
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]);
- // Reset the resume snapshot
- XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(0));
- // Target needs to be set to not current
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent);
- XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0);
+ XCTAssertEqual(event.targetChanges.size(), 1);
+ XCTAssertEqual(event.targetMismatches.size(), 1);
+ XCTAssertEqual(event.documentUpdates.size(), 0);
}
-- (void)testDocumentUpdate {
+- (void)testExistenceFilterMismatchRemovesCurrentChanges {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
+
+ FSTWatchTargetChange *markCurrent =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ [aggregator handleTargetChange:markCurrent];
+
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDeletedDocument *deletedDoc1 =
- [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3)];
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
- FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO);
+ FSTDocumentWatchChange *addDoc = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+ [aggregator handleDocumentChange:addDoc];
+
+ // The existence filter mismatch will remove the document from target 1, but not synthesize a
+ // document delete.
+ FSTExistenceFilterWatchChange *existenceFilter =
+ [FSTExistenceFilterWatchChange changeWithFilter:[FSTExistenceFilter filterWithCount:0]
+ targetID:1];
+ [aggregator handleExistenceFilter:existenceFilter];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
+ XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
+ XCTAssertEqual(event.documentUpdates.size(), 1);
+ XCTAssertEqual(event.targetMismatches.size(), 1);
+ XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
+
+ XCTAssertEqual(event.targetChanges.size(), 1);
+
+ FSTTargetChange *targetChange1 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
+}
+
+- (void)testDocumentUpdate {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
document:doc1];
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc2.key
document:doc2];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2 ]];
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 2);
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
- // Update doc1
- [event addDocumentUpdate:deletedDoc1];
- [event addDocumentUpdate:doc3];
+ [_targetMetadataProvider setSyncedKeys:DocumentKeySet{doc1.key, doc2.key}
+ forQueryData:targetMap[@1]];
+
+ FSTDeletedDocument *deletedDoc1 =
+ [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3)];
+ FSTDocumentWatchChange *change3 =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1 ]
+ documentKey:deletedDoc1.key
+ document:deletedDoc1];
+ [aggregator handleDocumentChange:change3];
+
+ FSTDocument *updatedDoc2 = FSTTestDoc("docs/2", 3, @{ @"value" : @2 }, NO);
+ FSTDocumentWatchChange *change4 =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:updatedDoc2.key
+ document:updatedDoc2];
+ [aggregator handleDocumentChange:change4];
+
+ FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO);
+ FSTDocumentWatchChange *change5 =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc3.key
+ document:doc3];
+ [aggregator handleDocumentChange:change5];
+
+ event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 3);
// doc1 is replaced
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), deletedDoc1);
- // doc2 is untouched
- XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
+ // doc2 is updated
+ XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), updatedDoc2);
// doc3 is new
XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3);
// Target is unchanged
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ FSTTargetChange *targetChange =
+ FSTTestTargetChange(DocumentKeySet{doc3.key}, DocumentKeySet{updatedDoc2.key},
+ DocumentKeySet{deletedDoc1.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange);
}
- (void)testResumeTokensHandledPerTarget {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2 ]];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
+
+ FSTWatchTargetChange *change1 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ [aggregator handleTargetChange:change1];
+
NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
- FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
- targetIDs:@[ @1 ]
- resumeToken:_resumeToken1];
- FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
- targetIDs:@[ @2 ]
- resumeToken:resumeToken2];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
- XCTAssertEqual(event.targetChanges.count, 2);
-
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
-
- XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@2].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2);
+ FSTWatchTargetChange *change2 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @2 ]
+ resumeToken:resumeToken2];
+ [aggregator handleTargetChange:change2];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
+ XCTAssertEqual(event.targetChanges.size(), 2);
+
+ FSTTargetChange *targetChange1 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
+
+ FSTTargetChange *targetChange2 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
}
- (void)testLastResumeTokenWins {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2 ]];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
+
+ FSTWatchTargetChange *change1 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ [aggregator handleTargetChange:change1];
+
NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
+ FSTWatchTargetChange *change2 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @1 ]
+ resumeToken:resumeToken2];
+ [aggregator handleTargetChange:change2];
+
NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding];
+ FSTWatchTargetChange *change3 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @2 ]
+ resumeToken:resumeToken3];
+ [aggregator handleTargetChange:change3];
- FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
- targetIDs:@[ @1 ]
- resumeToken:_resumeToken1];
- FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
- targetIDs:@[ @1 ]
- resumeToken:resumeToken2];
- FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
- targetIDs:@[ @2 ]
- resumeToken:resumeToken3];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1, @2 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2, change3 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
- XCTAssertEqual(event.targetChanges.count, 2);
-
- FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2);
-
- XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@2].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone);
- XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3);
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
+ XCTAssertEqual(event.targetChanges.size(), 2);
+
+ FSTTargetChange *targetChange1 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
+
+ FSTTargetChange *targetChange2 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken3, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
}
- (void)testSynthesizeDeletes {
- FSTWatchChange *shouldSynthesize =
- [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ]];
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForLimboTargets:@[ @1 ]];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ shouldSynthesize ]];
+ DocumentKey limboKey = testutil::Key("coll/limbo");
- FSTRemoteEvent *event = [aggregator remoteEvent];
- DocumentKey synthesized = DocumentKey::FromPathString("docs/2");
- XCTAssertEqual(event.documentUpdates.find(synthesized), event.documentUpdates.end());
+ FSTWatchChange *resolveLimboTarget =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ]];
+
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ resolveLimboTarget ]];
- FSTTargetChange *limboTargetChange = event.targetChanges[@1];
- [event synthesizeDeleteForLimboTargetChange:limboTargetChange key:synthesized];
FSTDeletedDocument *expected =
- [FSTDeletedDocument documentWithKey:synthesized version:event.snapshotVersion];
- XCTAssertEqualObjects(expected, event.documentUpdates.at(synthesized));
- XCTAssertTrue(event.limboDocumentChanges.contains(synthesized));
+ [FSTDeletedDocument documentWithKey:limboKey version:event.snapshotVersion];
+ XCTAssertEqualObjects(event.documentUpdates.at(limboKey), expected);
+ XCTAssertTrue(event.limboDocumentChanges.contains(limboKey));
}
- (void)testDoesntSynthesizeDeletesForWrongState {
- FSTWatchChange *wrongState =
- [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @2 ]];
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForLimboTargets:@[ @1 ]];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @2 ] outstanding:_noPendingResponses changes:@[ wrongState ]];
+ FSTWatchChange *wrongState =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @1 ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ wrongState ]];
- DocumentKey notSynthesized = DocumentKey::FromPathString("docs/no1");
- [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@2] key:notSynthesized];
- XCTAssertEqual(event.documentUpdates.find(notSynthesized), event.documentUpdates.end());
- XCTAssertFalse(event.limboDocumentChanges.contains(notSynthesized));
+ XCTAssertEqual(event.documentUpdates.size(), 0);
+ XCTAssertEqual(event.limboDocumentChanges.size(), 0);
}
- (void)testDoesntSynthesizeDeletesForExistingDoc {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForLimboTargets:@[ @3 ]];
+
FSTWatchChange *hasDocument =
[FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @3 ]];
- FSTDocument *doc = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTWatchChange *docChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @3 ]
- removedTargetIDs:@[]
- documentKey:doc.key
- document:doc];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @3 ]
- outstanding:_noPendingResponses
- changes:@[ hasDocument, docChange ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
- [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@3] key:doc.key];
- FSTMaybeDocument *docData = event.documentUpdates.at(doc.key);
- XCTAssertFalse([docData isKindOfClass:[FSTDeletedDocument class]]);
- XCTAssertFalse(event.limboDocumentChanges.contains(doc.key));
+
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{FSTTestDocKey(@"coll/limbo")}
+ changes:@[ hasDocument ]];
+
+ XCTAssertEqual(event.documentUpdates.size(), 0);
+ XCTAssertEqual(event.limboDocumentChanges.size(), 0);
}
-- (void)testFilterUpdates {
+- (void)testSeparatesDocumentUpdates {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForLimboTargets:@[ @1 ]];
+
FSTDocument *newDoc = FSTTestDoc("docs/new", 1, @{@"key" : @"value"}, NO);
- FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO);
FSTWatchChange *newDocChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:newDoc.key
document:newDoc];
+ FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO);
FSTWatchChange *existingDocChange =
[[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:existingDoc.key
document:existingDoc];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ newDocChange, existingDocChange ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
- DocumentKeySet existingKeys = DocumentKeySet{existingDoc.key};
-
- FSTTargetChange *updateChange = event.targetChanges[@1];
- XCTAssertTrue([updateChange.mapping isKindOfClass:[FSTUpdateMapping class]]);
- FSTUpdateMapping *update = (FSTUpdateMapping *)updateChange.mapping;
- FSTDocumentKey *existingDocKey = existingDoc.key;
- FSTDocumentKey *newDocKey = newDoc.key;
- XCTAssertTrue(update.addedDocuments.contains(existingDocKey));
-
- [update filterUpdatesUsingExistingKeys:existingKeys];
- // Now it's been filtered, since it already existed.
- XCTAssertFalse(update.addedDocuments.contains(existingDocKey));
- XCTAssertTrue(update.addedDocuments.contains(newDocKey));
-}
-
-- (void)testDoesntFilterResets {
- FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO);
- const DocumentKey &existingDocKey = existingDoc.key;
- FSTWatchTargetChange *resetTargetChange =
- [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
- targetIDs:@[ @2 ]
- resumeToken:_resumeToken1];
- FSTWatchChange *existingDocChange =
- [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ]
- removedTargetIDs:@[]
- documentKey:existingDocKey
- document:existingDoc];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @2 ]
- outstanding:_noPendingResponses
- changes:@[ resetTargetChange, existingDocChange ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
- DocumentKeySet existingKeys = DocumentKeySet{existingDocKey};
-
- FSTTargetChange *resetChange = event.targetChanges[@2];
- XCTAssertTrue([resetChange.mapping isKindOfClass:[FSTResetMapping class]]);
- FSTResetMapping *resetMapping = (FSTResetMapping *)resetChange.mapping;
- XCTAssertTrue(resetMapping.documents.contains(existingDocKey));
-
- [resetMapping filterUpdatesUsingExistingKeys:existingKeys];
- // Document is still there, even though it already exists. Reset mappings don't get filtered.
- XCTAssertTrue(resetMapping.documents.contains(existingDocKey));
+ FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc("docs/deleted", 1);
+ FSTWatchChange *deletedDocChange =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1 ]
+ documentKey:deletedDoc.key
+ document:deletedDoc];
+
+ FSTDeletedDocument *missingDoc = FSTTestDeletedDoc("docs/missing", 1);
+ FSTWatchChange *missingDocChange =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1 ]
+ documentKey:missingDoc.key
+ document:missingDoc];
+
+ FSTRemoteEvent *event = [self
+ remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{existingDoc.key, deletedDoc.key}
+ changes:@[
+ newDocChange, existingDocChange, deletedDocChange, missingDocChange
+ ]];
+
+ FSTTargetChange *targetChange =
+ FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key},
+ DocumentKeySet{deletedDoc.key}, _resumeToken1, NO);
+
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange);
}
- (void)testTracksLimboDocuments {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [NSMutableDictionary dictionary];
+ [targetMap addEntriesFromDictionary:[self queryDataForTargets:@[ @1 ]]];
+ [targetMap addEntriesFromDictionary:[self queryDataForLimboTargets:@[ @2 ]]];
+
// Add 3 docs: 1 is limbo and non-limbo, 2 is limbo-only, 3 is non-limbo
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"key" : @"value"}, NO);
FSTDocument *doc2 = FSTTestDoc("docs/2", 1, @{@"key" : @"value"}, NO);
FSTDocument *doc3 = FSTTestDoc("docs/3", 1, @{@"key" : @"value"}, NO);
// Target 2 is a limbo target
-
FSTWatchChange *docChange1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2 ]
removedTargetIDs:@[]
documentKey:doc1.key
@@ -702,20 +915,13 @@ NS_ASSUME_NONNULL_BEGIN
FSTWatchChange *targetsChange =
[FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1, @2 ]];
- NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
- listens[@1] = [FSTQueryData alloc];
- listens[@2] = [[FSTQueryData alloc] initWithQuery:[FSTQuery alloc]
- targetID:2
- listenSequenceNumber:1000
- purpose:FSTQueryPurposeLimboResolution];
- FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3)
- listenTargets:listens
- pendingTargetResponses:@{}];
-
- [aggregator addWatchChanges:@[ docChange1, docChange2, docChange3, targetsChange ]];
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ docChange1, docChange2, docChange3, targetsChange ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
DocumentKeySet limboDocChanges = event.limboDocumentChanges;
// Doc1 is in both limbo and non-limbo targets, therefore not tracked as limbo
XCTAssertFalse(limboDocChanges.contains(doc1.key));
diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm
index 77010e5..c131f7e 100644
--- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm
+++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm
@@ -256,12 +256,15 @@ static NSString *const kNoIOSTag = @"no-ios";
} else if (watchEntity[@"doc"]) {
NSArray *docSpec = watchEntity[@"doc"];
FSTDocumentKey *key = FSTTestDocKey(docSpec[0]);
- FSTObjectValue *value = FSTTestObjectValue(docSpec[2]);
+ FSTObjectValue *_Nullable value =
+ [docSpec[2] isKindOfClass:[NSNull class]] ? nil : FSTTestObjectValue(docSpec[2]);
SnapshotVersion version = [self parseVersion:docSpec[1]];
- FSTMaybeDocument *doc = [FSTDocument documentWithData:value
- key:key
- version:std::move(version)
- hasLocalMutations:NO];
+ FSTMaybeDocument *doc =
+ value ? [FSTDocument documentWithData:value
+ key:key
+ version:std::move(version)
+ hasLocalMutations:NO]
+ : [FSTDeletedDocument documentWithKey:key version:std::move(version)];
FSTWatchChange *change =
[[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"]
removedTargetIDs:watchEntity[@"removedTargets"]
diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
index abd2cf4..3e5d4fb 100644
--- a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
+++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
@@ -1,4 +1,458 @@
{
+ "Existence filter match": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter match",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000
+ }
+ ]
+ },
+ "Existence filter match after pending update": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter match after pending update",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Existence filter with empty target": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter with empty target",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Existence filter ignored with pending target": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter ignored with pending target",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ]
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
"Existence filter mismatch triggers re-run of query": {
"describeName": "Existence Filters:",
"itName": "Existence filter mismatch triggers re-run of query",
@@ -501,6 +955,332 @@
}
]
},
+ "Existence filter handled at global snapshot": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter handled at global snapshot",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1",
+ "collection/2"
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/3",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/3",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/2",
+ 2000,
+ {
+ "v": 2
+ }
+ ],
+ [
+ "collection/3",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-3000"
+ ],
+ "watchSnapshot": 3000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/2",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Existence filter synthesizes deletes": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter synthesizes deletes",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ]
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
"Existence filter limbo resolution is denied": {
"describeName": "Existence Filters:",
"itName": "Existence filter limbo resolution is denied",
diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
index ee2d883..a186496 100644
--- a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
+++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
@@ -1146,5 +1146,416 @@
]
}
]
+ },
+ "Limbo documents handle receiving ack and then current": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents handle receiving ack and then current",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userPatch": [
+ "collection/a",
+ {
+ "include": false
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "limboDocs": [
+ "collection/b"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/b",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 1
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-3000"
+ ],
+ "watchSnapshot": 3000
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ]
+ ],
+ "removedTargets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ },
+ "watchSnapshot": 4000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ }
+ ]
}
}
diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
index 5a02463..6aa1daa 100644
--- a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
+++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
@@ -287,7 +287,15 @@
"targets": [
2
]
- },
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
"watchSnapshot": 2000,
"stateExpect": {
"limboDocs": [
@@ -1144,7 +1152,15 @@
"targets": [
2
]
- },
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
"watchSnapshot": 2000,
"stateExpect": {
"limboDocs": [
diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
index 7bfe557..e838d2f 100644
--- a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
+++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
@@ -238,6 +238,177 @@
}
]
},
+ "Doesn't raise events for empty target": {
+ "describeName": "Listens:",
+ "itName": "Doesn't raise events for empty target",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userListen": [
+ 6,
+ {
+ "path": "collection3",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "6": {
+ "query": {
+ "path": "collection3",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection2/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 6
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection2/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
"Ensure correct query results with latency-compensated deletes": {
"describeName": "Listens:",
"itName": "Ensure correct query results with latency-compensated deletes",
@@ -385,6 +556,79 @@
}
]
},
+ "Does not raise event for initial document delete": {
+ "describeName": "Listens:",
+ "itName": "Does not raise event for initial document delete",
+ "tags": [""],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ null
+ ]
+ ],
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1000
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
"Will process removals without waiting for a consistent snapshot": {
"describeName": "Listens:",
"itName": "Will process removals without waiting for a consistent snapshot",
@@ -1690,5 +1934,836 @@
]
}
]
+ },
+ "Synthesizes deletes for missing document": {
+ "describeName": "Listens:",
+ "itName": "Synthesizes deletes for missing document",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 4,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 4
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Re-opens target without existence filter": {
+ "describeName": "Listens:",
+ "itName": "Re-opens target without existence filter",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ null
+ ]
+ ],
+ "removedTargets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Ignores update from inactive target": {
+ "describeName": "Listens:",
+ "itName": "Ignores update from inactive target",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Does not synthesize deletes for previously acked documents": {
+ "describeName": "Listens:",
+ "itName": "Does not synthesize deletes for previously acked documents",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-2000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
}
}
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h
index ccc01ca..7946c06 100644
--- a/Firestore/Example/Tests/Util/FSTHelpers.h
+++ b/Firestore/Example/Tests/Util/FSTHelpers.h
@@ -16,11 +16,11 @@
#import <Foundation/Foundation.h>
-#include <map>
#include <vector>
#import "Firestore/Source/Core/FSTTypes.h"
#import "Firestore/Source/Model/FSTDocumentDictionary.h"
+#import "Firestore/Source/Remote/FSTRemoteEvent.h"
#include "Firestore/core/src/firebase/firestore/model/field_path.h"
#include "Firestore/core/src/firebase/firestore/model/field_value.h"
@@ -148,6 +148,42 @@ inline NSString *FSTRemoveExceptionPrefix(NSString *exception) {
XCTAssertTrue(didThrow, ##__VA_ARGS__); \
} while (0)
+/**
+ * An implementation of FSTTargetMetadataProvider that provides controlled access to the
+ * `FSTTargetMetadataProvider` callbacks. Any target accessed via these callbacks must be
+ * registered beforehand via the factory methods or via `setSyncedKeys:forQueryData:`.
+ */
+@interface FSTTestTargetMetadataProvider : NSObject <FSTTargetMetadataProvider>
+
+/**
+ * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for
+ * each of the given targets, where each target has previously seen query results containing just
+ * the given documentKey.
+ *
+ * Internally this means that the `remoteKeysForTarget` callback for these targets will return just
+ * the documentKey and that the provided targets will be returned as active from the
+ * `queryDataForTarget` target.
+ */
++ (instancetype)providerWithSingleResultForKey:(firebase::firestore::model::DocumentKey)documentKey
+ targets:(NSArray<FSTBoxedTargetID *> *)targets;
+
+/**
+ * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for
+ * each of the given targets, where each target has not seen any previous document.
+ *
+ * Internally this means that the `remoteKeysForTarget` callback for these targets will return an
+ * empty set of document keys and that the provided targets will be returned as active from the
+ * `queryDataForTarget` target.
+ */
++ (instancetype)providerWithEmptyResultForKey:(firebase::firestore::model::DocumentKey)documentKey
+ targets:(NSArray<FSTBoxedTargetID *> *)targets;
+
+/** Sets or replaces the local state for the provided query data. */
+- (void)setSyncedKeys:(firebase::firestore::model::DocumentKeySet)keys
+ forQueryData:(FSTQueryData *)queryData;
+
+@end
+
/** Creates a new FIRTimestamp from components. Note that year, month, and day are all one-based. */
FIRTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second);
@@ -250,6 +286,9 @@ FSTDeleteMutation *FSTTestDeleteMutation(NSString *path);
/** Converts a list of documents to a sorted map. */
FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs);
+/** Creates a remote event that inserts a new document. */
+FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, NSArray<NSNumber *> *addedToTargets);
+
/** Creates a remote event with changes to a document. */
FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
NSArray<NSNumber *> *updatedInTargets,
@@ -260,6 +299,19 @@ FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query,
NSArray<NSString *> *addedKeys,
NSArray<NSString *> *removedKeys);
+/** Creates a test target change that acks all 'docs' and marks the target as CURRENT */
+FSTTargetChange *FSTTestTargetChangeAckDocuments(firebase::firestore::model::DocumentKeySet docs);
+
+/** Creates a test target change that marks the target as CURRENT */
+FSTTargetChange *FSTTestTargetChangeMarkCurrent();
+
+/** Creates a test target change. */
+FSTTargetChange *FSTTestTargetChange(firebase::firestore::model::DocumentKeySet added,
+ firebase::firestore::model::DocumentKeySet modified,
+ firebase::firestore::model::DocumentKeySet removed,
+ NSData *resumeToken,
+ BOOL current);
+
/** Creates a resume token to match the given snapshot version. */
NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot);
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm
index 5ed4fd5..8ece82f 100644
--- a/Firestore/Example/Tests/Util/FSTHelpers.mm
+++ b/Firestore/Example/Tests/Util/FSTHelpers.mm
@@ -22,7 +22,7 @@
#include <cinttypes>
#include <list>
-#include <map>
+#include <unordered_map>
#include <utility>
#include <vector>
@@ -58,6 +58,7 @@ namespace util = firebase::firestore::util;
namespace testutil = firebase::firestore::testutil;
using firebase::firestore::model::DatabaseId;
using firebase::firestore::model::DocumentKey;
+using firebase::firestore::model::DocumentKeySet;
using firebase::firestore::model::FieldMask;
using firebase::firestore::model::FieldPath;
using firebase::firestore::model::FieldTransform;
@@ -65,8 +66,8 @@ using firebase::firestore::model::FieldValue;
using firebase::firestore::model::Precondition;
using firebase::firestore::model::ResourcePath;
using firebase::firestore::model::ServerTimestampTransform;
+using firebase::firestore::model::SnapshotVersion;
using firebase::firestore::model::TransformOperation;
-using firebase::firestore::model::DocumentKeySet;
NS_ASSUME_NONNULL_BEGIN
@@ -298,29 +299,125 @@ FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view,
.snapshot;
}
+@implementation FSTTestTargetMetadataProvider {
+ std::unordered_map<FSTTargetID, DocumentKeySet> _syncedKeys;
+ std::unordered_map<FSTTargetID, FSTQueryData *> _queryData;
+}
+
++ (instancetype)providerWithSingleResultForKey:(DocumentKey)documentKey
+ targets:(NSArray<FSTBoxedTargetID *> *)targets {
+ FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new];
+ FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()];
+
+ for (FSTBoxedTargetID *targetID in targets) {
+ FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID.intValue
+ listenSequenceNumber:0
+ purpose:FSTQueryPurposeListen];
+ [metadataProvider setSyncedKeys:DocumentKeySet{documentKey} forQueryData:queryData];
+ }
+
+ return metadataProvider;
+}
+
++ (instancetype)providerWithEmptyResultForKey:(DocumentKey)documentKey
+ targets:(NSArray<FSTBoxedTargetID *> *)targets {
+ FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new];
+ FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()];
+
+ for (FSTBoxedTargetID *targetID in targets) {
+ FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID.intValue
+ listenSequenceNumber:0
+ purpose:FSTQueryPurposeListen];
+ [metadataProvider setSyncedKeys:DocumentKeySet {} forQueryData:queryData];
+ }
+
+ return metadataProvider;
+}
+
+- (void)setSyncedKeys:(DocumentKeySet)keys forQueryData:(FSTQueryData *)queryData {
+ _syncedKeys[queryData.targetID] = keys;
+ _queryData[queryData.targetID] = queryData;
+}
+
+- (DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetID {
+ auto it = _syncedKeys.find(targetID.intValue);
+ HARD_ASSERT(it != _syncedKeys.end(), "Cannot process unknown target %s", targetID.intValue);
+ return it->second;
+}
+
+- (nullable FSTQueryData *)queryDataForTarget:(FSTBoxedTargetID *)targetID {
+ auto it = _queryData.find(targetID.intValue);
+ HARD_ASSERT(it != _queryData.end(), "Cannot process unknown target %s", targetID.intValue);
+ return it->second;
+}
+
+@end
+
+FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc,
+ NSArray<FSTBoxedTargetID *> *addedToTargets) {
+ HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations],
+ "Docs from remote updates shouldn't have local changes.");
+ FSTDocumentWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:addedToTargets
+ removedTargetIDs:{}
+ documentKey:doc.key
+ document:doc];
+ FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc]
+ initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider
+ providerWithEmptyResultForKey:doc.key
+ targets:addedToTargets]];
+ [aggregator handleDocumentChange:change];
+ return [aggregator remoteEventAtSnapshotVersion:doc.version];
+}
+
FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
- NSArray<NSNumber *> *updatedInTargets,
- NSArray<NSNumber *> *removedFromTargets) {
+ NSArray<FSTBoxedTargetID *> *updatedInTargets,
+ NSArray<FSTBoxedTargetID *> *removedFromTargets) {
+ HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations],
+ "Docs from remote updates shouldn't have local changes.");
FSTDocumentWatchChange *change =
[[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets
removedTargetIDs:removedFromTargets
documentKey:doc.key
document:doc];
- NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
- FSTQueryData *dummyQueryData = [FSTQueryData alloc];
- for (NSNumber *targetID in updatedInTargets) {
- listens[targetID] = dummyQueryData;
- }
- for (NSNumber *targetID in removedFromTargets) {
- listens[targetID] = dummyQueryData;
- }
- NSMutableDictionary<NSNumber *, NSNumber *> *pending = [NSMutableDictionary dictionary];
- FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version
- listenTargets:listens
- pendingTargetResponses:pending];
- [aggregator addWatchChange:change];
- return [aggregator remoteEvent];
+ NSArray<FSTBoxedTargetID *> *targets =
+ [updatedInTargets arrayByAddingObjectsFromArray:removedFromTargets];
+ FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc]
+ initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider
+ providerWithSingleResultForKey:doc.key
+ targets:targets]];
+ [aggregator handleDocumentChange:change];
+ return [aggregator remoteEventAtSnapshotVersion:doc.version];
+}
+
+FSTTargetChange *FSTTestTargetChangeMarkCurrent() {
+ return [[FSTTargetChange alloc] initWithResumeToken:[NSData data]
+ current:YES
+ addedDocuments:DocumentKeySet {}
+ modifiedDocuments:DocumentKeySet {}
+ removedDocuments:DocumentKeySet{}];
+}
+
+FSTTargetChange *FSTTestTargetChangeAckDocuments(DocumentKeySet docs) {
+ return [[FSTTargetChange alloc] initWithResumeToken:[NSData data]
+ current:YES
+ addedDocuments:docs
+ modifiedDocuments:DocumentKeySet {}
+ removedDocuments:DocumentKeySet{}];
+}
+
+FSTTargetChange *FSTTestTargetChange(DocumentKeySet added,
+ DocumentKeySet modified,
+ DocumentKeySet removed,
+ NSData *resumeToken,
+ BOOL current) {
+ return [[FSTTargetChange alloc] initWithResumeToken:resumeToken
+ current:current
+ addedDocuments:added
+ modifiedDocuments:modified
+ removedDocuments:removed];
}
/** Creates a resume token to match the given snapshot version. */
diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm
index 7d0c1a3..89cb774 100644
--- a/Firestore/Source/Core/FSTSyncEngine.mm
+++ b/Firestore/Source/Core/FSTSyncEngine.mm
@@ -295,21 +295,6 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1;
- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
[self assertDelegateExistsForSelector:_cmd];
- // Make sure limbo documents are deleted if there were no results.
- // Filter out document additions to targets that they already belong to.
- [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^(
- FSTBoxedTargetID *_Nonnull targetID,
- FSTTargetChange *_Nonnull targetChange, BOOL *_Nonnull stop) {
- const auto iter = self->_limboKeysByTarget.find([targetID intValue]);
- if (iter == self->_limboKeysByTarget.end()) {
- FSTQueryView *qv = self.queryViewsByTarget[targetID];
- HARD_ASSERT(qv, "Missing queryview for non-limbo query: %s", [targetID intValue]);
- [targetChange.mapping filterUpdatesUsingExistingKeys:qv.view.syncedDocuments];
- } else {
- [remoteEvent synthesizeDeleteForLimboTargetChange:targetChange key:iter->second];
- }
- }];
-
FSTMaybeDocumentDictionary *changes = [self.localStore applyRemoteEvent:remoteEvent];
[self emitNewSnapshotsWithChanges:changes remoteEvent:remoteEvent];
}
@@ -345,18 +330,16 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1;
// It's a limbo doc. Create a synthetic event saying it was deleted. This is kind of a hack.
// Ideally, we would have a method in the local store to purge a document. However, it would
// be tricky to keep all of the local store's invariants with another method.
- NSMutableDictionary<NSNumber *, FSTTargetChange *> *targetChanges =
- [NSMutableDictionary dictionary];
FSTDeletedDocument *doc =
[FSTDeletedDocument documentWithKey:limboKey version:SnapshotVersion::None()];
DocumentKeySet limboDocuments = DocumentKeySet{doc.key};
- FSTRemoteEvent *event =
- [[FSTRemoteEvent alloc] initWithSnapshotVersion:SnapshotVersion::None()
- targetChanges:targetChanges
- documentUpdates:{
- { limboKey, doc }
- }
- limboDocuments:std::move(limboDocuments)];
+ FSTRemoteEvent *event = [[FSTRemoteEvent alloc] initWithSnapshotVersion:SnapshotVersion::None()
+ targetChanges:{}
+ targetMismatches:{}
+ documentUpdates:{
+ { limboKey, doc }
+ }
+ limboDocuments:std::move(limboDocuments)];
[self applyRemoteEvent:event];
} else {
FSTQueryView *queryView = self.queryViewsByTarget[@(targetID)];
@@ -439,7 +422,14 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1;
FSTDocumentDictionary *docs = [self.localStore executeQuery:queryView.query];
viewDocChanges = [view computeChangesWithDocuments:docs previousChanges:viewDocChanges];
}
- FSTTargetChange *_Nullable targetChange = remoteEvent.targetChanges[@(queryView.targetID)];
+
+ FSTTargetChange *_Nullable targetChange = nil;
+ if (remoteEvent) {
+ auto it = remoteEvent.targetChanges.find(queryView.targetID);
+ if (it != remoteEvent.targetChanges.end()) {
+ targetChange = it->second;
+ }
+ }
FSTViewChange *viewChange =
[queryView.view applyChangesToDocuments:viewDocChanges targetChange:targetChange];
@@ -531,6 +521,11 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1;
[self.remoteStore userDidChange:user];
}
+- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetId {
+ FSTQueryView *queryView = self.queryViewsByTarget[targetId];
+ return queryView ? queryView.view.syncedDocuments : DocumentKeySet{};
+}
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm
index 63efd4e..f954731 100644
--- a/Firestore/Source/Core/FSTView.mm
+++ b/Firestore/Source/Core/FSTView.mm
@@ -401,28 +401,18 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang
*/
- (void)applyTargetChange:(nullable FSTTargetChange *)targetChange {
if (targetChange) {
- FSTTargetMapping *targetMapping = targetChange.mapping;
- if ([targetMapping isKindOfClass:[FSTResetMapping class]]) {
- _syncedDocuments = ((FSTResetMapping *)targetMapping).documents;
- } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) {
- for (const DocumentKey &key : ((FSTUpdateMapping *)targetMapping).addedDocuments) {
- _syncedDocuments = _syncedDocuments.insert(key);
- }
- for (const DocumentKey &key : ((FSTUpdateMapping *)targetMapping).removedDocuments) {
- _syncedDocuments = _syncedDocuments.erase(key);
- }
+ for (const DocumentKey &key : targetChange.addedDocuments) {
+ _syncedDocuments = _syncedDocuments.insert(key);
}
-
- switch (targetChange.currentStatusUpdate) {
- case FSTCurrentStatusUpdateMarkCurrent:
- self.current = YES;
- break;
- case FSTCurrentStatusUpdateMarkNotCurrent:
- self.current = NO;
- break;
- case FSTCurrentStatusUpdateNone:
- break;
+ for (const DocumentKey &key : targetChange.modifiedDocuments) {
+ HARD_ASSERT(_syncedDocuments.find(key) != _syncedDocuments.end(),
+ "Modified document %s not found in view.", key.ToString());
}
+ for (const DocumentKey &key : targetChange.removedDocuments) {
+ _syncedDocuments = _syncedDocuments.erase(key);
+ }
+
+ self.current = targetChange.current;
}
}
diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm
index 7469c71..6aab78e 100644
--- a/Firestore/Source/Local/FSTLocalStore.mm
+++ b/Firestore/Source/Local/FSTLocalStore.mm
@@ -268,46 +268,32 @@ NS_ASSUME_NONNULL_BEGIN
FSTListenSequenceNumber sequenceNumber = [self.listenSequence next];
id<FSTQueryCache> queryCache = self.queryCache;
- [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^(
- NSNumber *targetIDNumber, FSTTargetChange *change, BOOL *stop) {
- FSTTargetID targetID = targetIDNumber.intValue;
+ for (const auto &entry : remoteEvent.targetChanges) {
+ FSTTargetID targetID = entry.first;
+ FSTBoxedTargetID *boxedTargetID = @(targetID);
+ FSTTargetChange *change = entry.second;
// Do not ref/unref unassigned targetIDs - it may lead to leaks.
- FSTQueryData *queryData = self.targetIDs[targetIDNumber];
+ FSTQueryData *queryData = self.targetIDs[boxedTargetID];
if (!queryData) {
- return;
+ continue;
}
+ [queryCache removeMatchingKeys:change.removedDocuments forTargetID:targetID];
+ [queryCache addMatchingKeys:change.addedDocuments forTargetID:targetID];
+
// Update the resume token if the change includes one. Don't clear any preexisting value.
// Bump the sequence number as well, so that documents being removed now are ordered later
// than documents that were previously removed from this target.
NSData *resumeToken = change.resumeToken;
if (resumeToken.length > 0) {
- queryData = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion
+ queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshotVersion
resumeToken:resumeToken
sequenceNumber:sequenceNumber];
- self.targetIDs[targetIDNumber] = queryData;
+ self.targetIDs[boxedTargetID] = queryData;
[self.queryCache updateQueryData:queryData];
}
-
- FSTTargetMapping *mapping = change.mapping;
- if (mapping) {
- // First make sure that all references are deleted.
- if ([mapping isKindOfClass:[FSTResetMapping class]]) {
- FSTResetMapping *reset = (FSTResetMapping *)mapping;
- [queryCache removeMatchingKeysForTargetID:targetID];
- [queryCache addMatchingKeys:reset.documents forTargetID:targetID];
-
- } else if ([mapping isKindOfClass:[FSTUpdateMapping class]]) {
- FSTUpdateMapping *update = (FSTUpdateMapping *)mapping;
- [queryCache removeMatchingKeys:update.removedDocuments forTargetID:targetID];
- [queryCache addMatchingKeys:update.addedDocuments forTargetID:targetID];
-
- } else {
- HARD_FAIL("Unknown mapping type: %s", mapping);
- }
- }
- }];
+ }
// TODO(klimt): This could probably be an NSMutableDictionary.
DocumentKeySet changedDocKeys;
diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h
index c84e34d..9ea0f9c 100644
--- a/Firestore/Source/Remote/FSTRemoteEvent.h
+++ b/Firestore/Source/Remote/FSTRemoteEvent.h
@@ -17,6 +17,9 @@
#import <Foundation/Foundation.h>
#include <map>
+#include <set>
+#include <unordered_map>
+#include <unordered_set>
#import "Firestore/Source/Core/FSTTypes.h"
#import "Firestore/Source/Model/FSTDocumentDictionary.h"
@@ -30,128 +33,82 @@
@class FSTMaybeDocument;
@class FSTWatchChange;
@class FSTQueryData;
+@class FSTDocumentWatchChange;
+@class FSTWatchTargetChange;
+@class FSTExistenceFilterWatchChange;
NS_ASSUME_NONNULL_BEGIN
-#pragma mark - FSTTargetMapping
-
-/**
- * TargetMapping represents a change to the documents in a query from the server. This can either
- * be an incremental Update or a full Reset.
- *
- * <p>This is an empty abstract class so that all the different kinds of changes can have a common
- * base class.
- */
-@interface FSTTargetMapping : NSObject
-
-/**
- * Strips out mapping changes that aren't actually changes. That is, if the document already
- * existed in the target, and is being added in the target, and this is not a reset, we can
- * skip doing the work to associate the document with the target because it has already been done.
- */
-- (void)filterUpdatesUsingExistingKeys:
- (const firebase::firestore::model::DocumentKeySet &)existingKeys;
-
-@end
-
-#pragma mark - FSTResetMapping
-
-/** The new set of documents to replace the current documents for a target. */
-@interface FSTResetMapping : FSTTargetMapping
-
/**
- * Creates a new mapping with the keys for the given documents added. This is intended primarily
- * for testing.
+ * Interface implemented by RemoteStore to expose target metadata to the FSTWatchChangeAggregator.
*/
-+ (FSTResetMapping *)mappingWithDocuments:(NSArray<FSTDocument *> *)documents;
-
-/** The new set of documents for the target. */
-- (const firebase::firestore::model::DocumentKeySet &)documents;
-@end
-
-#pragma mark - FSTUpdateMapping
+@protocol FSTTargetMetadataProvider
/**
- * A target should update its set of documents with the given added/removed set of documents.
+ * Returns the set of remote document keys for the given target ID as of the last raised snapshot.
*/
-@interface FSTUpdateMapping : FSTTargetMapping
+- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetID;
/**
- * Creates a new mapping with the keys for the given documents added. This is intended primarily
- * for testing.
+ * Returns the FSTQueryData for an active target ID or 'null' if this query has become inactive
*/
-+ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added
- removedDocuments:(NSArray<FSTDocument *> *)removed;
+- (nullable FSTQueryData *)queryDataForTarget:(FSTBoxedTargetID *)targetID;
-- (firebase::firestore::model::DocumentKeySet)applyTo:
- (const firebase::firestore::model::DocumentKeySet &)keys;
-
-/** The documents added to the target. */
-- (const firebase::firestore::model::DocumentKeySet &)addedDocuments;
-/** The documents removed from the target. */
-- (const firebase::firestore::model::DocumentKeySet &)removedDocuments;
@end
#pragma mark - FSTTargetChange
/**
- * Represents an update to the current status of a target, either explicitly having no new state, or
- * the new value to set. Note "current" has special meaning in the RPC protocol that implies that a
- * target is both up-to-date and consistent with the rest of the watch stream.
- */
-typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) {
- /** The current status is not affected and should not be modified */
- FSTCurrentStatusUpdateNone,
- /** The target must be marked as no longer "current" */
- FSTCurrentStatusUpdateMarkNotCurrent,
- /** The target must be marked as "current" */
- FSTCurrentStatusUpdateMarkCurrent,
-};
-
-/**
- * A part of an FSTRemoteEvent specifying set of changes to a specific target. These changes track
- * what documents are currently included in the target as well as the current snapshot version and
- * resume token but the actual changes *to* documents are not part of the FSTTargetChange since
- * documents may be part of multiple targets.
+ * An FSTTargetChange specifies the set of changes for a specific target as part of an
+ * FSTRemoteEvent. These changes track which documents are added, modified or emoved, as well as the
+ * target's resume token and whether the target is marked CURRENT.
+ *
+ * The actual changes *to* documents are not part of the FSTTargetChange since documents may be part
+ * of multiple targets.
*/
@interface FSTTargetChange : NSObject
/**
* Creates a new target change with the given SnapshotVersion.
*/
-- (instancetype)initWithSnapshotVersion:
- (firebase::firestore::model::SnapshotVersion)snapshotVersion;
+- (instancetype)initWithResumeToken:(NSData *)resumeToken
+ current:(BOOL)current
+ addedDocuments:(firebase::firestore::model::DocumentKeySet)addedDocuments
+ modifiedDocuments:(firebase::firestore::model::DocumentKeySet)modifiedDocuments
+ removedDocuments:(firebase::firestore::model::DocumentKeySet)removedDocuments
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
/**
- * Creates a new target change with the given documents. Instances of FSTDocument are considered
- * added. Instance of FSTDeletedDocument are considered removed. This is intended primarily for
- * testing.
+ * An opaque, server-assigned token that allows watching a query to be resumed after
+ * disconnecting without retransmitting all the data that matches the query. The resume token
+ * essentially identifies a point in time from which the server should resume sending results.
*/
-+ (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs
- currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate;
+@property(nonatomic, strong, readonly) NSData *resumeToken;
/**
- * The snapshot version representing the last state at which this target received a consistent
- * snapshot from the backend.
+ * The "current" (synced) status of this target. Note that "current" has special meaning in the RPC
+ * protocol that implies that a target is both up-to-date and consistent with the rest of the watch
+ * stream.
*/
-- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion;
+@property(nonatomic, assign, readonly) BOOL current;
/**
- * The new "current" (synced) status of this target. Set to CurrentStatusUpdateNone if the status
- * should not be updated. Note "current" has special meaning for in the RPC protocol that implies
- * that a target is both up-to-date and consistent with the rest of the watch stream.
+ * The set of documents that were newly assigned to this target as part of this remote event.
*/
-@property(nonatomic, assign, readonly) FSTCurrentStatusUpdate currentStatusUpdate;
+- (const firebase::firestore::model::DocumentKeySet &)addedDocuments;
-/** A set of changes to documents in this target. */
-@property(nonatomic, strong, readonly) FSTTargetMapping *mapping;
+/**
+ * The set of documents that were already assigned to this target but received an update during this
+ * remote event.
+ */
+- (const firebase::firestore::model::DocumentKeySet &)modifiedDocuments;
/**
- * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting
- * without retransmitting all the data that matches the query. The resume token essentially
- * identifies a point in time from which the server should resume sending results.
+ * The set of documents that were removed from this target as part of this remote event.
*/
-@property(nonatomic, strong, readonly) NSData *resumeToken;
+- (const firebase::firestore::model::DocumentKeySet &)removedDocuments;
@end
@@ -165,34 +122,38 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) {
- (instancetype)
initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion
- targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges
+ targetChanges:(std::unordered_map<FSTTargetID, FSTTargetChange *>)targetChanges
+ targetMismatches:(std::unordered_set<FSTTargetID>)targetMismatches
documentUpdates:
- (std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *>)documentUpdates
+ (std::unordered_map<firebase::firestore::model::DocumentKey,
+ FSTMaybeDocument *,
+ firebase::firestore::model::DocumentKeyHash>)documentUpdates
limboDocuments:(firebase::firestore::model::DocumentKeySet)limboDocuments;
/** The snapshot version this event brings us up to. */
- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion;
-/** A map from target to changes to the target. See TargetChange. */
-@property(nonatomic, strong, readonly)
- NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges;
-
/**
- * A set of which documents have changed or been deleted, along with the doc's new values
- * (if not deleted).
+ * A set of which document updates are due only to limbo resolution targets.
*/
-- (const std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *> &)documentUpdates;
-
- (const firebase::firestore::model::DocumentKeySet &)limboDocumentChanges;
-/** Adds a document update to this remote event */
-- (void)addDocumentUpdate:(FSTMaybeDocument *)document;
+/** A map from target to changes to the target. See TargetChange. */
+- (const std::unordered_map<FSTTargetID, FSTTargetChange *> &)targetChanges;
-/** Handles an existence filter mismatch */
-- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID;
+/**
+ * A set of targets that is known to be inconsistent. Listens for these targets should be
+ * re-established without resume tokens.
+ */
+- (const std::unordered_set<FSTTargetID> &)targetMismatches;
-- (void)synthesizeDeleteForLimboTargetChange:(FSTTargetChange *)targetChange
- key:(const firebase::firestore::model::DocumentKey &)key;
+/**
+ * A set of which documents have changed or been deleted, along with the doc's new values (if not
+ * deleted).
+ */
+- (const std::unordered_map<firebase::firestore::model::DocumentKey,
+ FSTMaybeDocument *,
+ firebase::firestore::model::DocumentKeyHash> &)documentUpdates;
@end
@@ -204,33 +165,35 @@ initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVer
*/
@interface FSTWatchChangeAggregator : NSObject
-- (instancetype)
-initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion
- listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets
- pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses
+- (instancetype)initWithTargetMetadataProvider:(id<FSTTargetMetadataProvider>)targetMetadataProvider
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
-/** The number of pending responses that are being waited on from watch */
-@property(nonatomic, strong, readonly)
- NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingTargetResponses;
+/** Processes and adds the FSTDocumentWatchChange to the current set of changes. */
+- (void)handleDocumentChange:(FSTDocumentWatchChange *)documentChange;
-/** Aggregates a watch change into the current state */
-- (void)addWatchChange:(FSTWatchChange *)watchChange;
+/** Processes and adds the WatchTargetChange to the current set of changes. */
+- (void)handleTargetChange:(FSTWatchTargetChange *)targetChange;
-/** Aggregates all provided watch changes to the current state in order */
-- (void)addWatchChanges:(NSArray<FSTWatchChange *> *)watchChanges;
+/**
+ * Handles existence filters and synthesizes deletes for filter mismatches. Targets that are
+ * invalidated by filter mismatches are added to `targetMismatches`.
+ */
+- (void)handleExistenceFilter:(FSTExistenceFilterWatchChange *)existenceFilter;
+
+/**
+ * Increment the number of acks needed from watch before we can consider the server to be 'in-sync'
+ * with the client's active targets.
+ */
+- (void)recordTargetRequest:(FSTBoxedTargetID *)targetID;
/**
* Converts the current state into a remote event with the snapshot version taken from the
* initializer.
*/
-- (FSTRemoteEvent *)remoteEvent;
-
-/** The existence filters - if any - for the given target IDs. */
-@property(nonatomic, strong, readonly)
- NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *existenceFilters;
+- (FSTRemoteEvent *)remoteEventAtSnapshotVersion:
+ (const firebase::firestore::model::SnapshotVersion &)snapshotVersion;
@end
diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm
index 67af650..5e00310 100644
--- a/Firestore/Source/Remote/FSTRemoteEvent.mm
+++ b/Firestore/Source/Remote/FSTRemoteEvent.mm
@@ -17,10 +17,16 @@
#import "Firestore/Source/Remote/FSTRemoteEvent.h"
#include <map>
+#include <set>
+#include <unordered_map>
#include <utility>
+#import "Firestore/Source/Core/FSTQuery.h"
+#import "Firestore/Source/Core/FSTViewSnapshot.h"
#import "Firestore/Source/Local/FSTQueryData.h"
#import "Firestore/Source/Model/FSTDocument.h"
+#import "Firestore/Source/Remote/FSTExistenceFilter.h"
+#import "Firestore/Source/Remote/FSTRemoteStore.h"
#import "Firestore/Source/Remote/FSTWatchChange.h"
#import "Firestore/Source/Util/FSTClasses.h"
@@ -30,619 +36,578 @@
#include "Firestore/core/src/firebase/firestore/util/log.h"
using firebase::firestore::model::DocumentKey;
+using firebase::firestore::model::DocumentKeyHash;
+using firebase::firestore::model::DocumentKeySet;
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
-}
+#pragma mark - FSTTargetChange
-- (void)removeDocumentKey:(const DocumentKey &)documentKey {
- @throw FSTAbstractMethodException(); // NOLINT
+@implementation FSTTargetChange {
+ DocumentKeySet _addedDocuments;
+ DocumentKeySet _modifiedDocuments;
+ DocumentKeySet _removedDocuments;
}
-- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys {
- @throw FSTAbstractMethodException(); // NOLINT
+- (instancetype)initWithResumeToken:(NSData *)resumeToken
+ current:(BOOL)current
+ addedDocuments:(DocumentKeySet)addedDocuments
+ modifiedDocuments:(DocumentKeySet)modifiedDocuments
+ removedDocuments:(DocumentKeySet)removedDocuments {
+ if (self = [super init]) {
+ _resumeToken = [resumeToken copy];
+ _current = current;
+ _addedDocuments = std::move(addedDocuments);
+ _modifiedDocuments = std::move(modifiedDocuments);
+ _removedDocuments = std::move(removedDocuments);
+ }
+ return self;
}
-@end
-
-#pragma mark - FSTResetMapping
-
-@implementation FSTResetMapping {
- DocumentKeySet _documents;
+- (const DocumentKeySet &)addedDocuments {
+ return _addedDocuments;
}
-+ (instancetype)mappingWithDocuments:(NSArray<FSTDocument *> *)documents {
- DocumentKeySet keys;
- for (FSTDocument *doc in documents) {
- keys = keys.insert(doc.key);
- }
- return [[FSTResetMapping alloc] initWithDocuments:std::move(keys)];
+- (const DocumentKeySet &)modifiedDocuments {
+ return _modifiedDocuments;
}
-- (instancetype)initWithDocuments:(DocumentKeySet)documents {
- self = [super init];
- if (self) {
- _documents = std::move(documents);
- }
- return self;
-}
-
-- (const DocumentKeySet &)documents {
- return _documents;
+- (const DocumentKeySet &)removedDocuments {
+ return _removedDocuments;
}
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
- if (![other isMemberOfClass:[FSTResetMapping class]]) {
+ if (![other isMemberOfClass:[FSTTargetChange class]]) {
return NO;
}
- FSTResetMapping *otherMapping = (FSTResetMapping *)other;
- return _documents == otherMapping.documents;
+ return [self current] == [other current] &&
+ [[self resumeToken] isEqualToData:[other resumeToken]] &&
+ [self addedDocuments] == [other addedDocuments] &&
+ [self modifiedDocuments] == [other modifiedDocuments] &&
+ [self removedDocuments] == [other removedDocuments];
}
-- (NSUInteger)hash {
- return Hash(_documents);
-}
+@end
-- (void)addDocumentKey:(const DocumentKey &)documentKey {
- _documents = _documents.insert(documentKey);
-}
+#pragma mark - FSTTargetState
-- (void)removeDocumentKey:(const DocumentKey &)documentKey {
- _documents = _documents.erase(documentKey);
-}
+/** Tracks the internal state of a Watch target. */
+@interface FSTTargetState : NSObject
-- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys {
- // No-op. Resets are not filtered.
-}
+/**
+ * Whether this target has been marked 'current'.
+ *
+ * 'Current' has special meaning in the RPC protocol: It implies that the Watch backend has sent us
+ * all changes up to the point at which the target was added and that the target is consistent with
+ * the rest of the watch stream.
+ */
+@property(nonatomic) BOOL current;
-@end
+/** The last resume token sent to us for this target. */
+@property(nonatomic, readonly, strong) NSData *resumeToken;
-#pragma mark - FSTUpdateMapping
+/** Whether we have modified any state that should trigger a snapshot. */
+@property(nonatomic, readonly) BOOL hasPendingChanges;
-@implementation FSTUpdateMapping {
- DocumentKeySet _addedDocuments;
- DocumentKeySet _removedDocuments;
-}
+/** Whether this target has pending target adds or target removes. */
+- (BOOL)isPending;
-+ (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)];
-}
+/**
+ * Applies the resume token to the TargetChange, but only when it has a new value. Empty
+ * resumeTokens are discarded.
+ */
+- (void)updateResumeToken:(NSData *)resumeToken;
-- (instancetype)initWithAddedDocuments:(DocumentKeySet)addedDocuments
- removedDocuments:(DocumentKeySet)removedDocuments {
- self = [super init];
- if (self) {
- _addedDocuments = std::move(addedDocuments);
- _removedDocuments = std::move(removedDocuments);
- }
- return self;
-}
+/** Resets the document changes and sets `hasPendingChanges` to false. */
+- (void)clearPendingChanges;
+/**
+ * Creates a target change from the current set of changes.
+ *
+ * To reset the document changes after raising this snapshot, call `clearPendingChanges()`.
+ */
+- (FSTTargetChange *)toTargetChange;
-- (const DocumentKeySet &)addedDocuments {
- return _addedDocuments;
-}
+- (void)recordTargetRequest;
+- (void)recordTargetResponse;
+- (void)markCurrent;
+- (void)addDocumentChangeWithType:(FSTDocumentViewChangeType)type
+ forKey:(const DocumentKey &)documentKey;
+- (void)removeDocumentChangeForKey:(const DocumentKey &)documentKey;
-- (const DocumentKeySet &)removedDocuments {
- return _removedDocuments;
-}
+@end
-- (BOOL)isEqual:(id)other {
- if (other == self) {
- return YES;
- }
- if (![other isMemberOfClass:[FSTUpdateMapping class]]) {
- return NO;
- }
+@implementation FSTTargetState {
+ /**
+ * The number of outstanding responses (adds or removes) that we are waiting on. We only consider
+ * targets active that have no outstanding responses.
+ */
+ int _outstandingResponses;
- FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other;
- return _addedDocuments == otherMapping.addedDocuments &&
- _removedDocuments == otherMapping.removedDocuments;
+ /**
+ * Keeps track of the document changes since the last raised snapshot.
+ *
+ * These changes are continuously updated as we receive document updates and always reflect the
+ * current set of changes against the last issued snapshot.
+ */
+ std::unordered_map<DocumentKey, FSTDocumentViewChangeType, DocumentKeyHash> _documentChanges;
}
-- (NSUInteger)hash {
- return Hash(_addedDocuments, _removedDocuments);
-}
+- (instancetype)init {
+ if (self = [super init]) {
+ _resumeToken = [NSData data];
+ _outstandingResponses = 0;
-- (DocumentKeySet)applyTo:(const DocumentKeySet &)keys {
- DocumentKeySet result = keys;
- for (const DocumentKey &key : _addedDocuments) {
- result = result.insert(key);
+ // We initialize to 'true' so that newly-added targets are included in the next RemoteEvent.
+ _hasPendingChanges = YES;
}
- for (const DocumentKey &key : _removedDocuments) {
- result = result.erase(key);
- }
- return result;
-}
-
-- (void)addDocumentKey:(const DocumentKey &)documentKey {
- _addedDocuments = _addedDocuments.insert(documentKey);
- _removedDocuments = _removedDocuments.erase(documentKey);
+ return self;
}
-- (void)removeDocumentKey:(const DocumentKey &)documentKey {
- _addedDocuments = _addedDocuments.erase(documentKey);
- _removedDocuments = _removedDocuments.insert(documentKey);
+- (BOOL)isPending {
+ return _outstandingResponses != 0;
}
-- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys {
- DocumentKeySet result = _addedDocuments;
- for (const DocumentKey &key : _addedDocuments) {
- if (existingKeys.contains(key)) {
- result = result.erase(key);
- }
+- (void)updateResumeToken:(NSData *)resumeToken {
+ if (resumeToken.length > 0) {
+ _hasPendingChanges = YES;
+ _resumeToken = [resumeToken copy];
}
- _addedDocuments = result;
}
-@end
-
-#pragma mark - FSTTargetChange
+- (void)clearPendingChanges {
+ _hasPendingChanges = NO;
+ _documentChanges.clear();
+}
-@interface FSTTargetChange ()
-@property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate;
-@property(nonatomic, strong, nullable) FSTTargetMapping *mapping;
-@property(nonatomic, strong) NSData *resumeToken;
-@end
+- (void)recordTargetRequest {
+ _outstandingResponses += 1;
+}
-@implementation FSTTargetChange {
- SnapshotVersion _snapshotVersion;
+- (void)recordTargetResponse {
+ _outstandingResponses -= 1;
}
-- (instancetype)init {
- if (self = [super init]) {
- _currentStatusUpdate = FSTCurrentStatusUpdateNone;
- _resumeToken = [NSData data];
- }
- return self;
+- (void)markCurrent {
+ _hasPendingChanges = YES;
+ _current = true;
}
-- (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion {
- if (self = [self init]) {
- _snapshotVersion = std::move(snapshotVersion);
- }
- return self;
+- (void)addDocumentChangeWithType:(FSTDocumentViewChangeType)type
+ forKey:(const DocumentKey &)documentKey {
+ _hasPendingChanges = YES;
+ _documentChanges[documentKey] = type;
}
-- (const SnapshotVersion &)snapshotVersion {
- return _snapshotVersion;
+- (void)removeDocumentChangeForKey:(const DocumentKey &)documentKey {
+ _hasPendingChanges = YES;
+ _documentChanges.erase(documentKey);
}
-+ (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs
- currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate {
+- (FSTTargetChange *)toTargetChange {
DocumentKeySet addedDocuments;
+ DocumentKeySet modifiedDocuments;
DocumentKeySet removedDocuments;
- for (FSTMaybeDocument *doc in docs) {
- if ([doc isKindOfClass:[FSTDeletedDocument class]]) {
- removedDocuments = removedDocuments.insert(doc.key);
- } else {
- addedDocuments = addedDocuments.insert(doc.key);
+
+ for (const auto &entry : _documentChanges) {
+ switch (entry.second) {
+ case FSTDocumentViewChangeTypeAdded:
+ addedDocuments = addedDocuments.insert(entry.first);
+ break;
+ case FSTDocumentViewChangeTypeModified:
+ modifiedDocuments = modifiedDocuments.insert(entry.first);
+ break;
+ case FSTDocumentViewChangeTypeRemoved:
+ removedDocuments = removedDocuments.insert(entry.first);
+ break;
+ default:
+ HARD_FAIL("Encountered invalid change type: %s", entry.second);
}
}
- 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;
- }
+ return [[FSTTargetChange alloc] initWithResumeToken:_resumeToken
+ current:_current
+ addedDocuments:std::move(addedDocuments)
+ modifiedDocuments:std::move(modifiedDocuments)
+ removedDocuments:std::move(removedDocuments)];
}
-
@end
#pragma mark - FSTRemoteEvent
@implementation FSTRemoteEvent {
SnapshotVersion _snapshotVersion;
- NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges;
- std::map<DocumentKey, FSTMaybeDocument *> _documentUpdates;
+ std::unordered_map<FSTTargetID, FSTTargetChange *> _targetChanges;
+ std::unordered_set<FSTTargetID> _targetMismatches;
+ std::unordered_map<DocumentKey, FSTMaybeDocument *, DocumentKeyHash> _documentUpdates;
DocumentKeySet _limboDocumentChanges;
}
-- (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion
- targetChanges:
- (NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges
- documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates
- limboDocuments:(DocumentKeySet)limboDocuments {
+- (instancetype)
+initWithSnapshotVersion:(SnapshotVersion)snapshotVersion
+ targetChanges:(std::unordered_map<FSTTargetID, FSTTargetChange *>)targetChanges
+ targetMismatches:(std::unordered_set<FSTTargetID>)targetMismatches
+ documentUpdates:
+ (std::unordered_map<DocumentKey, FSTMaybeDocument *, DocumentKeyHash>)documentUpdates
+ limboDocuments:(DocumentKeySet)limboDocuments {
self = [super init];
if (self) {
_snapshotVersion = std::move(snapshotVersion);
- _targetChanges = targetChanges;
+ _targetChanges = std::move(targetChanges);
+ _targetMismatches = std::move(targetMismatches);
_documentUpdates = std::move(documentUpdates);
_limboDocumentChanges = std::move(limboDocuments);
}
return self;
}
-- (NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges {
- return _targetChanges;
+- (const SnapshotVersion &)snapshotVersion {
+ return _snapshotVersion;
}
- (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);
- }
+- (const std::unordered_map<FSTTargetID, FSTTargetChange *> &)targetChanges {
+ return _targetChanges;
}
-/** Adds a document update to this remote event */
-- (void)addDocumentUpdate:(FSTMaybeDocument *)document {
- _documentUpdates[document.key] = document;
+- (const std::unordered_map<DocumentKey, FSTMaybeDocument *, DocumentKeyHash> &)documentUpdates {
+ return _documentUpdates;
}
-/** 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;
+- (const std::unordered_set<FSTTargetID> &)targetMismatches {
+ return _targetMismatches;
}
@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;
+@implementation FSTWatchChangeAggregator {
+ /** The internal state of all tracked targets. */
+ std::unordered_map<FSTTargetID, FSTTargetState *> _targetStates;
-/** Whether this aggregator was frozen and can no longer be modified */
-@property(nonatomic, assign) BOOL frozen;
+ /** Keeps track of document to update */
+ std::unordered_map<DocumentKey, FSTMaybeDocument *, DocumentKeyHash> _pendingDocumentUpdates;
-@end
+ /** A mapping of document keys to their set of target IDs. */
+ std::unordered_map<DocumentKey, std::set<FSTTargetID>, DocumentKeyHash>
+ _pendingDocumentTargetMappings;
-@implementation FSTWatchChangeAggregator {
- NSMutableDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *_existenceFilters;
- /** Keeps track of document to update */
- std::map<DocumentKey, FSTMaybeDocument *> _documentUpdates;
+ /**
+ * A list of targets with existence filter mismatches. These targets are known to be inconsistent
+ * and their listens needs to be re-established by RemoteStore.
+ */
+ std::unordered_set<FSTTargetID> _pendingTargetResets;
- DocumentKeySet _limboDocuments;
- /** The snapshot version for every target change this creates. */
- SnapshotVersion _snapshotVersion;
+ id<FSTTargetMetadataProvider> _targetMetadataProvider;
}
-- (instancetype)
-initWithSnapshotVersion:(SnapshotVersion)snapshotVersion
- listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets
- pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses {
+- (instancetype)initWithTargetMetadataProvider:
+ (id<FSTTargetMetadataProvider>)targetMetadataProvider {
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];
+ _targetMetadataProvider = targetMetadataProvider;
}
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;
+- (void)handleDocumentChange:(FSTDocumentWatchChange *)documentChange {
+ for (FSTBoxedTargetID *targetID in documentChange.updatedTargetIDs) {
+ if ([documentChange.document isKindOfClass:[FSTDocument class]]) {
+ [self addDocument:documentChange.document toTarget:targetID.intValue];
+ } else if ([documentChange.document isKindOfClass:[FSTDeletedDocument class]]) {
+ [self removeDocument:documentChange.document
+ withKey:documentChange.documentKey
+ fromTarget:targetID.intValue];
}
}
- // 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;
+ for (FSTBoxedTargetID *targetID in documentChange.removedTargetIDs) {
+ [self removeDocument:documentChange.document
+ withKey:documentChange.documentKey
+ fromTarget:targetID.intValue];
}
}
-- (void)addTargetChange:(FSTWatchTargetChange *)targetChange {
- for (FSTBoxedTargetID *targetID in targetChange.targetIDs) {
- FSTTargetChange *change = [self targetChangeForTargetID:targetID];
+- (void)handleTargetChange:(FSTWatchTargetChange *)targetChange {
+ for (FSTBoxedTargetID *boxedTargetID in targetChange.targetIDs) {
+ int targetID = boxedTargetID.intValue;
+ FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID];
switch (targetChange.state) {
case FSTWatchTargetChangeStateNoChange:
if ([self isActiveTarget:targetID]) {
- // Creating the change above satisfies the semantics of no-change.
- change.resumeToken = targetChange.resumeToken;
+ [targetState updateResumeToken: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];
+ // We need to decrement the number of pending acks needed from watch for this targetId.
+ [targetState recordTargetResponse];
+ if (!targetState.isPending) {
+ // 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.
+ [targetState clearPendingChanges];
}
- change.resumeToken = targetChange.resumeToken;
+ [targetState updateResumeToken: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.");
+ [targetState recordTargetResponse];
+ if (!targetState.isPending) {
+ [self removeTarget: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;
+ [targetState markCurrent];
+ [targetState updateResumeToken: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;
+ // Reset the target and synthesizes removes for all existing documents. The backend will
+ // re-add any documents that still match the target before it sends the next global
+ // snapshot.
+ [self resetTarget:targetID];
+ [targetState updateResumeToken:targetChange.resumeToken];
}
break;
default:
- LOG_WARN("Unknown target watch change type: %s", targetChange.state);
+ HARD_FAIL("Unknown target watch change state: %s", targetChange.state);
}
}
}
+- (void)removeTarget:(FSTTargetID)targetID {
+ _targetStates.erase(targetID);
+}
+
+- (void)handleExistenceFilter:(FSTExistenceFilterWatchChange *)existenceFilter {
+ FSTTargetID targetID = existenceFilter.targetID;
+ int expectedCount = existenceFilter.filter.count;
+
+ FSTQueryData *queryData = [self queryDataForActiveTarget:targetID];
+ if (queryData) {
+ FSTQuery *query = queryData.query;
+ if ([query isDocumentQuery]) {
+ if (expectedCount == 0) {
+ // The existence filter told us the document does not exist. We deduce that this document
+ // does not exist and apply a deleted document to our updates. Without applying this deleted
+ // document there might be another query that will raise this document as part of a snapshot
+ // until it is resolved, essentially exposing inconsistency between queries.
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:query.path];
+ [self
+ removeDocument:[FSTDeletedDocument documentWithKey:key version:SnapshotVersion::None()]
+ withKey:key
+ fromTarget:targetID];
+ } else {
+ HARD_ASSERT(expectedCount == 1, "Single document existence filter with count: %s",
+ expectedCount);
+ }
+ } else {
+ int currentSize = [self currentDocumentCountForTarget:targetID];
+ if (currentSize != expectedCount) {
+ // Existence filter mismatch: We reset the mapping and raise a new snapshot with
+ // `isFromCache:true`.
+ [self resetTarget:targetID];
+ _pendingTargetResets.insert(targetID);
+ }
+ }
+ }
+}
+
+- (int)currentDocumentCountForTarget:(FSTTargetID)targetID {
+ FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID];
+ FSTTargetChange *targetChange = [targetState toTargetChange];
+ return ([_targetMetadataProvider remoteKeysForTarget:@(targetID)].size() +
+ targetChange.addedDocuments.size() - targetChange.removedDocuments.size());
+}
+
/**
- * Records that we got a watch target add/remove by decrementing the number of pending target
- * responses that we have.
+ * Resets the state of a Watch target to its initial state (e.g. sets 'current' to false, clears the
+ * resume token and removes its target mapping from all documents).
*/
-- (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID {
- NSNumber *count = self.pendingTargetResponses[targetID];
- int newCount = count ? [count intValue] - 1 : -1;
- if (newCount == 0) {
- [self.pendingTargetResponses removeObjectForKey:targetID];
+- (void)resetTarget:(FSTTargetID)targetID {
+ auto currentTargetState = _targetStates.find(targetID);
+ HARD_ASSERT(currentTargetState != _targetStates.end() && !(currentTargetState->second.isPending),
+ "Should only reset active targets");
+
+ _targetStates[targetID] = [FSTTargetState new];
+
+ // Trigger removal for any documents currently mapped to this target. These removals will be part
+ // of the initial snapshot if Watch does not resend these documents.
+ DocumentKeySet existingKeys = [_targetMetadataProvider remoteKeysForTarget:@(targetID)];
+
+ for (FSTDocumentKey *key : existingKeys) {
+ [self removeDocument:nil withKey:key fromTarget:targetID];
+ }
+}
+
+/**
+ * Adds the provided document to the internal list of document updates and its document key to the
+ * given target's mapping.
+ */
+- (void)addDocument:(FSTMaybeDocument *)document toTarget:(FSTTargetID)targetID {
+ if (![self isActiveTarget:targetID]) {
+ return;
+ }
+
+ FSTDocumentViewChangeType changeType = [self containsDocument:document.key inTarget:targetID]
+ ? FSTDocumentViewChangeTypeModified
+ : FSTDocumentViewChangeTypeAdded;
+
+ FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID];
+ [targetState addDocumentChangeWithType:changeType forKey:document.key];
+
+ _pendingDocumentUpdates[document.key] = document;
+ _pendingDocumentTargetMappings[document.key].insert(targetID);
+}
+
+/**
+ * Removes the provided document from the target mapping. If the document no longer matches the
+ * target, but the document's state is still known (e.g. we know that the document was deleted or we
+ * received the change that caused the filter mismatch), the new document can be provided to update
+ * the remote document cache.
+ */
+- (void)removeDocument:(FSTMaybeDocument *_Nullable)document
+ withKey:(const DocumentKey &)key
+ fromTarget:(FSTTargetID)targetID {
+ if (![self isActiveTarget:targetID]) {
+ return;
+ }
+
+ FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID];
+
+ if ([self containsDocument:key inTarget:targetID]) {
+ [targetState addDocumentChangeWithType:FSTDocumentViewChangeTypeRemoved forKey:key];
} else {
- self.pendingTargetResponses[targetID] = @(newCount);
+ // The document may have entered and left the target before we raised a snapshot, so we can just
+ // ignore the change.
+ [targetState removeDocumentChangeForKey:key];
+ }
+
+ _pendingDocumentTargetMappings[key].erase(targetID);
+
+ if (document) {
+ _pendingDocumentUpdates[key] = document;
+ }
+}
+
+/**
+ * Returns whether the LocalStore considers the document to be part of the specified target.
+ */
+- (BOOL)containsDocument:(FSTDocumentKey *)key inTarget:(FSTTargetID)targetID {
+ const DocumentKeySet &existingKeys = [_targetMetadataProvider remoteKeysForTarget:@(targetID)];
+ return existingKeys.contains(key);
+}
+
+- (FSTTargetState *)ensureTargetStateForTarget:(FSTTargetID)targetID {
+ if (!_targetStates[targetID]) {
+ _targetStates[targetID] = [FSTTargetState new];
}
+
+ return _targetStates[targetID];
}
/**
- * Returns true if the given targetId is active. Active targets are those for which there are no
+ * Returns YES 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 {
+- (BOOL)isActiveTarget:(FSTTargetID)targetID {
return [self queryDataForActiveTarget:targetID] != nil;
}
-- (FSTQueryData *_Nullable)queryDataForActiveTarget:(FSTBoxedTargetID *)targetID {
- FSTQueryData *queryData = self.listenTargets[targetID];
- return (queryData && !self.pendingTargetResponses[targetID]) ? queryData : nil;
+- (nullable FSTQueryData *)queryDataForActiveTarget:(FSTTargetID)targetID {
+ auto targetState = _targetStates.find(targetID);
+ return targetState != _targetStates.end() && targetState->second.isPending
+ ? nil
+ : [_targetMetadataProvider queryDataForTarget:@(targetID)];
}
-- (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange {
- FSTBoxedTargetID *targetID = @(existenceFilterChange.targetID);
- if ([self isActiveTarget:targetID]) {
- _existenceFilters[targetID] = existenceFilterChange.filter;
+- (FSTRemoteEvent *)remoteEventAtSnapshotVersion:(const SnapshotVersion &)snapshotVersion {
+ std::unordered_map<FSTTargetID, FSTTargetChange *> targetChanges;
+
+ for (const auto &entry : _targetStates) {
+ FSTTargetID targetID = entry.first;
+ FSTTargetState *targetState = entry.second;
+
+ FSTQueryData *queryData = [self queryDataForActiveTarget:targetID];
+ if (queryData) {
+ if (targetState.current && [queryData.query isDocumentQuery]) {
+ // Document queries for document that don't exist can produce an empty result set. To update
+ // our local cache, we synthesize a document delete if we have not previously received the
+ // document. This resolves the limbo state of the document, removing it from
+ // limboDocumentRefs.
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:queryData.query.path];
+ if (_pendingDocumentUpdates.find(key) == _pendingDocumentUpdates.end() &&
+ ![self containsDocument:key inTarget:targetID]) {
+ [self removeDocument:[FSTDeletedDocument documentWithKey:key version:snapshotVersion]
+ withKey:key
+ fromTarget:targetID];
+ }
+ }
+
+ if (targetState.hasPendingChanges) {
+ targetChanges[targetID] = [targetState toTargetChange];
+ [targetState clearPendingChanges];
+ }
+ }
}
-}
-- (FSTRemoteEvent *)remoteEvent {
- NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges = self.targetChanges;
+ DocumentKeySet resolvedLimboDocuments;
- NSMutableArray *targetsToRemove = [NSMutableArray array];
+ // We extract the set of limbo-only document updates as the GC logic special-cases documents that
+ // do not appear in the query cache.
+ //
+ // TODO(gsoltis): Expand on this comment.
+ for (const auto &entry : _pendingDocumentTargetMappings) {
+ BOOL isOnlyLimboTarget = YES;
+
+ for (FSTTargetID targetID : entry.second) {
+ FSTQueryData *queryData = [self queryDataForActiveTarget:targetID];
+ if (queryData && queryData.purpose != FSTQueryPurposeLimboResolution) {
+ isOnlyLimboTarget = NO;
+ break;
+ }
+ }
- // Apply any inactive targets.
- for (FSTBoxedTargetID *targetID in [targetChanges keyEnumerator]) {
- if (![self isActiveTarget:targetID]) {
- [targetsToRemove addObject:targetID];
+ if (isOnlyLimboTarget) {
+ resolvedLimboDocuments = resolvedLimboDocuments.insert(entry.first);
}
}
- [targetChanges removeObjectsForKeys:targetsToRemove];
+ FSTRemoteEvent *remoteEvent =
+ [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion
+ targetChanges:targetChanges
+ targetMismatches:_pendingTargetResets
+ documentUpdates:_pendingDocumentUpdates
+ limboDocuments:resolvedLimboDocuments];
+
+ _pendingDocumentUpdates.clear();
+ _pendingDocumentTargetMappings.clear();
+ _pendingTargetResets.clear();
- // 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];
+ return remoteEvent;
}
+- (void)recordTargetRequest:(FSTBoxedTargetID *)targetID {
+ // For each request we get we need to record we need a response for it.
+ FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID.intValue];
+ [targetState recordTargetRequest];
+}
@end
NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h
index 9b01ce4..302d56a 100644
--- a/Firestore/Source/Remote/FSTRemoteStore.h
+++ b/Firestore/Source/Remote/FSTRemoteStore.h
@@ -17,6 +17,7 @@
#import <Foundation/Foundation.h>
#import "Firestore/Source/Core/FSTTypes.h"
+#import "Firestore/Source/Remote/FSTRemoteEvent.h"
#include "Firestore/core/src/firebase/firestore/auth/user.h"
@@ -24,7 +25,6 @@
@class FSTLocalStore;
@class FSTMutationBatch;
@class FSTMutationBatchResult;
-@class FSTQuery;
@class FSTQueryData;
@class FSTRemoteEvent;
@class FSTTransaction;
@@ -73,6 +73,12 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error;
+/**
+ * Returns the set of remote document keys for the given target ID. This list includes the
+ * documents that were assigned to the target when we received the last snapshot.
+ */
+- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetId;
+
@end
/**
@@ -93,7 +99,7 @@ NS_ASSUME_NONNULL_BEGIN
* FSTRemoteStore handles all interaction with the backend through a simple, clean interface. This
* class is not thread safe and should be only called from the worker dispatch queue.
*/
-@interface FSTRemoteStore : NSObject
+@interface FSTRemoteStore : NSObject <FSTTargetMetadataProvider>
- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore
datastore:(FSTDatastore *)datastore
diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm
index 7338ffb..54d00c4 100644
--- a/Firestore/Source/Remote/FSTRemoteStore.mm
+++ b/Firestore/Source/Remote/FSTRemoteStore.mm
@@ -96,14 +96,13 @@ static const int kMaxPendingWrites = 10;
* 0 we know that the client and server are in the same state (once this state
* is reached the targetId is removed from the map to free the memory).
*/
-@property(nonatomic, strong, readonly)
- NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingTargetResponses;
-@property(nonatomic, strong) NSMutableArray<FSTWatchChange *> *accumulatedChanges;
@property(nonatomic, assign) FSTBatchID lastBatchSeen;
@property(nonatomic, strong, readonly) FSTOnlineStateTracker *onlineStateTracker;
+@property(nonatomic, strong, nullable) FSTWatchChangeAggregator *watchChangeAggregator;
+
#pragma mark Write Stream
// The writeStream is null when the network is disabled. The non-null check is performed by
// isNetworkEnabled.
@@ -126,8 +125,6 @@ static const int kMaxPendingWrites = 10;
_localStore = localStore;
_datastore = datastore;
_listenTargets = [NSMutableDictionary dictionary];
- _pendingTargetResponses = [NSMutableDictionary dictionary];
- _accumulatedChanges = [NSMutableArray array];
_lastBatchSeen = kFSTBatchIDUnknown;
_pendingWrites = [NSMutableArray array];
@@ -229,6 +226,7 @@ static const int kMaxPendingWrites = 10;
- (void)startWatchStream {
HARD_ASSERT([self shouldStartWatchStream],
"startWatchStream: called when shouldStartWatchStream: is false.");
+ _watchChangeAggregator = [[FSTWatchChangeAggregator alloc] initWithTargetMetadataProvider:self];
[self.watchStream startWithDelegate:self];
[self.onlineStateTracker handleWatchStreamStart];
}
@@ -248,7 +246,7 @@ static const int kMaxPendingWrites = 10;
}
- (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData {
- [self recordPendingRequestForTargetID:@(queryData.targetID)];
+ [self.watchChangeAggregator recordTargetRequest:@(queryData.targetID)];
[self.watchStream watchQuery:queryData];
}
@@ -267,16 +265,10 @@ static const int kMaxPendingWrites = 10;
}
- (void)sendUnwatchRequestForTargetID:(FSTBoxedTargetID *)targetID {
- [self recordPendingRequestForTargetID:targetID];
+ [self.watchChangeAggregator recordTargetRequest:targetID];
[self.watchStream unwatchTargetID:[targetID intValue]];
}
-- (void)recordPendingRequestForTargetID:(FSTBoxedTargetID *)targetID {
- NSNumber *count = [self.pendingTargetResponses objectForKey:targetID];
- count = @([count intValue] + 1);
- [self.pendingTargetResponses setObject:count forKey:targetID];
-}
-
/**
* Returns YES if the network is enabled, the watch stream has not yet been started and there are
* active watch targets.
@@ -286,11 +278,7 @@ static const int kMaxPendingWrites = 10;
}
- (void)cleanUpWatchStreamState {
- // If the connection is closed then we'll never get a snapshot version for the accumulated
- // changes and so we'll never be able to complete the batch. When we start up again the server
- // is going to resend these changes anyway, so just toss the accumulated state.
- [self.accumulatedChanges removeAllObjects];
- [self.pendingTargetResponses removeAllObjects];
+ _watchChangeAggregator = nil;
}
- (void)watchStreamDidOpen {
@@ -305,28 +293,27 @@ static const int kMaxPendingWrites = 10;
// Mark the connection as Online because we got a message from the server.
[self.onlineStateTracker updateState:FSTOnlineStateOnline];
- FSTWatchTargetChange *watchTargetChange =
- [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil;
-
- if (watchTargetChange && watchTargetChange.state == FSTWatchTargetChangeStateRemoved &&
- watchTargetChange.cause) {
- // There was an error on a target, don't wait for a consistent snapshot to raise events
- [self processTargetErrorForWatchChange:(FSTWatchTargetChange *)change];
- } else {
- // Accumulate watch changes but don't process them if there's no snapshotVersion or it's
- // older than a previous snapshot we've processed (can happen after we resume a target
- // using a resume token).
- [self.accumulatedChanges addObject:change];
- if (snapshotVersion == SnapshotVersion::None() ||
- snapshotVersion < [self.localStore lastRemoteSnapshotVersion]) {
- return;
+ if ([change isKindOfClass:[FSTWatchTargetChange class]]) {
+ FSTWatchTargetChange *watchTargetChange = (FSTWatchTargetChange *)change;
+ if (watchTargetChange.state == FSTWatchTargetChangeStateRemoved && watchTargetChange.cause) {
+ // There was an error on a target, don't wait for a consistent snapshot to raise events
+ return [self processTargetErrorForWatchChange:watchTargetChange];
+ } else {
+ [self.watchChangeAggregator handleTargetChange:watchTargetChange];
}
+ } else if ([change isKindOfClass:[FSTDocumentWatchChange class]]) {
+ [self.watchChangeAggregator handleDocumentChange:(FSTDocumentWatchChange *)change];
+ } else {
+ HARD_ASSERT([change isKindOfClass:[FSTExistenceFilterWatchChange class]],
+ "Expected watchChange to be an instance of FSTExistenceFilterWatchChange");
+ [self.watchChangeAggregator handleExistenceFilter:(FSTExistenceFilterWatchChange *)change];
+ }
- // Create a batch, giving it the accumulatedChanges array.
- NSArray<FSTWatchChange *> *changes = self.accumulatedChanges;
- self.accumulatedChanges = [NSMutableArray array];
-
- [self processBatchedWatchChanges:changes snapshotVersion:snapshotVersion];
+ if (snapshotVersion != SnapshotVersion::None() &&
+ snapshotVersion >= [self.localStore lastRemoteSnapshotVersion]) {
+ // We have received a target change with a global snapshot if the snapshot version is not equal
+ // to SnapshotVersion.None().
+ [self raiseWatchSnapshotWithSnapshotVersion:snapshotVersion];
}
}
@@ -353,107 +340,60 @@ static const int kMaxPendingWrites = 10;
* Takes a batch of changes from the Datastore, repackages them as a RemoteEvent, and passes that
* on to the SyncEngine.
*/
-- (void)processBatchedWatchChanges:(NSArray<FSTWatchChange *> *)changes
- snapshotVersion:(const SnapshotVersion &)snapshotVersion {
- FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:snapshotVersion
- listenTargets:self.listenTargets
- pendingTargetResponses:self.pendingTargetResponses];
- [aggregator addWatchChanges:changes];
- FSTRemoteEvent *remoteEvent = [aggregator remoteEvent];
- [self.pendingTargetResponses removeAllObjects];
- [self.pendingTargetResponses setDictionary:aggregator.pendingTargetResponses];
-
- // Handle existence filters and existence filter mismatches
- [aggregator.existenceFilters enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *target,
- FSTExistenceFilter *filter,
- BOOL *stop) {
- FSTTargetID targetID = target.intValue;
-
- FSTQueryData *queryData = self.listenTargets[target];
- FSTQuery *query = queryData.query;
- if (!queryData) {
- // A watched target might have been removed already.
- return;
-
- } else if ([query isDocumentQuery]) {
- if (filter.count == 0) {
- // The existence filter told us the document does not exist.
- // We need to deduce that this document does not exist and apply a deleted document to our
- // updates. Without applying a deleted document there might be another query that will
- // raise this document as part of a snapshot until it is resolved, essentially exposing
- // inconsistency between queries
- const DocumentKey key{query.path};
- FSTDeletedDocument *deletedDoc =
- [FSTDeletedDocument documentWithKey:key version:snapshotVersion];
- [remoteEvent addDocumentUpdate:deletedDoc];
- } else {
- HARD_ASSERT(filter.count == 1, "Single document existence filter with count: %s",
- filter.count);
- }
-
- } else {
- // Not a document query.
- DocumentKeySet trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID];
- FSTTargetMapping *mapping = remoteEvent.targetChanges[target].mapping;
- if (mapping) {
- if ([mapping isKindOfClass:[FSTUpdateMapping class]]) {
- FSTUpdateMapping *update = (FSTUpdateMapping *)mapping;
- trackedRemote = [update applyTo:trackedRemote];
- } else {
- HARD_ASSERT([mapping isKindOfClass:[FSTResetMapping class]],
- "Expected either reset or update mapping but got something else %s", mapping);
- trackedRemote = ((FSTResetMapping *)mapping).documents;
- }
- }
+- (void)raiseWatchSnapshotWithSnapshotVersion:(const SnapshotVersion &)snapshotVersion {
+ HARD_ASSERT(snapshotVersion != SnapshotVersion::None(),
+ "Can't raise event for unknown SnapshotVersion");
- if (trackedRemote.size() != static_cast<size_t>(filter.count)) {
- LOG_DEBUG("Existence filter mismatch, resetting mapping");
-
- // Make sure the mismatch is exposed in the remote event
- [remoteEvent handleExistenceFilterMismatchForTargetID:target];
-
- // Clear the resume token for the query, since we're in a known mismatch state.
- queryData = [[FSTQueryData alloc] initWithQuery:query
- targetID:targetID
- listenSequenceNumber:queryData.sequenceNumber
- purpose:queryData.purpose];
- self.listenTargets[target] = queryData;
-
- // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't
- // send a resume token so that we get a full update.
- [self sendUnwatchRequestForTargetID:@(targetID)];
-
- // Mark the query we send as being on behalf of an existence filter mismatch, but don't
- // actually retain that in listenTargets. This ensures that we flag the first re-listen
- // this way without impacting future listens of this target (that might happen e.g. on
- // reconnect).
- FSTQueryData *requestQueryData =
- [[FSTQueryData alloc] initWithQuery:query
- targetID:targetID
- listenSequenceNumber:queryData.sequenceNumber
- purpose:FSTQueryPurposeExistenceFilterMismatch];
- [self sendWatchRequestWithQueryData:requestQueryData];
- }
- }
- }];
+ FSTRemoteEvent *remoteEvent =
+ [self.watchChangeAggregator remoteEventAtSnapshotVersion:snapshotVersion];
// Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when
// applying the completed FSTRemoteEvent.
- [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^(
- FSTBoxedTargetID *target, FSTTargetChange *change, BOOL *stop) {
- NSData *resumeToken = change.resumeToken;
+ for (const auto &entry : remoteEvent.targetChanges) {
+ NSData *resumeToken = entry.second.resumeToken;
if (resumeToken.length > 0) {
- FSTQueryData *queryData = self->_listenTargets[target];
+ FSTBoxedTargetID *targetID = @(entry.first);
+ FSTQueryData *queryData = _listenTargets[targetID];
// A watched target might have been removed already.
if (queryData) {
- self->_listenTargets[target] =
- [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion
+ _listenTargets[targetID] =
+ [queryData queryDataByReplacingSnapshotVersion:snapshotVersion
resumeToken:resumeToken
sequenceNumber:queryData.sequenceNumber];
}
}
- }];
+ }
+
+ // Re-establish listens for the targets that have been invalidated by existence filter mismatches.
+ for (FSTTargetID targetID : remoteEvent.targetMismatches) {
+ FSTQueryData *queryData = self.listenTargets[@(targetID)];
+
+ if (!queryData) {
+ // A watched target might have been removed already.
+ continue;
+ }
+
+ // Clear the resume token for the query, since we're in a known mismatch state.
+ queryData = [[FSTQueryData alloc] initWithQuery:queryData.query
+ targetID:targetID
+ listenSequenceNumber:queryData.sequenceNumber
+ purpose:queryData.purpose];
+ self.listenTargets[@(targetID)] = queryData;
+
+ // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't send a
+ // resume token so that we get a full update.
+ [self sendUnwatchRequestForTargetID:@(targetID)];
+
+ // Mark the query we send as being on behalf of an existence filter mismatch, but don't actually
+ // retain that in listenTargets. This ensures that we flag the first re-listen this way without
+ // impacting future listens of this target (that might happen e.g. on reconnect).
+ FSTQueryData *requestQueryData =
+ [[FSTQueryData alloc] initWithQuery:queryData.query
+ targetID:targetID
+ listenSequenceNumber:queryData.sequenceNumber
+ purpose:FSTQueryPurposeExistenceFilterMismatch];
+ [self sendWatchRequestWithQueryData:requestQueryData];
+ }
// Finally handle remote event
[self.syncEngine applyRemoteEvent:remoteEvent];
@@ -471,6 +411,14 @@ static const int kMaxPendingWrites = 10;
}
}
+- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetID {
+ return [self.syncEngine remoteKeysForTarget:targetID];
+}
+
+- (nullable FSTQueryData *)queryDataForTarget:(FSTBoxedTargetID *)targetID {
+ return self.listenTargets[targetID];
+}
+
#pragma mark Write Stream
/**
diff --git a/Firestore/Source/Remote/FSTWatchChange.mm b/Firestore/Source/Remote/FSTWatchChange.mm
index 284e980..04656b9 100644
--- a/Firestore/Source/Remote/FSTWatchChange.mm
+++ b/Firestore/Source/Remote/FSTWatchChange.mm
@@ -127,7 +127,7 @@ NS_ASSUME_NONNULL_BEGIN
if (self) {
_state = state;
_targetIDs = targetIDs;
- _resumeToken = resumeToken;
+ _resumeToken = [resumeToken copy];
_cause = cause;
}
return self;