@@ -47,6 +47,7 @@ import {
4747 Query ,
4848 query ,
4949 QuerySnapshot ,
50+ runTransaction ,
5051 setDoc ,
5152 startAfter ,
5253 startAt ,
@@ -2059,6 +2060,62 @@ apiDescribe('Queries', (persistence: boolean) => {
20592060 } ) ;
20602061 } ) ;
20612062 } ) ;
2063+
2064+ it ( 'resuming a query should use existence filter to detect deletes' , async ( ) => {
2065+ // Prepare the names and contents of the 100 documents to create.
2066+ const testDocs : { [ key : string ] : object } = { } ;
2067+ for ( let i = 0 ; i < 100 ; i ++ ) {
2068+ testDocs [ 'doc' + ( 1000 + i ) ] = { key : 42 } ;
2069+ }
2070+
2071+ return withTestCollection ( persistence , testDocs , async ( coll , db ) => {
2072+ // Run a query to populate the local cache with the 100 documents and a
2073+ // resume token.
2074+ const snapshot1 = await getDocs ( coll ) ;
2075+ expect ( snapshot1 . size , 'snapshot1.size' ) . to . equal ( 100 ) ;
2076+ const createdDocuments = snapshot1 . docs . map ( snapshot => snapshot . ref ) ;
2077+
2078+ // Delete 50 of the 100 documents. Do this in a transaction, rather than
2079+ // deleteDoc(), to avoid affecting the local cache.
2080+ const deletedDocumentIds = new Set < string > ( ) ;
2081+ await runTransaction ( db , async txn => {
2082+ for ( let i = 0 ; i < createdDocuments . length ; i += 2 ) {
2083+ const documentToDelete = createdDocuments [ i ] ;
2084+ txn . delete ( documentToDelete ) ;
2085+ deletedDocumentIds . add ( documentToDelete . id ) ;
2086+ }
2087+ } ) ;
2088+
2089+ // Wait for 10 seconds, during which Watch will stop tracking the query
2090+ // and will send an existence filter rather than "delete" events when the
2091+ // query is resumed.
2092+ await new Promise ( resolve => setTimeout ( resolve , 10000 ) ) ;
2093+
2094+ // Resume the query and save the resulting snapshot for verification.
2095+ const snapshot2 = await getDocs ( coll ) ;
2096+
2097+ // Verify that the snapshot from the resumed query contains the expected
2098+ // documents; that is, that it contains the 50 documents that were _not_
2099+ // deleted.
2100+ // TODO(b/270731363): Remove the "if" condition below once the
2101+ // Firestore Emulator is fixed to send an existence filter. At the time of
2102+ // writing, the Firestore emulator fails to send an existence filter,
2103+ // resulting in the client including the deleted documents in the snapshot
2104+ // of the resumed query.
2105+ if ( ! ( USE_EMULATOR && snapshot2 . size === 100 ) ) {
2106+ const actualDocumentIds = snapshot2 . docs
2107+ . map ( documentSnapshot => documentSnapshot . ref . id )
2108+ . sort ( ) ;
2109+ const expectedDocumentIds = createdDocuments
2110+ . filter ( documentRef => ! deletedDocumentIds . has ( documentRef . id ) )
2111+ . map ( documentRef => documentRef . id )
2112+ . sort ( ) ;
2113+ expect ( actualDocumentIds , 'snapshot2.docs' ) . to . deep . equal (
2114+ expectedDocumentIds
2115+ ) ;
2116+ }
2117+ } ) ;
2118+ } ) . timeout ( '90s' ) ;
20622119} ) ;
20632120
20642121function verifyDocumentChange < T > (
0 commit comments