Skip to content

Commit f8c1464

Browse files
SelaseKayMichaelVerdonrussellwheatley
authored
feat(firestore): add support for onSnapshotsInSync (#8379)
--------- Co-authored-by: MichaelVerdon <[email protected]> Co-authored-by: russellwheatley <[email protected]>
1 parent d0fa581 commit f8c1464

File tree

9 files changed

+276
-0
lines changed

9 files changed

+276
-0
lines changed

packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@
2222
import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.instanceCache;
2323

2424
import android.content.Context;
25+
import android.util.SparseArray;
26+
import com.facebook.react.bridge.Arguments;
27+
import com.facebook.react.bridge.WritableMap;
2528
import com.google.android.gms.tasks.Task;
2629
import com.google.android.gms.tasks.Tasks;
2730
import com.google.firebase.firestore.FirebaseFirestore;
31+
import com.google.firebase.firestore.ListenerRegistration;
2832
import com.google.firebase.firestore.LoadBundleTask;
33+
import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter;
2934
import io.invertase.firebase.common.UniversalFirebaseModule;
3035
import io.invertase.firebase.common.UniversalFirebasePreferences;
3136
import java.nio.charset.StandardCharsets;
@@ -34,13 +39,43 @@
3439
import java.util.Objects;
3540

3641
public class UniversalFirebaseFirestoreModule extends UniversalFirebaseModule {
42+
private static SparseArray<ListenerRegistration> onSnapshotInSyncListeners = new SparseArray<>();
3743

3844
private static HashMap<String, String> emulatorConfigs = new HashMap<>();
3945

4046
UniversalFirebaseFirestoreModule(Context context, String serviceName) {
4147
super(context, serviceName);
4248
}
4349

50+
void addSnapshotsInSync(String appName, String databaseId, int listenerId) {
51+
52+
FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId);
53+
ListenerRegistration listenerRegistration =
54+
firebaseFirestore.addSnapshotsInSyncListener(
55+
() -> {
56+
ReactNativeFirebaseEventEmitter emitter =
57+
ReactNativeFirebaseEventEmitter.getSharedInstance();
58+
WritableMap body = Arguments.createMap();
59+
emitter.sendEvent(
60+
new ReactNativeFirebaseFirestoreEvent(
61+
ReactNativeFirebaseFirestoreEvent.SNAPSHOT_IN_SYNC_EVENT_SYNC,
62+
body,
63+
appName,
64+
databaseId,
65+
listenerId));
66+
});
67+
68+
onSnapshotInSyncListeners.put(listenerId, listenerRegistration);
69+
}
70+
71+
void removeSnapshotsInSync(String appName, String databaseId, int listenerId) {
72+
ListenerRegistration listenerRegistration = onSnapshotInSyncListeners.get(listenerId);
73+
if (listenerRegistration != null) {
74+
listenerRegistration.remove();
75+
onSnapshotInSyncListeners.remove(listenerId);
76+
}
77+
}
78+
4479
Task<Void> disableNetwork(String appName, String databaseId) {
4580
return getFirestoreForApp(appName, databaseId).disableNetwork();
4681
}

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreEvent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class ReactNativeFirebaseFirestoreEvent implements NativeEvent {
2626
static final String COLLECTION_EVENT_SYNC = "firestore_collection_sync_event";
2727
static final String DOCUMENT_EVENT_SYNC = "firestore_document_sync_event";
2828
static final String TRANSACTION_EVENT_SYNC = "firestore_transaction_event";
29+
static final String SNAPSHOT_IN_SYNC_EVENT_SYNC = "firestore_snapshots_in_sync_event";
2930
private static final String KEY_ID = "listenerId";
3031
private static final String KEY_BODY = "body";
3132
private static final String KEY_APP_NAME = "appName";

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,20 @@ public void persistenceCacheIndexManager(
193193
promise.resolve(null);
194194
}
195195

196+
@ReactMethod
197+
public void addSnapshotsInSync(
198+
String appName, String databaseId, int listenerId, Promise promise) {
199+
module.addSnapshotsInSync(appName, databaseId, listenerId);
200+
promise.resolve(null);
201+
}
202+
203+
@ReactMethod
204+
public void removeSnapshotsInSync(
205+
String appName, String databaseId, int listenerId, Promise promise) {
206+
module.removeSnapshotsInSync(appName, databaseId, listenerId);
207+
promise.resolve(null);
208+
}
209+
196210
private WritableMap taskProgressToWritableMap(LoadBundleTaskProgress progress) {
197211
WritableMap writableMap = Arguments.createMap();
198212
writableMap.putDouble("bytesLoaded", progress.getBytesLoaded());

packages/firestore/e2e/firestore.e2e.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,5 +915,101 @@ describe('firestore()', function () {
915915
});
916916
});
917917
});
918+
919+
describe('snapshotsInSync', function () {
920+
const { getFirestore, onSnapshotsInSync, doc, setDoc, deleteDoc } = firestoreModular;
921+
922+
it('snapshotsInSync fires when document is updated and synced', async function () {
923+
const events = [];
924+
925+
const db = getFirestore();
926+
const testDoc1 = doc(db, `${COLLECTION}/snapshotsInSync1`);
927+
const testDoc2 = doc(db, `${COLLECTION}/snapshotsInSync2`);
928+
929+
if (Platform.other) {
930+
// Should throw error for lite SDK
931+
try {
932+
const unsubscribe = onSnapshotsInSync(getFirestore(), () => {});
933+
unsubscribe();
934+
} catch (e) {
935+
e.message.should.equal('Not supported in the lite SDK.');
936+
}
937+
return;
938+
}
939+
940+
let unsubscribe;
941+
const syncPromise = new Promise(resolve => {
942+
unsubscribe = onSnapshotsInSync(db, () => {
943+
events.push('sync');
944+
if (events.length >= 1) {
945+
resolve();
946+
}
947+
});
948+
});
949+
950+
await Promise.all([setDoc(testDoc1, { test: 1 }), setDoc(testDoc2, { test: 2 })]);
951+
952+
await syncPromise;
953+
954+
unsubscribe();
955+
956+
// Verify unsubscribe worked by doing another write
957+
await setDoc(testDoc1, { test: 3 });
958+
959+
// Cleanup
960+
await Promise.all([deleteDoc(testDoc1), deleteDoc(testDoc2)]);
961+
962+
events.length.should.be.greaterThan(0);
963+
events.forEach(event => event.should.equal('sync'));
964+
});
965+
966+
it('unsubscribe() call should prevent further sync events', async function () {
967+
const events = [];
968+
969+
const db = getFirestore();
970+
const testDoc1 = doc(db, `${COLLECTION}/snapshotsInSync1`);
971+
const testDoc2 = doc(db, `${COLLECTION}/snapshotsInSync2`);
972+
973+
if (Platform.other) {
974+
// Should throw error for lite SDK
975+
try {
976+
const unsubscribe = onSnapshotsInSync(getFirestore(), () => {});
977+
unsubscribe();
978+
} catch (e) {
979+
e.message.should.equal('Not supported in the lite SDK.');
980+
}
981+
return;
982+
}
983+
984+
let unsubscribe;
985+
const syncPromise = new Promise(resolve => {
986+
unsubscribe = onSnapshotsInSync(db, () => {
987+
events.push('sync');
988+
if (events.length >= 1) {
989+
resolve();
990+
}
991+
});
992+
});
993+
994+
// Trigger initial sync events
995+
await Promise.all([setDoc(testDoc1, { test: 1 }), setDoc(testDoc2, { test: 2 })]);
996+
997+
await syncPromise;
998+
999+
// Record the number of events before unsubscribe
1000+
const eventsBeforeUnsubscribe = events.length;
1001+
1002+
await unsubscribe();
1003+
1004+
await setDoc(testDoc1, { test: 3 });
1005+
await setDoc(testDoc2, { test: 4 });
1006+
1007+
await Promise.all([deleteDoc(testDoc1), deleteDoc(testDoc2)]);
1008+
1009+
// Verify that no additional events were recorded after unsubscribe
1010+
events.length.should.equal(eventsBeforeUnsubscribe);
1011+
events.forEach(event => event.should.equal('sync'));
1012+
});
1013+
});
9181014
});
9191015
});

packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
*/
1717

1818
#import "RNFBFirestoreModule.h"
19+
#import <RNFBApp/RNFBRCTEventEmitter.h>
1920
#import <React/RCTUtils.h>
2021
#import "FirebaseFirestoreInternal/FIRPersistentCacheIndexManager.h"
2122
#import "RNFBFirestoreCommon.h"
2223
#import "RNFBPreferences.h"
2324

2425
NSMutableDictionary *emulatorConfigs;
26+
static __strong NSMutableDictionary *snapshotsInSyncListeners;
27+
static NSString *const RNFB_FIRESTORE_SNAPSHOTS_IN_SYNC = @"firestore_snapshots_in_sync_event";
2528

2629
@implementation RNFBFirestoreModule
2730
#pragma mark -
@@ -240,6 +243,51 @@ + (BOOL)requiresMainQueueSetup {
240243
resolve(nil);
241244
}
242245

246+
RCT_EXPORT_METHOD(addSnapshotsInSync
247+
: (FIRApp *)firebaseApp
248+
: (NSString *)databaseId
249+
: (nonnull NSNumber *)listenerId
250+
: (RCTPromiseResolveBlock)resolve
251+
: (RCTPromiseRejectBlock)reject) {
252+
if (snapshotsInSyncListeners[listenerId]) {
253+
resolve(nil);
254+
return;
255+
}
256+
257+
FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp
258+
databaseId:databaseId];
259+
260+
id<FIRListenerRegistration> listener = [firestore addSnapshotsInSyncListener:^{
261+
[[RNFBRCTEventEmitter shared]
262+
sendEventWithName:RNFB_FIRESTORE_SNAPSHOTS_IN_SYNC
263+
body:@{
264+
@"appName" : [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name],
265+
@"databaseId" : databaseId,
266+
@"listenerId" : listenerId,
267+
@"body" : @{}
268+
}];
269+
}];
270+
271+
snapshotsInSyncListeners[listenerId] = listener;
272+
273+
resolve(nil);
274+
}
275+
276+
RCT_EXPORT_METHOD(removeSnapshotsInSync
277+
: (FIRApp *)firebaseApp
278+
: (NSString *)databaseId
279+
: (nonnull NSNumber *)listenerId
280+
: (RCTPromiseResolveBlock)resolve
281+
: (RCTPromiseRejectBlock)reject) {
282+
id<FIRListenerRegistration> listener = snapshotsInSyncListeners[listenerId];
283+
if (listener) {
284+
[listener remove];
285+
[snapshotsInSyncListeners removeObjectForKey:listenerId];
286+
}
287+
288+
resolve(nil);
289+
}
290+
243291
- (NSMutableDictionary *)taskProgressToDictionary:(FIRLoadBundleTaskProgress *)progress {
244292
NSMutableDictionary *progressMap = [[NSMutableDictionary alloc] init];
245293
progressMap[@"bytesLoaded"] = @(progress.bytesLoaded);

packages/firestore/lib/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const nativeEvents = [
5656
'firestore_collection_sync_event',
5757
'firestore_document_sync_event',
5858
'firestore_transaction_event',
59+
'firestore_snapshots_in_sync_event',
5960
];
6061

6162
class FirebaseFirestoreModule extends FirebaseModule {
@@ -84,6 +85,13 @@ class FirebaseFirestoreModule extends FirebaseModule {
8485
);
8586
});
8687

