Skip to content

Commit 5c5ff7b

Browse files
cartantdavideast
authored andcommitted
fix(database): retrieve initial list content once (#820)
* fix(database): retrieve initial list content once Closes #819 * test(database): add a child_added quirk test
1 parent 561e7b7 commit 5c5ff7b

File tree

2 files changed

+83
-74
lines changed

2 files changed

+83
-74
lines changed

src/database/firebase_list_factory.spec.ts

+26
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,32 @@ describe('FirebaseListFactory', () => {
391391
});
392392

393393

394+
it('should be resistant to non-asynchronous child_added quirks', (done: any) => {
395+
396+
// If push is called (or set or update, too, I guess) immediately after
397+
// an on or once listener is added, it appears that the on or once
398+
// child_added listeners are invoked immediately - i.e. not
399+
// asynchronously - and the list implementation needs to support that.
400+
401+
questions.$ref.ref.push({ number: 1 })
402+
.then(() => {
403+
let calls = [];
404+
questions.$ref.ref.once('child_added', (snap) => calls.push('child_added:' + snap.val().number));
405+
skipAndTake(questions).subscribe(
406+
(list) => {
407+
expect(calls).toEqual(['child_added:2', 'pushed']);
408+
expect(list.map(i => i.number)).toEqual([1, 2]);
409+
done();
410+
},
411+
done.fail
412+
);
413+
questions.push({ number: 2 });
414+
calls.push('pushed');
415+
})
416+
.catch(done.fail);
417+
});
418+
419+
394420
it('should emit a new value when a child moves', (done: any) => {
395421
let question = skipAndTake(questions, 1, 2)
396422
subscription = _do.call(question, (data: any) => {

src/database/firebase_list_factory.ts

+57-74
Original file line numberDiff line numberDiff line change
@@ -108,88 +108,71 @@ export function FirebaseListFactory (
108108
* is loaded, the observable starts emitting values.
109109
*/
110110
function firebaseListObservable(ref: firebase.database.Reference | firebase.database.Query, {preserveSnapshot}: FirebaseListFactoryOpts = {}): FirebaseListObservable<any> {
111+
111112
const toValue = preserveSnapshot ? (snapshot => snapshot) : utils.unwrapMapFn;
112113
const toKey = preserveSnapshot ? (value => value.key) : (value => value.$key);
113-
// Keep track of callback handles for calling ref.off(event, handle)
114-
const handles = [];
114+
115115
const listObs = new FirebaseListObservable(ref, (obs: Observer<any[]>) => {
116-
ref.once('value')
117-
.then((snap) => {
118-
let initialArray = [];
119-
snap.forEach(child => {
120-
initialArray.push(toValue(child))
121-
});
122-
return initialArray;
123-
})
124-
.then((initialArray) => {
125-
const isInitiallyEmpty = initialArray.length === 0;
126-
let hasInitialLoad = false;
127-
let lastKey;
128-
129-
if (!isInitiallyEmpty) {
130-
// The last key in the initial array tells us where
131-
// to begin listening in realtime
132-
lastKey = toKey(initialArray[initialArray.length - 1]);
133-
}
134116

135-
const addFn = ref.on('child_added', (child: any, prevKey: string) => {
136-
// If the initial load has not been set and the current key is
137-
// the last key of the initialArray, we know we have hit the
138-
// initial load
139-
if (!isInitiallyEmpty && !hasInitialLoad) {
140-
if (child.key === lastKey) {
141-
hasInitialLoad = true;
142-
obs.next(initialArray);
143-
return;
144-
}
145-
}
146-
147-
if (hasInitialLoad) {
148-
initialArray = onChildAdded(initialArray, toValue(child), toKey, prevKey);
149-
}
150-
151-
// only emit the array after the initial load
152-
if (hasInitialLoad) {
153-
obs.next(initialArray);
154-
}
155-
}, err => {
156-
if (err) { obs.error(err); obs.complete(); }
157-
});
117+
// Keep track of callback handles for calling ref.off(event, handle)
118+
const handles = [];
119+
let hasLoaded = false;
120+
let lastLoadedKey: string = null;
121+
let array = [];
158122

159-
handles.push({ event: 'child_added', handle: addFn });
123+
// The list children are always added to, removed from and changed within
124+
// the array using the child_added/removed/changed events. The value event
125+
// is only used to determine when the initial load is complete.
160126

161-
let remFn = ref.on('child_removed', (child: any) => {
162-
initialArray = onChildRemoved(initialArray, toValue(child), toKey);
163-
if (hasInitialLoad) {
164-
obs.next(initialArray);
165-
}
166-
}, err => {
167-
if (err) { obs.error(err); obs.complete(); }
168-
});
169-
handles.push({ event: 'child_removed', handle: remFn });
170-
171-
let chgFn = ref.on('child_changed', (child: any, prevKey: string) => {
172-
initialArray = onChildChanged(initialArray, toValue(child), toKey, prevKey)
173-
if (hasInitialLoad) {
174-
// This also manages when the only change is prevKey change
175-
obs.next(initialArray);
176-
}
177-
}, err => {
178-
if (err) { obs.error(err); obs.complete(); }
127+
ref.once('value', (snap: any) => {
128+
if (snap.exists()) {
129+
snap.forEach((child: any) => {
130+
lastLoadedKey = child.key;
179131
});
180-
handles.push({ event: 'child_changed', handle: chgFn });
181-
182-
// If empty emit the array
183-
if (isInitiallyEmpty) {
184-
obs.next(initialArray);
185-
hasInitialLoad = true;
132+
if (array.find((child: any) => toKey(child) === lastLoadedKey)) {
133+
hasLoaded = true;
134+
obs.next(array);
186135
}
187-
}, err => {
188-
if (err) {
189-
obs.error(err);
190-
obs.complete();
191-
}
192-
});
136+
} else {
137+
hasLoaded = true;
138+
obs.next(array);
139+
}
140+
}, err => {
141+
if (err) { obs.error(err); obs.complete(); }
142+
});
143+
144+
const addFn = ref.on('child_added', (child: any, prevKey: string) => {
145+
array = onChildAdded(array, toValue(child), toKey, prevKey);
146+
if (hasLoaded) {
147+
obs.next(array);
148+
} else if (child.key === lastLoadedKey) {
149+
hasLoaded = true;
150+
obs.next(array);
151+
}
152+
}, err => {
153+
if (err) { obs.error(err); obs.complete(); }
154+
});
155+
handles.push({ event: 'child_added', handle: addFn });
156+
157+
let remFn = ref.on('child_removed', (child: any) => {
158+
array = onChildRemoved(array, toValue(child), toKey);
159+
if (hasLoaded) {
160+
obs.next(array);
161+
}
162+
}, err => {
163+
if (err) { obs.error(err); obs.complete(); }
164+
});
165+
handles.push({ event: 'child_removed', handle: remFn });
166+
167+
let chgFn = ref.on('child_changed', (child: any, prevKey: string) => {
168+
array = onChildChanged(array, toValue(child), toKey, prevKey);
169+
if (hasLoaded) {
170+
obs.next(array);
171+
}
172+
}, err => {
173+
if (err) { obs.error(err); obs.complete(); }
174+
});
175+
handles.push({ event: 'child_changed', handle: chgFn });
193176

194177
return () => {
195178
// Loop through callback handles and dispose of each event with handle

0 commit comments

Comments
 (0)