From 0fa10ad3cb2a72bec19830f5bc7dd6e3a062e3e0 Mon Sep 17 00:00:00 2001 From: "jude.kwashie" Date: Fri, 28 Feb 2025 16:09:14 +0000 Subject: [PATCH 1/9] chore(firestore): add support for onSnapshotsInSync --- packages/firestore/lib/modular/snapshot.d.ts | 10 ++++++++++ packages/firestore/lib/modular/snapshot.js | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/packages/firestore/lib/modular/snapshot.d.ts b/packages/firestore/lib/modular/snapshot.d.ts index 38384a5252..bdb6f267a5 100644 --- a/packages/firestore/lib/modular/snapshot.d.ts +++ b/packages/firestore/lib/modular/snapshot.d.ts @@ -227,3 +227,13 @@ export declare function queryEqual, right: Query, ): boolean; + +/** + * Attaches a listener for a snapshots-in-sync event. + * The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if + * a single server-generated change affects multiple listeners. + * + * @param firestore + * @param onSync + */ +export declare function onSnapshotsInSync(firestore: Firestore, onSync: () => void): Unsubscribe; \ No newline at end of file diff --git a/packages/firestore/lib/modular/snapshot.js b/packages/firestore/lib/modular/snapshot.js index 946f1c819d..21805eab53 100644 --- a/packages/firestore/lib/modular/snapshot.js +++ b/packages/firestore/lib/modular/snapshot.js @@ -18,3 +18,8 @@ export function onSnapshot(reference, ...args) { export function snapshotEqual(left, right) { return left.isEqual.call(left, right, MODULAR_DEPRECATION_ARG); } + + +export function onSnapshotsInSync(firestore, ...args) { + return firestore.onSnapshotsInSync.call(firestore, ...args, MODULAR_DEPRECATION_ARG); +} \ No newline at end of file From a6d49914e7a74df51c2f4742628cde1948a08166 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Tue, 29 Apr 2025 11:41:03 +0100 Subject: [PATCH 2/9] fix: formatting --- packages/firestore/lib/modular/snapshot.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/firestore/lib/modular/snapshot.d.ts b/packages/firestore/lib/modular/snapshot.d.ts index bdb6f267a5..400e36faf8 100644 --- a/packages/firestore/lib/modular/snapshot.d.ts +++ b/packages/firestore/lib/modular/snapshot.d.ts @@ -236,4 +236,6 @@ export declare function queryEqual void): Unsubscribe; \ No newline at end of file +export declare function onSnapshotsInSync( + firestore: Firestore, onSync: () => void +): Unsubscribe; From 3a624bffddec91a8eebbe3edbdcf500f84d86d5e Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Tue, 29 Apr 2025 11:42:06 +0100 Subject: [PATCH 3/9] fix: formatting --- packages/firestore/lib/modular/snapshot.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/firestore/lib/modular/snapshot.js b/packages/firestore/lib/modular/snapshot.js index 21805eab53..b3e35430bb 100644 --- a/packages/firestore/lib/modular/snapshot.js +++ b/packages/firestore/lib/modular/snapshot.js @@ -19,7 +19,6 @@ export function snapshotEqual(left, right) { return left.isEqual.call(left, right, MODULAR_DEPRECATION_ARG); } - export function onSnapshotsInSync(firestore, ...args) { return firestore.onSnapshotsInSync.call(firestore, ...args, MODULAR_DEPRECATION_ARG); -} \ No newline at end of file +} From 01582b7c235b9ff0f4f2f07d68229266967ba503 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Tue, 29 Apr 2025 11:47:06 +0100 Subject: [PATCH 4/9] fix: formatting --- packages/firestore/lib/modular/snapshot.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/firestore/lib/modular/snapshot.d.ts b/packages/firestore/lib/modular/snapshot.d.ts index 400e36faf8..4def1d0fa7 100644 --- a/packages/firestore/lib/modular/snapshot.d.ts +++ b/packages/firestore/lib/modular/snapshot.d.ts @@ -236,6 +236,4 @@ export declare function queryEqual void -): Unsubscribe; +export declare function onSnapshotsInSync(firestore: Firestore, onSync: () => void): Unsubscribe; From adcc24064e6f7f7a1bdd42d9c8d938006fd1a360 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Tue, 29 Apr 2025 13:16:10 +0100 Subject: [PATCH 5/9] feat: add other impl --- packages/firestore/lib/modular/snapshot.d.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/firestore/lib/modular/snapshot.d.ts b/packages/firestore/lib/modular/snapshot.d.ts index 4def1d0fa7..fb00198108 100644 --- a/packages/firestore/lib/modular/snapshot.d.ts +++ b/packages/firestore/lib/modular/snapshot.d.ts @@ -237,3 +237,18 @@ export declare function queryEqual void): Unsubscribe; + +/** + * /** + * Attaches a listener for a snapshots-in-sync event. + * The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if + * a single server-generated change affects multiple listeners. + * + * @param firestore + * @param onSync + */ +export declare function onSnapshotsInSync(firestore: Firestore, observer: { + next?: (value: void) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}): Unsubscribe; \ No newline at end of file From 598fd30e7549077ffd79ec5f504291934e489a3e Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Tue, 29 Apr 2025 13:38:08 +0100 Subject: [PATCH 6/9] feat: add test files --- .../onSnapshotsInSync.e2e.js | 36 +++++++++++++++++++ packages/firestore/lib/modular/snapshot.js | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js diff --git a/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js b/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js new file mode 100644 index 0000000000..eca14dcb0f --- /dev/null +++ b/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +const COLLECTION = 'firestore'; +const NO_RULE_COLLECTION = 'no_rules'; +const { wipe } = require('../helpers'); +import { onSnapshotsInSync } from '../../lib/modular/snapshot'; + +describe('firestore().doc().onSnapshot()', function () { + before(function () { + return wipe(); + }); + + describe('modular', function () { + it('onSnapshotsInSync() returns an unsubscribe function', function () { + const firestore = firebase.firestore(); + const unsubscribe = firestore.onSnapshotsInSync(function () {}); + + expect(unsubscribe).to.be.a('function'); + expect(unsubscribe()).to.equal(undefined); + }); + }); +}); diff --git a/packages/firestore/lib/modular/snapshot.js b/packages/firestore/lib/modular/snapshot.js index b3e35430bb..1d1c089578 100644 --- a/packages/firestore/lib/modular/snapshot.js +++ b/packages/firestore/lib/modular/snapshot.js @@ -20,5 +20,5 @@ export function snapshotEqual(left, right) { } export function onSnapshotsInSync(firestore, ...args) { - return firestore.onSnapshotsInSync.call(firestore, ...args, MODULAR_DEPRECATION_ARG); + return firestore.addSnapshotsInSyncListener.call(firestore, ...args, MODULAR_DEPRECATION_ARG); } From c5244189cc41a5f59bcbf124f017eec6073125b4 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Tue, 29 Apr 2025 13:44:37 +0100 Subject: [PATCH 7/9] feat: add test --- .../onSnapshotsInSync.e2e.js | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js b/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js index eca14dcb0f..fee9a09c49 100644 --- a/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js +++ b/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js @@ -27,10 +27,61 @@ describe('firestore().doc().onSnapshot()', function () { describe('modular', function () { it('onSnapshotsInSync() returns an unsubscribe function', function () { const firestore = firebase.firestore(); - const unsubscribe = firestore.onSnapshotsInSync(function () {}); + const unsubscribe = onSnapshotsInSync(firestore); expect(unsubscribe).to.be.a('function'); expect(unsubscribe()).to.equal(undefined); }); }); + + it('onSnapshotsInSync fires after listeners are in sync', () => { + const testDocs = { + a: { foo: 1 } + }; + return withTestCollection(persistence, testDocs, async (coll, db) => { + let events = []; + const gotInitialSnapshot = (() => { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + })(); + const docA = doc(coll, 'a'); + + onSnapshot(docA, snap => { + events.push('doc'); + gotInitialSnapshot.resolve(); + }); + await gotInitialSnapshot.promise; + events = []; + + const done = (() => { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + })(); + onSnapshotsInSync(db, () => { + events.push('snapshots-in-sync'); + if (events.length === 3) { + // We should have an initial snapshots-in-sync event, then a snapshot + // event for set(), then another event to indicate we're in sync + // again. + expect(events).to.deep.equal([ + 'snapshots-in-sync', + 'doc', + 'snapshots-in-sync' + ]); + done.resolve(); + } + }); + + await setDoc(docA, { foo: 3 }); + await done.promise; + }); + }); }); From 849b59a0ec9a79597ef3357d90b13e77724fbbc1 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Wed, 30 Apr 2025 10:09:55 +0100 Subject: [PATCH 8/9] feat: started native code --- ...NativeFirebaseFirestoreDocumentModule.java | 13 ++ .../onSnapshotsInSync.e2e.js | 121 ++++++++++-------- .../ios/RNFBFirestore/RNFBFirestoreModule.m | 8 ++ packages/firestore/lib/modular/snapshot.js | 17 ++- 4 files changed, 102 insertions(+), 57 deletions(-) diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java index 43c956cd9a..aadf2e84ba 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java @@ -341,4 +341,17 @@ private void sendOnSnapshotError( databaseId, listenerId)); } + + @ReactMethod + public void onSnapshotsInSync(Promise promise) { + ListenerRegistration registration = FirebaseFirestore.getInstance() + .addSnapshotsInSyncListener(() -> { + WritableMap result = Arguments.createMap(); + getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("onSnapshotsInSync", result); + }); + + promise.resolve(null); + } } diff --git a/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js b/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js index fee9a09c49..82d75857eb 100644 --- a/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js +++ b/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js @@ -24,64 +24,75 @@ describe('firestore().doc().onSnapshot()', function () { return wipe(); }); - describe('modular', function () { - it('onSnapshotsInSync() returns an unsubscribe function', function () { - const firestore = firebase.firestore(); - const unsubscribe = onSnapshotsInSync(firestore); - - expect(unsubscribe).to.be.a('function'); - expect(unsubscribe()).to.equal(undefined); - }); + it('onSnapshotsInSync() invokes callback and returns unsubscribe', async function () { + const firestore = firebase.firestore(); + const ref = firestore.collection(COLLECTION).doc('syncTest'); + + await ref.set({ foo: 'bar' }); + + const onSync = sinon.spy(); + + const unsubscribeSnapshot = ref.onSnapshot(() => {}); + const unsubscribeSync = onSnapshotsInSync(firestore, onSync); + + // Allow sync to process + await Utils.sleep(500); + + onSync.should.be.calledOnce(); + unsubscribeSync.should.be.a.Function(); + + unsubscribeSnapshot(); + unsubscribeSync(); }); - it('onSnapshotsInSync fires after listeners are in sync', () => { - const testDocs = { - a: { foo: 1 } - }; - return withTestCollection(persistence, testDocs, async (coll, db) => { - let events = []; - const gotInitialSnapshot = (() => { - let resolve, reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; - })(); - const docA = doc(coll, 'a'); + // it('onSnapshotsInSync fires after listeners are in sync', () => { + // const testDocs = { + // a: { foo: 1 } + // }; + // return withTestCollection(persistence, testDocs, async (coll, db) => { + // let events = []; + // const gotInitialSnapshot = (() => { + // let resolve, reject; + // const promise = new Promise((res, rej) => { + // resolve = res; + // reject = rej; + // }); + // return { promise, resolve, reject }; + // })(); + // const docA = doc(coll, 'a'); - onSnapshot(docA, snap => { - events.push('doc'); - gotInitialSnapshot.resolve(); - }); - await gotInitialSnapshot.promise; - events = []; + // onSnapshot(docA, snap => { + // events.push('doc'); + // gotInitialSnapshot.resolve(); + // }); + // await gotInitialSnapshot.promise; + // events = []; - const done = (() => { - let resolve, reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; - })(); - onSnapshotsInSync(db, () => { - events.push('snapshots-in-sync'); - if (events.length === 3) { - // We should have an initial snapshots-in-sync event, then a snapshot - // event for set(), then another event to indicate we're in sync - // again. - expect(events).to.deep.equal([ - 'snapshots-in-sync', - 'doc', - 'snapshots-in-sync' - ]); - done.resolve(); - } - }); + // const done = (() => { + // let resolve, reject; + // const promise = new Promise((res, rej) => { + // resolve = res; + // reject = rej; + // }); + // return { promise, resolve, reject }; + // })(); + // onSnapshotsInSync(db, () => { + // events.push('snapshots-in-sync'); + // if (events.length === 3) { + // // We should have an initial snapshots-in-sync event, then a snapshot + // // event for set(), then another event to indicate we're in sync + // // again. + // expect(events).to.deep.equal([ + // 'snapshots-in-sync', + // 'doc', + // 'snapshots-in-sync' + // ]); + // done.resolve(); + // } + // }); - await setDoc(docA, { foo: 3 }); - await done.promise; - }); - }); + // await setDoc(docA, { foo: 3 }); + // await done.promise; + // }); + // }); }); diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m index bdb1a1de48..e5197c6c64 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m @@ -240,6 +240,14 @@ + (BOOL)requiresMainQueueSetup { resolve(nil); } +RCT_EXPORT_METHOD(addSnapshotsInSyncListener:(RCTResponseSenderBlock)callback) { + FIRListenerRegistration *registration = [[FIRFirestore firestore] + addSnapshotsInSyncListener:^{ + callback(@[]); + }]; +} + + - (NSMutableDictionary *)taskProgressToDictionary:(FIRLoadBundleTaskProgress *)progress { NSMutableDictionary *progressMap = [[NSMutableDictionary alloc] init]; progressMap[@"bytesLoaded"] = @(progress.bytesLoaded); diff --git a/packages/firestore/lib/modular/snapshot.js b/packages/firestore/lib/modular/snapshot.js index 1d1c089578..3e0a11eeb1 100644 --- a/packages/firestore/lib/modular/snapshot.js +++ b/packages/firestore/lib/modular/snapshot.js @@ -5,6 +5,9 @@ */ import { MODULAR_DEPRECATION_ARG } from '../../../app/lib/common'; +import { NativeEventEmitter, NativeModules } from 'react-native'; + +const emitter = new NativeEventEmitter(NativeModules.RNFBFirestoreModule); /** * @param {Query | DocumentReference} reference @@ -19,6 +22,16 @@ export function snapshotEqual(left, right) { return left.isEqual.call(left, right, MODULAR_DEPRECATION_ARG); } -export function onSnapshotsInSync(firestore, ...args) { - return firestore.addSnapshotsInSyncListener.call(firestore, ...args, MODULAR_DEPRECATION_ARG); +// export function onSnapshotsInSync(firestore, ...args) { +// return firestore.addSnapshotsInSyncListener.call(firestore, ...args, MODULAR_DEPRECATION_ARG); +// } + +export function onSnapshotsInSync(_firestore, callback) { + console.log('Registering onSnapshotsInSync'); + if (typeof firestoreInstance.onSnapshotsInSync === 'function') { + return firestoreInstance.onSnapshotsInSync(callback); + } + + console.warn('onSnapshotsInSync is not implemented'); + return () => {}; } From 8fef582ddb8cbdce94e718cb4d8435624d1b9cf4 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Wed, 7 May 2025 11:05:09 +0100 Subject: [PATCH 9/9] feat: started working on listeners --- ...NativeFirebaseFirestoreDocumentModule.java | 13 -- .../ReactNativeFirebaseFirestoreModule.java | 58 ++++++++ .../onSnapshotsInSync.e2e.js | 126 +++++++----------- packages/firestore/lib/index.js | 67 ++++++++++ packages/firestore/lib/modular/snapshot.js | 14 -- 5 files changed, 173 insertions(+), 105 deletions(-) diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java index aadf2e84ba..43c956cd9a 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java @@ -341,17 +341,4 @@ private void sendOnSnapshotError( databaseId, listenerId)); } - - @ReactMethod - public void onSnapshotsInSync(Promise promise) { - ListenerRegistration registration = FirebaseFirestore.getInstance() - .addSnapshotsInSyncListener(() -> { - WritableMap result = Arguments.createMap(); - getReactApplicationContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("onSnapshotsInSync", result); - }); - - promise.resolve(null); - } } diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java index b8f5ceaab4..543f545dc5 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java @@ -21,6 +21,7 @@ import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreCommon.rejectPromiseFirestoreException; import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.createFirestoreKey; import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.getFirestoreForApp; +import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.getQueryForFirestore; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -193,6 +194,63 @@ public void persistenceCacheIndexManager( promise.resolve(null); } + @ReactMethod + public void snapshotsInSyncListener( + String appName, + String databaseId, + int listenerId + ) { + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); + ReactNativeFirebaseFirestoreQuery firestoreQuery = + new ReactNativeFirebaseFirestoreQuery( + appName, + databaseId, + getQueryForFirestore(firebaseFirestore) + ); + + handleSnapshotsInSync(appName, databaseId); + } + + @ReactMethod + public void onSnapshotsInSync(String appName, String databaseId, Promise promise) { + ListenerRegistration registration = FirebaseFirestore.getInstance() + .addSnapshotsInSyncListener(() -> { + WritableMap result = Arguments.createMap(); + getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("firestore_onSnapshotsInSync", result); + }); + + promise.resolve(null); + } + + private void handleSnapshotsInSync( + ReactNativeFirebaseFirestoreQuery firestoreQuery, + String appName, + String databaseId, + int listenerId + ) { + + final EventListener listener = + (querySnapshot, exception) -> { + if (exception != null) { + ListenerRegistration listenerRegistration = collectionSnapshotListeners.get(listenerId); + if (listenerRegistration != null) { + listenerRegistration.remove(); + collectionSnapshotListeners.remove(listenerId); + } + sendOnSnapshotError(appName, databaseId, listenerId, exception); + } else { + sendOnSnapshotEvent(appName, databaseId, listenerId, querySnapshot, metadataChanges); + } + }; + + ListenerRegistration listenerRegistration = + firestoreQuery.query.addSnapshotListener(metadataChanges, listener); + + collectionSnapshotListeners.put(listenerId, listenerRegistration); + } + private WritableMap taskProgressToWritableMap(LoadBundleTaskProgress progress) { WritableMap writableMap = Arguments.createMap(); writableMap.putDouble("bytesLoaded", progress.getBytesLoaded()); diff --git a/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js b/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js index 82d75857eb..b9c41c7e58 100644 --- a/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js +++ b/packages/firestore/e2e/DocumentReference/onSnapshotsInSync.e2e.js @@ -14,85 +14,55 @@ * limitations under the License. * */ -const COLLECTION = 'firestore'; -const NO_RULE_COLLECTION = 'no_rules'; -const { wipe } = require('../helpers'); -import { onSnapshotsInSync } from '../../lib/modular/snapshot'; - -describe('firestore().doc().onSnapshot()', function () { - before(function () { - return wipe(); - }); - it('onSnapshotsInSync() invokes callback and returns unsubscribe', async function () { - const firestore = firebase.firestore(); - const ref = firestore.collection(COLLECTION).doc('syncTest'); - - await ref.set({ foo: 'bar' }); - - const onSync = sinon.spy(); - - const unsubscribeSnapshot = ref.onSnapshot(() => {}); - const unsubscribeSync = onSnapshotsInSync(firestore, onSync); - - // Allow sync to process - await Utils.sleep(500); - - onSync.should.be.calledOnce(); - unsubscribeSync.should.be.a.Function(); - - unsubscribeSnapshot(); - unsubscribeSync(); +describe('firestore().onSnapshotsInSync() modular', function () { + if (Platform.other) return; + + const { getFirestore, doc, onSnapshot, onSnapshotsInSync, setDoc } = firestoreModular; + const TEST_DOC = `${COLLECTION}/modular_sync_test`; + + it('calls onSnapshotsInSync after a snapshot is delivered', async function () { + const db = getFirestore(); + const docRef = doc(db, TEST_DOC); + + const snapshotSpy = sinon.spy(); + const syncSpy = sinon.spy(); + + // Set initial data + await setDoc(docRef, { foo: 'initial' }); + + // Subscribe to snapshot and sync + const unsubSnapshot = onSnapshot(docRef, snapshotSpy); + const unsubSync = onSnapshotsInSync(db, syncSpy); + + // Trigger change + await setDoc(docRef, { foo: 'updated' }); + + // Wait for both + await Utils.spyToBeCalledOnceAsync(snapshotSpy); + await Utils.spyToBeCalledOnceAsync(syncSpy); + + snapshotSpy.should.be.calledOnce(); + syncSpy.should.be.calledOnce(); + + unsubSnapshot(); + unsubSync(); }); - // it('onSnapshotsInSync fires after listeners are in sync', () => { - // const testDocs = { - // a: { foo: 1 } - // }; - // return withTestCollection(persistence, testDocs, async (coll, db) => { - // let events = []; - // const gotInitialSnapshot = (() => { - // let resolve, reject; - // const promise = new Promise((res, rej) => { - // resolve = res; - // reject = rej; - // }); - // return { promise, resolve, reject }; - // })(); - // const docA = doc(coll, 'a'); - - // onSnapshot(docA, snap => { - // events.push('doc'); - // gotInitialSnapshot.resolve(); - // }); - // await gotInitialSnapshot.promise; - // events = []; - - // const done = (() => { - // let resolve, reject; - // const promise = new Promise((res, rej) => { - // resolve = res; - // reject = rej; - // }); - // return { promise, resolve, reject }; - // })(); - // onSnapshotsInSync(db, () => { - // events.push('snapshots-in-sync'); - // if (events.length === 3) { - // // We should have an initial snapshots-in-sync event, then a snapshot - // // event for set(), then another event to indicate we're in sync - // // again. - // expect(events).to.deep.equal([ - // 'snapshots-in-sync', - // 'doc', - // 'snapshots-in-sync' - // ]); - // done.resolve(); - // } - // }); - - // await setDoc(docA, { foo: 3 }); - // await done.promise; - // }); - // }); + it('should not fire sync after unsubscribe', async function () { + const db = getFirestore(); + const syncSpy = sinon.spy(); + + const unsubSync = onSnapshotsInSync(db, syncSpy); + unsubSync(); + + await Utils.sleep(1000); + + await setDoc(doc(db, `${COLLECTION}/modular_sync_test_unsub`), { foo: 'change' }); + + await Utils.sleep(1000); + + syncSpy.should.not.be.called(); + }); }); + diff --git a/packages/firestore/lib/index.js b/packages/firestore/lib/index.js index e95cb29713..a1a4fb438a 100644 --- a/packages/firestore/lib/index.js +++ b/packages/firestore/lib/index.js @@ -56,6 +56,7 @@ const nativeEvents = [ 'firestore_collection_sync_event', 'firestore_document_sync_event', 'firestore_transaction_event', + 'firestore_snapshots_in_sync_event', ]; class FirebaseFirestoreModule extends FirebaseModule { @@ -84,6 +85,13 @@ class FirebaseFirestoreModule extends FirebaseModule { ); }); + this.emitter.addListener(this.eventNameForApp('firestore_snapshots_in_sync_event'), event => { + this.emitter.emit( + this.eventNameForApp(`firestore_snapshots_in_sync_event:${event.listenerId}`), + event, + ); + }); + this._settings = { ignoreUndefinedProperties: false, persistence: true, @@ -134,6 +142,65 @@ class FirebaseFirestoreModule extends FirebaseModule { await this.native.terminate(); } + onSnapshot(...args) { + let snapshotListenOptions; + let callback; + let onNext; + let onError; + + try { + const options = parseSnapshotArgs(args); + snapshotListenOptions = options.snapshotListenOptions; + callback = options.callback; + onNext = options.onNext; + onError = options.onError; + } catch (e) { + throw new Error(`firebase.firestore().doc().onSnapshot(*) ${e.message}`); + } + + function handleSuccess(documentSnapshot) { + callback(documentSnapshot, null); + onNext(documentSnapshot); + } + + function handleError(error) { + callback(null, error); + onError(error); + } + + const listenerId = _id++; + + const onSnapshotSubscription = this._firestore.emitter.addListener( + this._firestore.eventNameForApp(`firestore_snapshots_in_sync_event:${listenerId}`), + event => { + if (event.body.error) { + handleError(NativeError.fromEvent(event.body.error, 'firestore')); + } else { + const documentSnapshot = createDeprecationProxy( + new FirestoreDocumentSnapshot(this._firestore, event.body.snapshot), + ); + handleSuccess(documentSnapshot); + } + }, + ); + + const unsubscribe = () => { + onSnapshotSubscription.remove(); + this._firestore.native.documentOffSnapshot(listenerId); + }; + + this.firestore.native.snapshotsInSyncListener(listenerId); + + return unsubscribe; + } + + async onSnapshotsInSync(firestore, callback) { + this.native.onSnapshotsInSync(firestore, callback, MODULAR_DEPRECATION_ARG); + return () => { + firestoreEmitter.removeListener('onSnapshotsInSync', callback); + }; + } + useEmulator(host, port) { if (!host || !isString(host) || !port || !isNumber(port)) { throw new Error('firebase.firestore().useEmulator() takes a non-empty host and port'); diff --git a/packages/firestore/lib/modular/snapshot.js b/packages/firestore/lib/modular/snapshot.js index 3e0a11eeb1..c143e5dfe3 100644 --- a/packages/firestore/lib/modular/snapshot.js +++ b/packages/firestore/lib/modular/snapshot.js @@ -21,17 +21,3 @@ export function onSnapshot(reference, ...args) { export function snapshotEqual(left, right) { return left.isEqual.call(left, right, MODULAR_DEPRECATION_ARG); } - -// export function onSnapshotsInSync(firestore, ...args) { -// return firestore.addSnapshotsInSyncListener.call(firestore, ...args, MODULAR_DEPRECATION_ARG); -// } - -export function onSnapshotsInSync(_firestore, callback) { - console.log('Registering onSnapshotsInSync'); - if (typeof firestoreInstance.onSnapshotsInSync === 'function') { - return firestoreInstance.onSnapshotsInSync(callback); - } - - console.warn('onSnapshotsInSync is not implemented'); - return () => {}; -}