88+
this.emitter.addListener(this.eventNameForApp('firestore_snapshots_in_sync_event'), event => {
89+
this.emitter.emit(
90+
this.eventNameForApp(`firestore_snapshots_in_sync_event:${event.listenerId}`),
91+
event,
92+
);
93+
});
94+
8795
this._settings = {
8896
ignoreUndefinedProperties: false,
8997
persistence: true,

packages/firestore/lib/modular/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ export function collectionGroup(firestore, collectionId) {
8989
return firestore.collectionGroup.call(firestore, collectionId, MODULAR_DEPRECATION_ARG);
9090
}
9191

92+
let _id_SnapshotInSync = 0;
93+
94+
export function onSnapshotsInSync(firestore, callback) {
95+
const listenerId = _id_SnapshotInSync++;
96+
firestore.native.addSnapshotsInSync(listenerId);
97+
const onSnapshotsInSyncSubscription = firestore.emitter.addListener(
98+
firestore.eventNameForApp(`firestore_snapshots_in_sync_event:${listenerId}`),
99+
() => {
100+
callback();
101+
},
102+
);
103+
104+
return () => {
105+
onSnapshotsInSyncSubscription.remove();
106+
firestore.native.removeSnapshotsInSync(listenerId);
107+
};
108+
}
109+
92110
/**
93111
* @param {DocumentReference} reference
94112
* @param {import('.').PartialWithFieldValue} data

packages/firestore/lib/web/RNFBFirestoreModule.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ export default {
132132
return rejectWithCodeAndMessage('unsupported', 'Not supported in the lite SDK.');
133133
},
134134

135+
addSnapshotsInSync() {
136+
return rejectWithCodeAndMessage('unsupported', 'Not supported in the lite SDK.');
137+
},
138+
139+
removeSnapshotsInSync() {
140+
return rejectWithCodeAndMessage('unsupported', 'Not supported in the lite SDK.');
141+
},
142+
135143
/**
136144
* Use the Firestore emulator.
137145
* @param {string} appName - The app name.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React, { useEffect } from 'react';
2+
import { AppRegistry, Button, Text, View } from 'react-native';
3+
4+
import '@react-native-firebase/app';
5+
import firestore, { onSnapshotsInSync } from '@react-native-firebase/firestore';
6+
7+
const fire = firestore();
8+
function App() {
9+
let unsubscribe;
10+
useEffect(() => {
11+
unsubscribe = onSnapshotsInSync(fire, () => {
12+
console.log('onSnapshotsInSync');
13+
});
14+
}, []);
15+
16+
async function addDocument() {
17+
await firestore().collection('flutter-tests').doc('one').set({ foo: 'bar' });
18+
}
19+
20+
return (
21+
<View>
22+
<Text>React Native Firebase</Text>
23+
<Text>onSnapshotsInSync API</Text>
24+
<Button
25+
title="add document"
26+
onPress={async () => {
27+
try {
28+
addDocument();
29+
} catch (e) {
30+
console.log('EEEE', e);
31+
}
32+
}}
33+
/>
34+
<Button
35+
title="unsubscribe to snapshot in sync"
36+
onPress={async () => {
37+
try {
38+
unsubscribe();
39+
} catch (e) {
40+
console.log('EEEE', e);
41+
}
42+
}}
43+
/>
44+
</View>
45+
);
46+
}
47+
48+
AppRegistry.registerComponent('testing', () => App);

0 commit comments

Comments
 (0)