Skip to content

Commit c276dcc

Browse files
Clear the lastLimboFreeSnapshot version on existence filter mismatch
1 parent 4c042b2 commit c276dcc

File tree

2 files changed

+49
-21
lines changed

2 files changed

+49
-21
lines changed

packages/firestore/src/local/local_store_impl.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -501,24 +501,32 @@ export function localStoreApplyRemoteEventToLocalCache(
501501
})
502502
);
503503

504-
const resumeToken = change.resumeToken;
505-
// Update the resume token if the change includes one.
506-
if (resumeToken.approximateByteSize() > 0) {
507-
const newTargetData = oldTargetData
508-
.withResumeToken(resumeToken, remoteVersion)
509-
.withSequenceNumber(txn.currentSequenceNumber);
510-
newTargetDataByTargetMap = newTargetDataByTargetMap.insert(
511-
targetId,
512-
newTargetData
504+
let newTargetData = oldTargetData.withSequenceNumber(
505+
txn.currentSequenceNumber
506+
);
507+
if (remoteEvent.targetMismatches.has(targetId)) {
508+
newTargetData = newTargetData.withResumeToken(
509+
ByteString.EMPTY_BYTE_STRING,
510+
SnapshotVersion.min()
511+
);
512+
} else if (change.resumeToken.approximateByteSize() > 0) {
513+
newTargetData = newTargetData.withResumeToken(
514+
change.resumeToken,
515+
remoteVersion
513516
);
517+
}
514518

515-
// Update the target data if there are target changes (or if
516-
// sufficient time has passed since the last update).
517-
if (shouldPersistTargetData(oldTargetData, newTargetData, change)) {
518-
promises.push(
519-
localStoreImpl.targetCache.updateTargetData(txn, newTargetData)
520-
);
521-
}
519+
newTargetDataByTargetMap = newTargetDataByTargetMap.insert(
520+
targetId,
521+
newTargetData
522+
);
523+
524+
// Update the target data if there are target changes (or if
525+
// sufficient time has passed since the last update).
526+
if (shouldPersistTargetData(oldTargetData, newTargetData, change)) {
527+
promises.push(
528+
localStoreImpl.targetCache.updateTargetData(txn, newTargetData)
529+
);
522530
}
523531
});
524532

@@ -675,11 +683,6 @@ function shouldPersistTargetData(
675683
newTargetData: TargetData,
676684
change: TargetChange
677685
): boolean {
678-
hardAssert(
679-
newTargetData.resumeToken.approximateByteSize() > 0,
680-
'Attempted to persist target data with no resume token'
681-
);
682-
683686
// Always persist target data if we don't already have a resume token.
684687
if (oldTargetData.resumeToken.approximateByteSize() === 0) {
685688
return true;

packages/firestore/test/unit/specs/existence_filter_spec.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,29 @@ describeSpec('Existence Filters:', [], () => {
211211
})
212212
);
213213
});
214+
215+
specTest(
216+
'Existence filter mismatch invalidates index-free query',
217+
['durable-persistence'],
218+
() => {
219+
// This is a test for https://github.com/firebase/firebase-android-sdk/issues/3249
220+
// In this particular scenario, the user received an existence filter
221+
// mismatch, but the SDK only cleared the target-to-document mapping and
222+
// not the lastLimboFreeSnapshot version. This caused the SDK to resume
223+
// the query but not include old documents.
224+
const query1 = query('collection');
225+
const doc1 = doc('collection/1', 1000, { v: 1 });
226+
const doc2 = doc('collection/2', 1000, { v: 2 });
227+
return spec()
228+
.userListens(query1)
229+
.watchAcksFull(query1, 1000, doc1, doc2)
230+
.expectEvents(query1, { added: [doc1, doc2] })
231+
.watchFilters([query1], doc1.key) // doc2 was deleted
232+
.watchSnapshots(2000)
233+
.expectEvents(query1, { fromCache: true })
234+
.restart()
235+
.userListens(query1)
236+
.expectEvents(query1, { added: [doc1, doc2], fromCache: true });
237+
}
238+
);
214239
});

0 commit comments

Comments
 (0)