Skip to content

Commit fca1076

Browse files
authored
Fix b/72502745: OnlineState changes cause limbo document crash. (#470)
Context: I made a previous change to raise isFromCache=true events when the client goes offline. As part of this change I added an assert for "OnlineState should not affect limbo documents", which it turns out was not valid because: * When we go offline, we set the view to non-current and call View.applyChanges(). * View.applyChanges() calls applyTargetChange() even though there's no target change and it recalculates limbo documents. * When the view is not current, we consider no documents to be in limbo. * Therefore all limbo documents are removed and so applyChanges() ends up returning unexpected LimboDocumentChanges. Fix: I've modified the View logic so that we don't recalculate limbo documents (and generate LimboDocumentChanges) when the View is not current, so now my assert holds and there should be less spurious removal / re-adding of limbo documents.
1 parent 81d6b1e commit fca1076

File tree

2 files changed

+46
-12
lines changed

2 files changed

+46
-12
lines changed

packages/firestore/src/core/view.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ export class View {
244244
);
245245
});
246246

247-
const limboChanges = this.applyTargetChange(targetChange);
247+
this.applyTargetChange(targetChange);
248+
const limboChanges = this.updateLimboDocuments();
248249
const synced = this.limboDocuments.size === 0 && this.current;
249250
const newSyncState = synced ? SyncState.Synced : SyncState.Local;
250251
const syncStateChanged = newSyncState !== this.syncState;
@@ -320,9 +321,7 @@ export class View {
320321
* Updates syncedDocuments, current, and limbo docs based on the given change.
321322
* Returns the list of changes to which docs are in limbo.
322323
*/
323-
private applyTargetChange(
324-
targetChange?: TargetChange
325-
): LimboDocumentChange[] {
324+
private applyTargetChange(targetChange?: TargetChange): void {
326325
if (targetChange) {
327326
const targetMapping = targetChange.mapping;
328327
if (targetMapping instanceof ResetMapping) {
@@ -348,19 +347,23 @@ export class View {
348347
);
349348
}
350349
}
350+
}
351+
352+
private updateLimboDocuments(): LimboDocumentChange[] {
353+
// We can only determine limbo documents when we're in-sync with the server.
354+
if (!this.current) {
355+
return [];
356+
}
351357

352-
// Recompute the set of limbo docs.
353358
// TODO(klimt): Do this incrementally so that it's not quadratic when
354359
// updating many documents.
355360
const oldLimboDocuments = this.limboDocuments;
356361
this.limboDocuments = documentKeySet();
357-
if (this.current) {
358-
this.documentSet.forEach(doc => {
359-
if (this.shouldBeInLimbo(doc.key)) {
360-
this.limboDocuments = this.limboDocuments.add(doc.key);
361-
}
362-
});
363-
}
362+
this.documentSet.forEach(doc => {
363+
if (this.shouldBeInLimbo(doc.key)) {
364+
this.limboDocuments = this.limboDocuments.add(doc.key);
365+
}
366+
});
364367

365368
// Diff the new limbo docs with the old limbo docs.
366369
const changes: LimboDocumentChange[] = [];

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,35 @@ describeSpec('Offline:', [], () => {
115115
.expectEvents(query, { fromCache: false })
116116
);
117117
});
118+
119+
specTest('Queries with limbo documents handle going offline.', [], () => {
120+
const query = Query.atPath(path('collection'));
121+
const docA = doc('collection/a', 1000, { key: 'a' });
122+
const docB = doc('collection/b', 1005, { key: 'b' });
123+
const limboQuery = Query.atPath(docA.key.path);
124+
return (
125+
spec()
126+
.userListens(query)
127+
.watchAcksFull(query, 1000, docA)
128+
.expectEvents(query, { added: [docA] })
129+
.watchResets(query)
130+
// No more documents
131+
.watchCurrents(query, 'resume-token-1001')
132+
.watchSnapshots(1001)
133+
// docA will now be in limbo (triggering fromCache=true)
134+
.expectLimboDocs(docA.key)
135+
.expectEvents(query, { fromCache: true })
136+
// first error triggers unknown state
137+
.watchStreamCloses(Code.UNAVAILABLE)
138+
.restoreListen(query, 'resume-token-1001')
139+
// getting two more errors triggers offline state.
140+
.watchStreamCloses(Code.UNAVAILABLE)
141+
.watchStreamCloses(Code.UNAVAILABLE)
142+
.watchAcksFull(query, 1001)
143+
.watchAcksFull(limboQuery, 1001)
144+
// Limbo document is resolved. No longer from cache.
145+
.expectEvents(query, { removed: [docA], fromCache: false })
146+
.expectLimboDocs()
147+
);
148+
});
118149
});

0 commit comments

Comments
 (0)