@@ -114,6 +114,52 @@ public <D extends MaybeDocument> DocumentChanges computeDocChanges(
114
114
return computeDocChanges (docChanges , null );
115
115
}
116
116
117
+ /**
118
+ * Computes the initial set of document changes based on the provided documents.
119
+ *
120
+ * <p>Unlike `computeDocChanges`, documents with committed mutations don't raise
121
+ * `hasPendingWrites`. This distinction allows us to only raise `hasPendingWrite` events for
122
+ * documents that changed during the lifetime of the View.
123
+ *
124
+ * @param docs The docs to apply to this view.
125
+ * @return A new set of docs, changes, and refill flag.
126
+ */
127
+ public <D extends MaybeDocument > DocumentChanges computeInitialChanges (
128
+ ImmutableSortedMap <DocumentKey , D > docs ) {
129
+ hardAssert (
130
+ this .documentSet .isEmpty (), "computeInitialChanges() called when docs are already present" );
131
+
132
+ DocumentViewChangeSet changeSet = new DocumentViewChangeSet ();
133
+ ImmutableSortedSet <DocumentKey > newMutatedKeys = this .mutatedKeys ;
134
+ DocumentSet newDocumentSet = this .documentSet ;
135
+
136
+ for (Map .Entry <DocumentKey , ? extends MaybeDocument > entry : docs ) {
137
+ DocumentKey key = entry .getKey ();
138
+ MaybeDocument maybeDoc = entry .getValue ();
139
+
140
+ if (maybeDoc instanceof Document ) {
141
+ Document doc = (Document ) maybeDoc ;
142
+ if (this .query .matches (doc )) {
143
+ changeSet .addChange (DocumentViewChange .create (Type .ADDED , doc ));
144
+ newDocumentSet = newDocumentSet .add (doc );
145
+ if (doc .hasLocalMutations ()) {
146
+ newMutatedKeys = newMutatedKeys .insert (key );
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ if (this .query .hasLimit ()) {
153
+ for (long i = newDocumentSet .size () - this .query .getLimit (); i > 0 ; --i ) {
154
+ Document oldDoc = newDocumentSet .getLastDocument ();
155
+ newDocumentSet = newDocumentSet .remove (oldDoc .getKey ());
156
+ newMutatedKeys = newMutatedKeys .remove (oldDoc .getKey ());
157
+ changeSet .addChange (DocumentViewChange .create (Type .REMOVED , oldDoc ));
158
+ }
159
+ }
160
+ return new DocumentChanges (newDocumentSet , changeSet , newMutatedKeys , /*needsRefill=*/ false );
161
+ }
162
+
117
163
/**
118
164
* Iterates over a set of doc changes, applies the query limit, and computes what the new results
119
165
* should be, what the changes were, and whether we may need to go back to the local cache for
@@ -169,49 +215,66 @@ public <D extends MaybeDocument> DocumentChanges computeDocChanges(
169
215
}
170
216
}
171
217
172
- if (newDoc != null ) {
173
- newDocumentSet = newDocumentSet .add (newDoc );
174
- if (newDoc .hasLocalMutations ()) {
175
- newMutatedKeys = newMutatedKeys .insert (newDoc .getKey ());
176
- } else {
177
- newMutatedKeys = newMutatedKeys .remove (newDoc .getKey ());
178
- }
179
- } else {
180
- newDocumentSet = newDocumentSet .remove (key );
181
- newMutatedKeys = newMutatedKeys .remove (key );
182
- }
218
+ boolean oldDocHadPendingMutations =
219
+ oldDoc != null && this .mutatedKeys .contains (oldDoc .getKey ());
220
+
221
+ // We only consider committed mutations for documents that were mutated during the lifetime of
222
+ // the view.
223
+ boolean newDocHasPendingMutations =
224
+ newDoc != null
225
+ && (newDoc .hasLocalMutations ()
226
+ || (this .mutatedKeys .contains (newDoc .getKey ())
227
+ && newDoc .hasCommittedMutations ()));
228
+
229
+ boolean changeApplied = false ;
230
+
183
231
// Calculate change
184
232
if (oldDoc != null && newDoc != null ) {
185
233
boolean docsEqual = oldDoc .getData ().equals (newDoc .getData ());
186
- if (!docsEqual || oldDoc .hasLocalMutations () != newDoc .hasLocalMutations ()) {
187
- // only report a change if document actually changed.
188
- if (docsEqual ) {
189
- changeSet .addChange (DocumentViewChange .create (Type .METADATA , newDoc ));
190
- } else {
234
+ if (!docsEqual ) {
235
+ if (!shouldWaitForSyncedDocument (oldDoc , newDoc )) {
191
236
changeSet .addChange (DocumentViewChange .create (Type .MODIFIED , newDoc ));
237
+ changeApplied = true ;
192
238
}
193
-
194
239
if (lastDocInLimit != null && query .comparator ().compare (newDoc , lastDocInLimit ) > 0 ) {
195
240
// This doc moved from inside the limit to after the limit. That means there may be some
196
241
// doc in the local cache that's actually less than this one.
197
242
needsRefill = true ;
198
243
}
244
+ } else if (oldDocHadPendingMutations != newDocHasPendingMutations ) {
245
+ changeSet .addChange (DocumentViewChange .create (Type .METADATA , newDoc ));
246
+ changeApplied = true ;
199
247
}
200
248
} else if (oldDoc == null && newDoc != null ) {
201
249
changeSet .addChange (DocumentViewChange .create (Type .ADDED , newDoc ));
250
+ changeApplied = true ;
202
251
} else if (oldDoc != null && newDoc == null ) {
203
252
changeSet .addChange (DocumentViewChange .create (Type .REMOVED , oldDoc ));
253
+ changeApplied = true ;
204
254
if (lastDocInLimit != null ) {
205
255
// A doc was removed from a full limit query. We'll need to requery from the local cache
206
256
// to see if we know about some other doc that should be in the results.
207
257
needsRefill = true ;
208
258
}
209
259
}
260
+
261
+ if (changeApplied ) {
262
+ if (newDoc != null ) {
263
+ newDocumentSet = newDocumentSet .add (newDoc );
264
+ if (newDoc .hasLocalMutations ()) {
265
+ newMutatedKeys = newMutatedKeys .insert (newDoc .getKey ());
266
+ } else {
267
+ newMutatedKeys = newMutatedKeys .remove (newDoc .getKey ());
268
+ }
269
+ } else {
270
+ newDocumentSet = newDocumentSet .remove (key );
271
+ newMutatedKeys = newMutatedKeys .remove (key );
272
+ }
273
+ }
210
274
}
211
275
212
276
if (query .hasLimit ()) {
213
- // TODO: Make QuerySnapshot size be constant time.
214
- while (newDocumentSet .size () > query .getLimit ()) {
277
+ for (long i = newDocumentSet .size () - this .query .getLimit (); i > 0 ; --i ) {
215
278
Document oldDoc = newDocumentSet .getLastDocument ();
216
279
newDocumentSet = newDocumentSet .remove (oldDoc .getKey ());
217
280
newMutatedKeys = newMutatedKeys .remove (oldDoc .getKey ());
@@ -226,6 +289,18 @@ public <D extends MaybeDocument> DocumentChanges computeDocChanges(
226
289
return new DocumentChanges (newDocumentSet , changeSet , newMutatedKeys , needsRefill );
227
290
}
228
291
292
+ private boolean shouldWaitForSyncedDocument (Document oldDoc , Document newDoc ) {
293
+ // We suppress the initial change event for documents that were modified as part of a write
294
+ // acknowledgment (e.g. when the value of a server transform is applied) as Watch will send us
295
+ // the same document again. By suppressing the event, we only raise two user visible events (one
296
+ // with `hasPendingWrites` and the final state of the document) instead of three (one with
297
+ // `hasPendingWrites`, the modified document with `hasPendingWrites` and the final state of the
298
+ // document).
299
+ return (oldDoc .hasLocalMutations ()
300
+ && newDoc .hasCommittedMutations ()
301
+ && !newDoc .hasLocalMutations ());
302
+ }
303
+
229
304
/**
230
305
* Updates the view with the given ViewDocumentChanges and updates limbo docs and sync state from
231
306
* the given (optional) target change.
@@ -273,15 +348,14 @@ public ViewChange applyChanges(DocumentChanges docChanges, TargetChange targetCh
273
348
ViewSnapshot snapshot = null ;
274
349
if (viewChanges .size () != 0 || syncStatedChanged ) {
275
350
boolean fromCache = newSyncState == SyncState .LOCAL ;
276
- boolean hasPendingWrites = !docChanges .mutatedKeys .isEmpty ();
277
351
snapshot =
278
352
new ViewSnapshot (
279
353
query ,
280
354
docChanges .documentSet ,
281
355
oldDocumentSet ,
282
356
viewChanges ,
283
357
fromCache ,
284
- hasPendingWrites ,
358
+ docChanges . mutatedKeys ,
285
359
syncStatedChanged );
286
360
}
287
361
return new ViewChange (snapshot , limboDocumentChanges );
0 commit comments