@@ -100,58 +100,99 @@ export function FirebaseListFactory (
100
100
} ) ;
101
101
}
102
102
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
+ */
103
110
function 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 = [ ] ;
105
113
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
+ } ) ;
149
186
150
187
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
+
155
196
} ) ;
156
197
157
198
// TODO: should be in the subscription zone instead
0 commit comments