@@ -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