Skip to content

Commit d0d0b1c

Browse files
author
Brian Chen
authored
Adding onSnapshotsInSync (not public) (#778)
1 parent 9f201a5 commit d0d0b1c

File tree

8 files changed

+1059
-7
lines changed

8 files changed

+1059
-7
lines changed

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@
5050
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
5151
import com.google.firebase.firestore.util.AsyncQueue.TimerId;
5252
import com.google.firebase.firestore.util.Logger.Level;
53+
import java.util.ArrayList;
5354
import java.util.Arrays;
5455
import java.util.Collections;
5556
import java.util.Date;
5657
import java.util.List;
5758
import java.util.Map;
5859
import java.util.concurrent.CountDownLatch;
60+
import java.util.concurrent.Semaphore;
5961
import org.junit.After;
6062
import org.junit.Test;
6163
import org.junit.runner.RunWith;
@@ -509,6 +511,50 @@ public void testAddingToACollectionYieldsTheCorrectDocumentReference() {
509511
assertEquals(data, document.getData());
510512
}
511513

514+
@Test
515+
public void testSnapshotsInSyncListenerFiresAfterListenersInSync() {
516+
Map<String, Object> data = map("foo", 1.0);
517+
CollectionReference collection = testCollection();
518+
DocumentReference documentReference = waitFor(collection.add(data));
519+
List<String> events = new ArrayList<>();
520+
521+
Semaphore gotInitialSnapshot = new Semaphore(0);
522+
Semaphore done = new Semaphore(0);
523+
524+
ListenerRegistration listenerRegistration = null;
525+
526+
documentReference.addSnapshotListener(
527+
(value, error) -> {
528+
events.add("doc");
529+
gotInitialSnapshot.release();
530+
});
531+
waitFor(gotInitialSnapshot);
532+
events.clear();
533+
534+
try {
535+
listenerRegistration =
536+
documentReference
537+
.getFirestore()
538+
.addSnapshotsInSyncListener(
539+
() -> {
540+
events.add("snapshots-in-sync");
541+
if (events.size() == 3) {
542+
// We should have an initial snapshots-in-sync event, then a snapshot event
543+
// for set(), then another event to indicate we're in sync again.
544+
assertEquals(
545+
Arrays.asList("snapshots-in-sync", "doc", "snapshots-in-sync"), events);
546+
done.release();
547+
}
548+
});
549+
waitFor(documentReference.set(map("foo", 3.0)));
550+
waitFor(done);
551+
} finally {
552+
if (listenerRegistration != null) {
553+
listenerRegistration.remove();
554+
}
555+
}
556+
}
557+
512558
@Test
513559
public void testQueriesAreValidatedOnClient() {
514560
// NOTE: Failure cases are validated in ValidationTest.

firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
package com.google.firebase.firestore;
1616

1717
import static com.google.common.base.Preconditions.checkNotNull;
18+
import static com.google.firebase.firestore.util.Assert.hardAssert;
1819

20+
import android.app.Activity;
1921
import android.content.Context;
2022
import androidx.annotation.NonNull;
2123
import androidx.annotation.Nullable;
@@ -30,12 +32,15 @@
3032
import com.google.firebase.firestore.auth.CredentialsProvider;
3133
import com.google.firebase.firestore.auth.EmptyCredentialsProvider;
3234
import com.google.firebase.firestore.auth.FirebaseAuthCredentialsProvider;
35+
import com.google.firebase.firestore.core.ActivityScope;
36+
import com.google.firebase.firestore.core.AsyncEventListener;
3337
import com.google.firebase.firestore.core.DatabaseInfo;
3438
import com.google.firebase.firestore.core.FirestoreClient;
3539
import com.google.firebase.firestore.local.SQLitePersistence;
3640
import com.google.firebase.firestore.model.DatabaseId;
3741
import com.google.firebase.firestore.model.ResourcePath;
3842
import com.google.firebase.firestore.util.AsyncQueue;
43+
import com.google.firebase.firestore.util.Executors;
3944
import com.google.firebase.firestore.util.Logger;
4045
import com.google.firebase.firestore.util.Logger.Level;
4146
import java.util.concurrent.Executor;
@@ -460,6 +465,94 @@ public Task<Void> clearPersistence() {
460465
return source.getTask();
461466
}
462467

468+
/**
469+
* Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that
470+
* all listeners affected by a given change have fired, even if a single server-generated change
471+
* affects multiple listeners.
472+
*
473+
* <p>NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other,
474+
* but does not relate to whether those snapshots are in sync with the server. Use
475+
* SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or
476+
* the server.
477+
*
478+
* @param runnable A callback to be called every time all snapshot listeners are in sync with each
479+
* other.
480+
* @return A registration object that can be used to remove the listener.
481+
*/
482+
ListenerRegistration addSnapshotsInSyncListener(@NonNull Runnable runnable) {
483+
return addSnapshotsInSyncListener(Executors.DEFAULT_CALLBACK_EXECUTOR, runnable);
484+
}
485+
486+
/**
487+
* Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that
488+
* all listeners affected by a given change have fired, even if a single server-generated change
489+
* affects multiple listeners.
490+
*
491+
* <p>NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other,
492+
* but does not relate to whether those snapshots are in sync with the server. Use
493+
* SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or
494+
* the server.
495+
*
496+
* @param activity The activity to scope the listener to.
497+
* @param runnable A callback to be called every time all snapshot listeners are in sync with each
498+
* other.
499+
* @return A registration object that can be used to remove the listener.
500+
*/
501+
@NonNull
502+
ListenerRegistration addSnapshotsInSyncListener(Activity activity, @NonNull Runnable runnable) {
503+
return addSnapshotsInSyncListener(Executors.DEFAULT_CALLBACK_EXECUTOR, activity, runnable);
504+
}
505+
506+
/**
507+
* Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that
508+
* all listeners affected by a given change have fired, even if a single server-generated change
509+
* affects multiple listeners.
510+
*
511+
* <p>NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other,
512+
* but does not relate to whether those snapshots are in sync with the server. Use
513+
* SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or
514+
* the server.
515+
*
516+
* @param executor The executor to use to call the listener.
517+
* @param runnable A callback to be called every time all snapshot listeners are in sync with each
518+
* other.
519+
* @return A registration object that can be used to remove the listener.
520+
*/
521+
@NonNull
522+
ListenerRegistration addSnapshotsInSyncListener(Executor executor, @NonNull Runnable runnable) {
523+
return addSnapshotsInSyncListener(executor, null, runnable);
524+
}
525+
526+
/**
527+
* Internal helper method to add a snapshotsInSync listener.
528+
*
529+
* <p>Will be Activity scoped if the activity parameter is non-{@code null}.
530+
*
531+
* @param userExecutor The executor to use to call the listener.
532+
* @param activity Optional activity this listener is scoped to.
533+
* @param runnable A callback to be called every time all snapshot listeners are in sync with each
534+
* other.
535+
* @return A registration object that can be used to remove the listener.
536+
*/
537+
private ListenerRegistration addSnapshotsInSyncListener(
538+
Executor userExecutor, @Nullable Activity activity, @NonNull Runnable runnable) {
539+
ensureClientConfigured();
540+
EventListener<Void> eventListener =
541+
(Void v, FirebaseFirestoreException error) -> {
542+
hardAssert(error == null, "snapshots-in-sync listeners should never get errors.");
543+
runnable.run();
544+
};
545+
AsyncEventListener<Void> asyncListener =
546+
new AsyncEventListener<Void>(userExecutor, eventListener);
547+
client.addSnapshotsInSyncListener(asyncListener);
548+
return ActivityScope.bind(
549+
activity,
550+
() -> {
551+
asyncListener.mute();
552+
client.removeSnapshotsInSyncListener(asyncListener);
553+
});
554+
}
555+
463556
FirestoreClient getClient() {
464557
return client;
465558
}

firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@
1414

1515
package com.google.firebase.firestore.core;
1616

17+
import static com.google.firebase.firestore.util.Assert.hardAssert;
18+
19+
import com.google.firebase.firestore.EventListener;
1720
import com.google.firebase.firestore.core.SyncEngine.SyncEngineCallback;
1821
import com.google.firebase.firestore.util.Util;
1922
import io.grpc.Status;
2023
import java.util.ArrayList;
2124
import java.util.HashMap;
25+
import java.util.HashSet;
2226
import java.util.List;
2327
import java.util.Map;
28+
import java.util.Set;
2429

2530
/**
2631
* EventManager is responsible for mapping queries to query event listeners. It handles "fan-out."
@@ -54,6 +59,8 @@ public static class ListenOptions {
5459

5560
private final Map<Query, QueryListenersInfo> queries;
5661

62+
private final Set<EventListener<Void>> snapshotsInSyncListeners = new HashSet<>();
63+
5764
private OnlineState onlineState = OnlineState.UNKNOWN;
5865

5966
public EventManager(SyncEngine syncEngine) {
@@ -81,10 +88,16 @@ public int addQueryListener(QueryListener queryListener) {
8188

8289
queryInfo.listeners.add(queryListener);
8390

84-
queryListener.onOnlineStateChanged(onlineState);
91+
// Run global snapshot listeners if a consistent snapshot has been emitted.
92+
boolean raisedEvent = queryListener.onOnlineStateChanged(onlineState);
93+
hardAssert(
94+
!raisedEvent, "onOnlineStateChanged() shouldn't raise an event for brand-new listeners.");
8595

8696
if (queryInfo.viewSnapshot != null) {
87-
queryListener.onViewSnapshot(queryInfo.viewSnapshot);
97+
raisedEvent = queryListener.onViewSnapshot(queryInfo.viewSnapshot);
98+
if (raisedEvent) {
99+
raiseSnapshotsInSyncEvent();
100+
}
88101
}
89102

90103
if (firstListen) {
@@ -109,18 +122,40 @@ public void removeQueryListener(QueryListener listener) {
109122
}
110123
}
111124

125+
public void addSnapshotsInSyncListener(EventListener<Void> listener) {
126+
snapshotsInSyncListeners.add(listener);
127+
listener.onEvent(null, null);
128+
}
129+
130+
public void removeSnapshotsInSyncListener(EventListener<Void> listener) {
131+
snapshotsInSyncListeners.remove(listener);
132+
}
133+
134+
/** Call all global snapshot listeners that have been set. */
135+
private void raiseSnapshotsInSyncEvent() {
136+
for (EventListener<Void> listener : snapshotsInSyncListeners) {
137+
listener.onEvent(null, null);
138+
}
139+
}
140+
112141
@Override
113142
public void onViewSnapshots(List<ViewSnapshot> snapshotList) {
143+
boolean raisedEvent = false;
114144
for (ViewSnapshot viewSnapshot : snapshotList) {
115145
Query query = viewSnapshot.getQuery();
116146
QueryListenersInfo info = queries.get(query);
117147
if (info != null) {
118148
for (QueryListener listener : info.listeners) {
119-
listener.onViewSnapshot(viewSnapshot);
149+
if (listener.onViewSnapshot(viewSnapshot)) {
150+
raisedEvent = true;
151+
}
120152
}
121153
info.viewSnapshot = viewSnapshot;
122154
}
123155
}
156+
if (raisedEvent) {
157+
raiseSnapshotsInSyncEvent();
158+
}
124159
}
125160

126161
@Override
@@ -136,11 +171,17 @@ public void onError(Query query, Status error) {
136171

137172
@Override
138173
public void handleOnlineStateChange(OnlineState onlineState) {
174+
boolean raisedEvent = false;
139175
this.onlineState = onlineState;
140176
for (QueryListenersInfo info : queries.values()) {
141177
for (QueryListener listener : info.listeners) {
142-
listener.onOnlineStateChanged(onlineState);
178+
if (listener.onOnlineStateChanged(onlineState)) {
179+
raisedEvent = true;
180+
}
143181
}
144182
}
183+
if (raisedEvent) {
184+
raiseSnapshotsInSyncEvent();
185+
}
145186
}
146187
}

firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,18 @@ private void initialize(Context context, User user, boolean usePersistence, long
289289
remoteStore.start();
290290
}
291291

292+
public void addSnapshotsInSyncListener(EventListener<Void> listener) {
293+
verifyNotTerminated();
294+
asyncQueue.enqueueAndForget(() -> eventManager.addSnapshotsInSyncListener(listener));
295+
}
296+
297+
public void removeSnapshotsInSyncListener(EventListener<Void> listener) {
298+
if (isTerminated()) {
299+
return;
300+
}
301+
eventManager.removeSnapshotsInSyncListener(listener);
302+
}
303+
292304
private void verifyNotTerminated() {
293305
if (this.isTerminated()) {
294306
throw new IllegalStateException("The client has already been terminated");

firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,17 @@ public Query getQuery() {
5959
return query;
6060
}
6161

62-
public void onViewSnapshot(ViewSnapshot newSnapshot) {
62+
/**
63+
* Applies the new ViewSnapshot to this listener, raising a user-facing event if applicable
64+
* (depending on what changed, whether the user has opted into metadata-only changes, etc.).
65+
* Returns true if a user-facing event was indeed raised.
66+
*/
67+
public boolean onViewSnapshot(ViewSnapshot newSnapshot) {
6368
hardAssert(
6469
!newSnapshot.getChanges().isEmpty() || newSnapshot.didSyncStateChange(),
6570
"We got a new snapshot with no changes?");
6671

72+
boolean raisedEvent = false;
6773
if (!options.includeDocumentMetadataChanges) {
6874
// Remove the metadata only changes
6975
List<DocumentViewChange> documentChanges = new ArrayList<>();
@@ -87,23 +93,30 @@ public void onViewSnapshot(ViewSnapshot newSnapshot) {
8793
if (!raisedInitialEvent) {
8894
if (shouldRaiseInitialEvent(newSnapshot, onlineState)) {
8995
raiseInitialEvent(newSnapshot);
96+
raisedEvent = true;
9097
}
9198
} else if (shouldRaiseEvent(newSnapshot)) {
9299
listener.onEvent(newSnapshot, null);
100+
raisedEvent = true;
93101
}
94102

95103
this.snapshot = newSnapshot;
104+
return raisedEvent;
96105
}
97106

98107
public void onError(FirebaseFirestoreException error) {
99108
listener.onEvent(null, error);
100109
}
101110

102-
public void onOnlineStateChanged(OnlineState onlineState) {
111+
/** Returns whether a snapshot was raised. */
112+
public boolean onOnlineStateChanged(OnlineState onlineState) {
103113
this.onlineState = onlineState;
114+
boolean raisedEvent = false;
104115
if (snapshot != null && !raisedInitialEvent && shouldRaiseInitialEvent(snapshot, onlineState)) {
105116
raiseInitialEvent(snapshot);
117+
raisedEvent = true;
106118
}
119+
return raisedEvent;
107120
}
108121

109122
private boolean shouldRaiseInitialEvent(ViewSnapshot snapshot, OnlineState onlineState) {

firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ public void testWillForwardOnOnlineStateChangedCalls() {
118118

119119
QueryListener spy = mock(QueryListener.class);
120120
when(spy.getQuery()).thenReturn(query1);
121-
doAnswer(invocation -> events.add(invocation.getArguments()[0]))
121+
doAnswer(
122+
invocation -> {
123+
events.add(invocation.getArguments()[0]);
124+
return false;
125+
})
122126
.when(spy)
123127
.onOnlineStateChanged(any());
124128

0 commit comments

Comments
 (0)