@@ -100,58 +100,99 @@ export function FirebaseListFactory (
100100 } ) ;
101101}
102102
103+ /**
104+ * Creates a FirebaseListObservable from a reference or query. Options can be provided as a second parameter.
105+ * This function understands the nuances of the Firebase SDK event ordering and other quirks. This function
106+ * takes into account that not all .on() callbacks are guaranteed to be asynchonous. It creates a initial array
107+ * from a promise of ref.once('value'), and then starts listening to child events. When the initial array
108+ * is loaded, the observable starts emitting values.
109+ */
103110function firebaseListObservable ( ref : firebase . database . Reference | firebase . database . Query , { preserveSnapshot} : FirebaseListFactoryOpts = { } ) : FirebaseListObservable < any > {
104-
111+ // Keep track of callback handles for calling ref.off(event, handle)
112+ const handles = [ ] ;
105113 const listObs = new FirebaseListObservable ( ref , ( obs : Observer < any [ ] > ) => {
106- let arr : any [ ] = [ ] ;
107- let hasInitialLoad = false ;
108- // The list should only emit after the initial load
109- // comes down from the Firebase database, (e.g.) all
110- // the initial child_added events have fired.
111- // This way a complete array is emitted which leads
112- // to better rendering performance
113- ref . once ( 'value' , ( snap ) => {
114- hasInitialLoad = true ;
115- obs . next ( preserveSnapshot ? arr : arr . map ( utils . unwrapMapFn ) ) ;
116- } ) . catch ( err => {
117- obs . error ( err ) ;
118- obs . complete ( )
119- } ) ;
120-
121- let addFn = ref . on ( 'child_added' , ( child : any , prevKey : string ) => {
122- arr = onChildAdded ( arr , child , prevKey ) ;
123- // only emit the array after the initial load
124- if ( hasInitialLoad ) {
125- obs . next ( preserveSnapshot ? arr : arr . map ( utils . unwrapMapFn ) ) ;
126- }
127- } , err => {
128- if ( err ) { obs . error ( err ) ; obs . complete ( ) ; }
129- } ) ;
130-
131- let remFn = ref . on ( 'child_removed' , ( child : any ) => {
132- arr = onChildRemoved ( arr , child )
133- if ( hasInitialLoad ) {
134- obs . next ( preserveSnapshot ? arr : arr . map ( utils . unwrapMapFn ) ) ;
135- }
136- } , err => {
137- if ( err ) { obs . error ( err ) ; obs . complete ( ) ; }
138- } ) ;
139-
140- let chgFn = ref . on ( 'child_changed' , ( child : any , prevKey : string ) => {
141- arr = onChildChanged ( arr , child , prevKey )
142- if ( hasInitialLoad ) {
143- // This also manages when the only change is prevKey change
144- obs . next ( preserveSnapshot ? arr : arr . map ( utils . unwrapMapFn ) ) ;
145- }
146- } , err => {
147- if ( err ) { obs . error ( err ) ; obs . complete ( ) ; }
148- } ) ;
114+ ref . once ( 'value' )
115+ . then ( ( snap ) => {
116+ let initialArray = [ ] ;
117+ snap . forEach ( child => {
118+ initialArray . push ( child )
119+ } ) ;
120+ return initialArray ;
121+ } )
122+ . then ( ( initialArray ) => {
123+ const isInitiallyEmpty = initialArray . length === 0 ;
124+ let hasInitialLoad = false ;
125+ let lastKey ;
126+
127+ if ( ! isInitiallyEmpty ) {
128+ // The last key in the initial array tells us where
129+ // to begin listening in realtime
130+ lastKey = initialArray [ initialArray . length - 1 ] . key ;
131+ }
132+
133+ const addFn = ref . on ( 'child_added' , ( child : any , prevKey : string ) => {
134+ // If the initial load has not been set and the current key is
135+ // the last key of the initialArray, we know we have hit the
136+ // initial load
137+ if ( ! isInitiallyEmpty ) {
138+ if ( child . key === lastKey ) {
139+ hasInitialLoad = true ;
140+ obs . next ( preserveSnapshot ? initialArray : initialArray . map ( utils . unwrapMapFn ) ) ;
141+ return ;
142+ }
143+ }
144+
145+ if ( hasInitialLoad ) {
146+ initialArray = onChildAdded ( initialArray , child , prevKey ) ;
147+ }
148+
149+ // only emit the array after the initial load
150+ if ( hasInitialLoad ) {
151+ obs . next ( preserveSnapshot ? initialArray : initialArray . map ( utils . unwrapMapFn ) ) ;
152+ }
153+ } , err => {
154+ if ( err ) { obs . error ( err ) ; obs . complete ( ) ; }
155+ } ) ;
156+
157+ handles . push ( { event : 'child_added' , handle : addFn } ) ;
158+
159+ let remFn = ref . on ( 'child_removed' , ( child : any ) => {
160+ initialArray = onChildRemoved ( initialArray , child )
161+ if ( hasInitialLoad ) {
162+ obs . next ( preserveSnapshot ? initialArray : initialArray . map ( utils . unwrapMapFn ) ) ;
163+ }
164+ } , err => {
165+ if ( err ) { obs . error ( err ) ; obs . complete ( ) ; }
166+ } ) ;
167+ handles . push ( { event : 'child_removed' , handle : remFn } ) ;
168+
169+ let chgFn = ref . on ( 'child_changed' , ( child : any , prevKey : string ) => {
170+ initialArray = onChildChanged ( initialArray , child , prevKey )
171+ if ( hasInitialLoad ) {
172+ // This also manages when the only change is prevKey change
173+ obs . next ( preserveSnapshot ? initialArray : initialArray . map ( utils . unwrapMapFn ) ) ;
174+ }
175+ } , err => {
176+ if ( err ) { obs . error ( err ) ; obs . complete ( ) ; }
177+ } ) ;
178+ handles . push ( { event : 'child_changed' , handle : chgFn } ) ;
179+
180+ // If empty emit the array
181+ if ( isInitiallyEmpty ) {
182+ obs . next ( initialArray ) ;
183+ hasInitialLoad = true ;
184+ }
185+ } ) ;
149186
150187 return ( ) => {
151- ref . off ( 'child_added' , addFn ) ;
152- ref . off ( 'child_removed' , remFn ) ;
153- ref . off ( 'child_changed' , chgFn ) ;
154- }
188+ // Loop through callback handles and dispose of each event with handle
189+ // The Firebase SDK requires the reference, event name, and callback to
190+ // properly unsubscribe, otherwise it can affect other subscriptions.
191+ handles . forEach ( item => {
192+ ref . off ( item . event , item . handle ) ;
193+ } ) ;
194+ } ;
195+
155196 } ) ;
156197
157198 // TODO: should be in the subscription zone instead
0 commit comments