@@ -36,6 +36,7 @@ import { assert, fail } from '../util/assert';
36
36
import { FirestoreError } from '../util/error' ;
37
37
import * as log from '../util/log' ;
38
38
import { AnyJs , primitiveComparator } from '../util/misc' ;
39
+ import * as objUtils from '../util/obj' ;
39
40
import { ObjectMap } from '../util/obj_map' ;
40
41
import { Deferred } from '../util/promise' ;
41
42
import { SortedMap } from '../util/sorted_map' ;
@@ -92,6 +93,19 @@ class QueryView {
92
93
) { }
93
94
}
94
95
96
+ /** Tracks a limbo resolution. */
97
+ class LimboResolution {
98
+ constructor ( public key : DocumentKey ) { }
99
+
100
+ /**
101
+ * Set to true once we've received a document. This is used in
102
+ * targetContainsDocument() and ultimately used by WatchChangeAggregator to
103
+ * decide whether it needs to manufacture a delete event for the target once
104
+ * the target is CURRENT.
105
+ */
106
+ receivedDocument : boolean ;
107
+ }
108
+
95
109
/**
96
110
* SyncEngine is the central controller in the client SDK architecture. It is
97
111
* the glue code between the EventManager, LocalStore, and RemoteStore. Some of
@@ -117,7 +131,9 @@ export class SyncEngine implements RemoteSyncer {
117
131
private limboTargetsByKey = new SortedMap < DocumentKey , TargetId > (
118
132
DocumentKey . comparator
119
133
) ;
120
- private limboKeysByTarget : { [ targetId : number ] : DocumentKey } = { } ;
134
+ private limboResolutionsByTarget : {
135
+ [ targetId : number ] : LimboResolution ;
136
+ } = { } ;
121
137
private limboDocumentRefs = new ReferenceSet ( ) ;
122
138
private limboCollector = new EagerGarbageCollector ( ) ;
123
139
/** Stores user completion handlers, indexed by User and BatchId. */
@@ -301,6 +317,38 @@ export class SyncEngine implements RemoteSyncer {
301
317
applyRemoteEvent ( remoteEvent : RemoteEvent ) : Promise < void > {
302
318
this . assertSubscribed ( 'applyRemoteEvent()' ) ;
303
319
320
+ // Update `receivedDocument` as appropriate for any limbo targets.
321
+ objUtils . forEach ( remoteEvent . targetChanges , ( targetId , targetChange ) => {
322
+ const limboResolution = this . limboResolutionsByTarget [ targetId ] ;
323
+ if ( limboResolution ) {
324
+ // Since this is a limbo resolution lookup, it's for a single document
325
+ // and it could be added, modified, or removed, but not a combination.
326
+ assert (
327
+ targetChange . addedDocuments . size +
328
+ targetChange . modifiedDocuments . size +
329
+ targetChange . removedDocuments . size <=
330
+ 1 ,
331
+ 'Limbo resolution for single document contains multiple changes.'
332
+ ) ;
333
+ if ( targetChange . addedDocuments . size > 0 ) {
334
+ limboResolution . receivedDocument = true ;
335
+ } else if ( targetChange . modifiedDocuments . size > 0 ) {
336
+ assert (
337
+ limboResolution . receivedDocument ,
338
+ 'Received change for limbo target document without add.'
339
+ ) ;
340
+ } else if ( targetChange . removedDocuments . size > 0 ) {
341
+ assert (
342
+ limboResolution . receivedDocument ,
343
+ 'Received remove for limbo target document without add.'
344
+ ) ;
345
+ limboResolution . receivedDocument = false ;
346
+ } else {
347
+ // This was probably just a CURRENT targetChange or similar.
348
+ }
349
+ }
350
+ } ) ;
351
+
304
352
return this . localStore . applyRemoteEvent ( remoteEvent ) . then ( changes => {
305
353
return this . emitNewSnapsAndNotifyLocalStore ( changes , remoteEvent ) ;
306
354
} ) ;
@@ -327,12 +375,13 @@ export class SyncEngine implements RemoteSyncer {
327
375
328
376
rejectListen ( targetId : TargetId , err : FirestoreError ) : Promise < void > {
329
377
this . assertSubscribed ( 'rejectListens()' ) ;
330
- const limboKey = this . limboKeysByTarget [ targetId ] ;
378
+ const limboResolution = this . limboResolutionsByTarget [ targetId ] ;
379
+ const limboKey = limboResolution && limboResolution . key ;
331
380
if ( limboKey ) {
332
381
// Since this query failed, we won't want to manually unlisten to it.
333
382
// So go ahead and remove it from bookkeeping.
334
383
this . limboTargetsByKey = this . limboTargetsByKey . remove ( limboKey ) ;
335
- delete this . limboKeysByTarget [ targetId ] ;
384
+ delete this . limboResolutionsByTarget [ targetId ] ;
336
385
337
386
// TODO(klimt): We really only should do the following on permission
338
387
// denied errors, but we don't have the cause code here.
@@ -476,7 +525,7 @@ export class SyncEngine implements RemoteSyncer {
476
525
log . debug ( LOG_TAG , 'New document in limbo: ' + key ) ;
477
526
const limboTargetId = this . targetIdGenerator . next ( ) ;
478
527
const query = Query . atPath ( key . path ) ;
479
- this . limboKeysByTarget [ limboTargetId ] = key ;
528
+ this . limboResolutionsByTarget [ limboTargetId ] = new LimboResolution ( key ) ;
480
529
this . remoteStore . listen (
481
530
new QueryData ( query , limboTargetId , QueryPurpose . LimboResolution )
482
531
) ;
@@ -501,7 +550,7 @@ export class SyncEngine implements RemoteSyncer {
501
550
}
502
551
this . remoteStore . unlisten ( limboTargetId ) ;
503
552
this . limboTargetsByKey = this . limboTargetsByKey . remove ( key ) ;
504
- delete this . limboKeysByTarget [ limboTargetId ] ;
553
+ delete this . limboResolutionsByTarget [ limboTargetId ] ;
505
554
} ) ;
506
555
} )
507
556
. toPromise ( ) ;
@@ -588,8 +637,13 @@ export class SyncEngine implements RemoteSyncer {
588
637
}
589
638
590
639
getRemoteKeysForTarget ( targetId : TargetId ) : DocumentKeySet {
591
- return this . queryViewsByTarget [ targetId ]
592
- ? this . queryViewsByTarget [ targetId ] . view . syncedDocuments
593
- : documentKeySet ( ) ;
640
+ const limboResolution = this . limboResolutionsByTarget [ targetId ] ;
641
+ if ( limboResolution && limboResolution . receivedDocument ) {
642
+ return documentKeySet ( ) . add ( limboResolution . key ) ;
643
+ } else {
644
+ return this . queryViewsByTarget [ targetId ]
645
+ ? this . queryViewsByTarget [ targetId ] . view . syncedDocuments
646
+ : documentKeySet ( ) ;
647
+ }
594
648
}
595
649
}
0 commit comments