@@ -31,6 +31,7 @@ import {
31
31
doc ,
32
32
DocumentChange ,
33
33
DocumentChangeType ,
34
+ DocumentData ,
34
35
documentId ,
35
36
enableNetwork ,
36
37
endAt ,
@@ -2195,6 +2196,149 @@ apiDescribe('Queries', persistence => {
2195
2196
} ) ;
2196
2197
} ) ;
2197
2198
} ) . timeout ( '90s' ) ;
2199
+
2200
+ // TODO(b/270731363): Re-enable this test once the Firestore emulator is fixed
2201
+ // to send an existence filter.
2202
+ // eslint-disable-next-line no-restricted-properties
2203
+ ( USE_EMULATOR ? it . skip : it ) (
2204
+ 'bloom filter should correctly encode complex Unicode characters' ,
2205
+ async ( ) => {
2206
+ // Firestore does not do any Unicode normalization on the document IDs.
2207
+ // Therefore, two document IDs that are canonically-equivalent (i.e. they
2208
+ // visually appear identical) but are represented by a different sequence
2209
+ // of Unicode code points are treated as distinct document IDs.
2210
+ const testDocIds = [
2211
+ 'DocumentToDelete' ,
2212
+ // The next two strings both end with "e" with an accent: the first uses
2213
+ // the dedicated Unicode code point for this character, while the second
2214
+ // uses the standard lowercase "e" followed by the accent combining
2215
+ // character.
2216
+ 'LowercaseEWithAcuteAccent_\u00E9' ,
2217
+ 'LowercaseEWithAcuteAccent_\u0065\u0301' ,
2218
+ // The next two strings both end with an "e" with two different accents
2219
+ // applied via the following two combining characters. The combining
2220
+ // characters are specified in a different order and Firestore treats
2221
+ // these document IDs as unique, despite the order of the combining
2222
+ // characters being irrelevant.
2223
+ 'LowercaseEWithMultipleAccents_\u0065\u0301\u0327' ,
2224
+ 'LowercaseEWithMultipleAccents_\u0065\u0327\u0301' ,
2225
+ // The next string contains a character outside the BMP (the "basic
2226
+ // multilingual plane"); that is, its code point is greater than 0xFFFF.
2227
+ // In UTF-16 (which JavaScript uses to store Unicode strings) this
2228
+ // requires a surrogate pair, two 16-bit code units, to represent this
2229
+ // character. Make sure that its presence is correctly tested in the
2230
+ // bloom filter, which uses UTF-8 encoding.
2231
+ 'Smiley_\u{1F600}'
2232
+ ] ;
2233
+
2234
+ // Verify assumptions about the equivalence of strings in `testDocIds`.
2235
+ expect ( testDocIds [ 1 ] . normalize ( ) ) . equals ( testDocIds [ 2 ] . normalize ( ) ) ;
2236
+ expect ( testDocIds [ 3 ] . normalize ( ) ) . equals ( testDocIds [ 4 ] . normalize ( ) ) ;
2237
+ expect ( testDocIds [ 5 ] ) . equals ( 'Smiley_\uD83D\uDE00' ) ;
2238
+
2239
+ // Create the mapping from document ID to document data for the document
2240
+ // IDs specified in `testDocIds`.
2241
+ const testDocs = testDocIds . reduce ( ( map , docId ) => {
2242
+ map [ docId ] = { foo : 42 } ;
2243
+ return map ;
2244
+ } , { } as { [ key : string ] : DocumentData } ) ;
2245
+
2246
+ // Ensure that the local cache is configured to use LRU garbage
2247
+ // collection (rather than eager garbage collection) so that the resume
2248
+ // token and document data does not get prematurely evicted.
2249
+ const lruPersistence = persistence . toLruGc ( ) ;
2250
+
2251
+ return withRetry ( async attemptNumber => {
2252
+ return withTestCollection (
2253
+ lruPersistence ,
2254
+ testDocs ,
2255
+ async ( coll , db ) => {
2256
+ // Run a query to populate the local cache with documents that have
2257
+ // names with complex Unicode characters.
2258
+ const snapshot1 = await getDocs ( coll ) ;
2259
+ const snapshot1DocumentIds = snapshot1 . docs . map (
2260
+ documentSnapshot => documentSnapshot . id
2261
+ ) ;
2262
+ expect (
2263
+ snapshot1DocumentIds ,
2264
+ 'snapshot1DocumentIds'
2265
+ ) . to . have . members ( testDocIds ) ;
2266
+
2267
+ // Delete one of the documents so that the next call to getDocs() will
2268
+ // experience an existence filter mismatch. Do this deletion in a
2269
+ // transaction, rather than using deleteDoc(), to avoid affecting the
2270
+ // local cache.
2271
+ await runTransaction ( db , async txn => {
2272
+ const snapshotOfDocumentToDelete = await txn . get (
2273
+ doc ( coll , 'DocumentToDelete' )
2274
+ ) ;
2275
+ expect (
2276
+ snapshotOfDocumentToDelete . exists ( ) ,
2277
+ 'snapshotOfDocumentToDelete.exists()'
2278
+ ) . to . be . true ;
2279
+ txn . delete ( snapshotOfDocumentToDelete . ref ) ;
2280
+ } ) ;
2281
+
2282
+ // Wait for 10 seconds, during which Watch will stop tracking the
2283
+ // query and will send an existence filter rather than "delete" events
2284
+ // when the query is resumed.
2285
+ await new Promise ( resolve => setTimeout ( resolve , 10000 ) ) ;
2286
+
2287
+ // Resume the query and save the resulting snapshot for verification.
2288
+ // Use some internal testing hooks to "capture" the existence filter
2289
+ // mismatches.
2290
+ const [ existenceFilterMismatches , snapshot2 ] =
2291
+ await captureExistenceFilterMismatches ( ( ) => getDocs ( coll ) ) ;
2292
+ const snapshot2DocumentIds = snapshot2 . docs . map (
2293
+ documentSnapshot => documentSnapshot . id
2294
+ ) ;
2295
+ const testDocIdsMinusDeletedDocId = testDocIds . filter (
2296
+ documentId => documentId !== 'DocumentToDelete'
2297
+ ) ;
2298
+ expect (
2299
+ snapshot2DocumentIds ,
2300
+ 'snapshot2DocumentIds'
2301
+ ) . to . have . members ( testDocIdsMinusDeletedDocId ) ;
2302
+
2303
+ // Verify that Watch sent an existence filter with the correct counts.
2304
+ expect (
2305
+ existenceFilterMismatches ,
2306
+ 'existenceFilterMismatches'
2307
+ ) . to . have . length ( 1 ) ;
2308
+ const { localCacheCount, existenceFilterCount, bloomFilter } =
2309
+ existenceFilterMismatches [ 0 ] ;
2310
+ expect ( localCacheCount , 'localCacheCount' ) . to . equal (
2311
+ testDocIds . length
2312
+ ) ;
2313
+ expect ( existenceFilterCount , 'existenceFilterCount' ) . to . equal (
2314
+ testDocIds . length - 1
2315
+ ) ;
2316
+
2317
+ // Verify that Watch sent a valid bloom filter.
2318
+ if ( ! bloomFilter ) {
2319
+ expect . fail (
2320
+ 'The existence filter should have specified a bloom filter ' +
2321
+ 'in its `unchanged_names` field.'
2322
+ ) ;
2323
+ throw new Error ( 'should never get here' ) ;
2324
+ }
2325
+
2326
+ // Verify that the bloom filter was successfully used to avert a full
2327
+ // requery. If a false positive occurred, which is statistically rare,
2328
+ // but technically possible, then retry the entire test.
2329
+ if ( attemptNumber === 1 && ! bloomFilter . applied ) {
2330
+ throw new RetryError ( ) ;
2331
+ }
2332
+
2333
+ expect (
2334
+ bloomFilter . applied ,
2335
+ `bloomFilter.applied with attemptNumber=${ attemptNumber } `
2336
+ ) . to . be . true ;
2337
+ }
2338
+ ) ;
2339
+ } ) ;
2340
+ }
2341
+ ) . timeout ( '90s' ) ;
2198
2342
} ) ;
2199
2343
2200
2344
function verifyDocumentChange < T > (
0 commit comments