@@ -32,6 +32,7 @@ import {
32
32
doc ,
33
33
DocumentChange ,
34
34
DocumentChangeType ,
35
+ DocumentData ,
35
36
documentId ,
36
37
enableNetwork ,
37
38
endAt ,
@@ -2197,6 +2198,122 @@ apiDescribe('Queries', persistence => {
2197
2198
} ) ;
2198
2199
} ) ;
2199
2200
} ) . timeout ( '90s' ) ;
2201
+
2202
+ // TODO(b/270731363): Re-enable this test once the Firestore emulator is fixed
2203
+ // to send an existence filter.
2204
+ // eslint-disable-next-line no-restricted-properties
2205
+ ( USE_EMULATOR ? it . skip : it . only ) (
2206
+ 'bloom filter should correctly encode special unicode characters' ,
2207
+ async ( ) => {
2208
+ const testDocIds = [
2209
+ 'DocumentToDelete' ,
2210
+ 'LowercaseEWithAcuteAccent_\u00E9' ,
2211
+ 'LowercaseEWithAcuteAccent_\u0065\u0301' ,
2212
+ 'LowercaseEWithMultipleAccents_\u0065\u0301\u0327' ,
2213
+ 'LowercaseEWithMultipleAccents_\u0065\u0327\u0301' ,
2214
+ 'Smiley_\u{1F600}'
2215
+ ] ;
2216
+ const testDocs = testDocIds . reduce ( ( map , docId ) => {
2217
+ map [ docId ] = { foo : 42 } ;
2218
+ return map ;
2219
+ } , { } as { [ key : string ] : DocumentData } ) ;
2220
+
2221
+ // Ensure that the local cache is configured to use LRU garbage
2222
+ // collection (rather than eager garbage collection) so that the resume
2223
+ // token and document data does not get prematurely evicted.
2224
+ const lruPersistence = persistence . toLruGc ( ) ;
2225
+
2226
+ return withRetry ( async attemptNumber => {
2227
+ return withTestCollection (
2228
+ lruPersistence ,
2229
+ testDocs ,
2230
+ async ( coll , db ) => {
2231
+ // Run a query to populate the local cache with documents that have
2232
+ // names with complex Unicode characters.
2233
+ const snapshot1 = await getDocs ( coll ) ;
2234
+ const snapshot1DocumentIds = snapshot1 . docs . map (
2235
+ documentSnapshot => documentSnapshot . id
2236
+ ) ;
2237
+ expect (
2238
+ snapshot1DocumentIds ,
2239
+ 'snapshot1DocumentIds'
2240
+ ) . to . have . members ( testDocIds ) ;
2241
+
2242
+ // Delete one of the documents so that the next call to getDocs() will
2243
+ // experience an existence filter mismatch. Do this deletion in a
2244
+ // transaction, rather than using deleteDoc(), to avoid affecting the
2245
+ // local cache.
2246
+ await runTransaction ( db , async txn => {
2247
+ const snapshotOfDocumentToDelete = await txn . get (
2248
+ doc ( coll , 'DocumentToDelete' )
2249
+ ) ;
2250
+ expect (
2251
+ snapshotOfDocumentToDelete . exists ( ) ,
2252
+ 'snapshotOfDocumentToDelete.exists()'
2253
+ ) . to . be . true ;
2254
+ txn . delete ( snapshotOfDocumentToDelete . ref ) ;
2255
+ } ) ;
2256
+
2257
+ // Wait for 10 seconds, during which Watch will stop tracking the
2258
+ // query and will send an existence filter rather than "delete" events
2259
+ // when the query is resumed.
2260
+ await new Promise ( resolve => setTimeout ( resolve , 10000 ) ) ;
2261
+
2262
+ // Resume the query and save the resulting snapshot for verification.
2263
+ // Use some internal testing hooks to "capture" the existence filter
2264
+ // mismatches.
2265
+ const [ existenceFilterMismatches , snapshot2 ] =
2266
+ await captureExistenceFilterMismatches ( ( ) => getDocs ( coll ) ) ;
2267
+ const snapshot2DocumentIds = snapshot2 . docs . map (
2268
+ documentSnapshot => documentSnapshot . id
2269
+ ) ;
2270
+ const testDocIdsMinusDeletedDocId = testDocIds . filter (
2271
+ documentId => documentId !== 'DocumentToDelete'
2272
+ ) ;
2273
+ expect (
2274
+ snapshot2DocumentIds ,
2275
+ 'snapshot2DocumentIds'
2276
+ ) . to . have . members ( testDocIdsMinusDeletedDocId ) ;
2277
+
2278
+ // Verify that Watch sent an existence filter with the correct counts.
2279
+ expect (
2280
+ existenceFilterMismatches ,
2281
+ 'existenceFilterMismatches'
2282
+ ) . to . have . length ( 1 ) ;
2283
+ const { localCacheCount, existenceFilterCount, bloomFilter } =
2284
+ existenceFilterMismatches [ 0 ] ;
2285
+ expect ( localCacheCount , 'localCacheCount' ) . to . equal (
2286
+ testDocIds . length
2287
+ ) ;
2288
+ expect ( existenceFilterCount , 'existenceFilterCount' ) . to . equal (
2289
+ testDocIds . length - 1
2290
+ ) ;
2291
+
2292
+ // Verify that Watch sent a valid bloom filter.
2293
+ if ( ! bloomFilter ) {
2294
+ expect . fail (
2295
+ 'The existence filter should have specified a bloom filter ' +
2296
+ 'in its `unchanged_names` field.'
2297
+ ) ;
2298
+ throw new Error ( 'should never get here' ) ;
2299
+ }
2300
+
2301
+ // Verify that the bloom filter was successfully used to avert a full
2302
+ // requery. If a false positive occurred, which is statistically rare,
2303
+ // but technically possible, then retry the entire test.
2304
+ if ( attemptNumber === 1 && ! bloomFilter . applied ) {
2305
+ throw new RetryError ( ) ;
2306
+ }
2307
+
2308
+ expect (
2309
+ bloomFilter . applied ,
2310
+ `bloomFilter.applied with attemptNumber=${ attemptNumber } `
2311
+ ) . to . be . true ;
2312
+ }
2313
+ ) ;
2314
+ } ) ;
2315
+ }
2316
+ ) . timeout ( '90s' ) ;
2200
2317
} ) ;
2201
2318
2202
2319
function verifyDocumentChange < T > (
0 commit comments