From 03abbeda62d91fe2e6a344ff54bc37d79ff5c157 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Fri, 10 Feb 2017 13:12:52 +1000 Subject: [PATCH 1/2] fix(database): retrieve initial list content once Closes #819 --- src/database/firebase_list_factory.ts | 131 +++++++++++--------------- 1 file changed, 57 insertions(+), 74 deletions(-) diff --git a/src/database/firebase_list_factory.ts b/src/database/firebase_list_factory.ts index d50491a2d..626117c5a 100644 --- a/src/database/firebase_list_factory.ts +++ b/src/database/firebase_list_factory.ts @@ -108,88 +108,71 @@ export function FirebaseListFactory ( * is loaded, the observable starts emitting values. */ function firebaseListObservable(ref: firebase.database.Reference | firebase.database.Query, {preserveSnapshot}: FirebaseListFactoryOpts = {}): FirebaseListObservable { + const toValue = preserveSnapshot ? (snapshot => snapshot) : utils.unwrapMapFn; const toKey = preserveSnapshot ? (value => value.key) : (value => value.$key); - // Keep track of callback handles for calling ref.off(event, handle) - const handles = []; + const listObs = new FirebaseListObservable(ref, (obs: Observer) => { - ref.once('value') - .then((snap) => { - let initialArray = []; - snap.forEach(child => { - initialArray.push(toValue(child)) - }); - return initialArray; - }) - .then((initialArray) => { - const isInitiallyEmpty = initialArray.length === 0; - let hasInitialLoad = false; - let lastKey; - - if (!isInitiallyEmpty) { - // The last key in the initial array tells us where - // to begin listening in realtime - lastKey = toKey(initialArray[initialArray.length - 1]); - } - const addFn = ref.on('child_added', (child: any, prevKey: string) => { - // If the initial load has not been set and the current key is - // the last key of the initialArray, we know we have hit the - // initial load - if (!isInitiallyEmpty && !hasInitialLoad) { - if (child.key === lastKey) { - hasInitialLoad = true; - obs.next(initialArray); - return; - } - } - - if (hasInitialLoad) { - initialArray = onChildAdded(initialArray, toValue(child), toKey, prevKey); - } - - // only emit the array after the initial load - if (hasInitialLoad) { - obs.next(initialArray); - } - }, err => { - if (err) { obs.error(err); obs.complete(); } - }); + // Keep track of callback handles for calling ref.off(event, handle) + const handles = []; + let hasLoaded = false; + let lastLoadedKey: string = null; + let array = []; - handles.push({ event: 'child_added', handle: addFn }); + // The list children are always added to, removed from and changed within + // the array using the child_added/removed/changed events. The value event + // is only used to determine when the initial load is complete. - let remFn = ref.on('child_removed', (child: any) => { - initialArray = onChildRemoved(initialArray, toValue(child), toKey); - if (hasInitialLoad) { - obs.next(initialArray); - } - }, err => { - if (err) { obs.error(err); obs.complete(); } - }); - handles.push({ event: 'child_removed', handle: remFn }); - - let chgFn = ref.on('child_changed', (child: any, prevKey: string) => { - initialArray = onChildChanged(initialArray, toValue(child), toKey, prevKey) - if (hasInitialLoad) { - // This also manages when the only change is prevKey change - obs.next(initialArray); - } - }, err => { - if (err) { obs.error(err); obs.complete(); } + ref.once('value', (snap: any) => { + if (snap.exists()) { + snap.forEach((child: any) => { + lastLoadedKey = child.key; }); - handles.push({ event: 'child_changed', handle: chgFn }); - - // If empty emit the array - if (isInitiallyEmpty) { - obs.next(initialArray); - hasInitialLoad = true; + if (array.find((child: any) => toKey(child) === lastLoadedKey)) { + hasLoaded = true; + obs.next(array); } - }, err => { - if (err) { - obs.error(err); - obs.complete(); - } - }); + } else { + hasLoaded = true; + obs.next(array); + } + }, err => { + if (err) { obs.error(err); obs.complete(); } + }); + + const addFn = ref.on('child_added', (child: any, prevKey: string) => { + array = onChildAdded(array, toValue(child), toKey, prevKey); + if (hasLoaded) { + obs.next(array); + } else if (child.key === lastLoadedKey) { + hasLoaded = true; + obs.next(array); + } + }, err => { + if (err) { obs.error(err); obs.complete(); } + }); + handles.push({ event: 'child_added', handle: addFn }); + + let remFn = ref.on('child_removed', (child: any) => { + array = onChildRemoved(array, toValue(child), toKey); + if (hasLoaded) { + obs.next(array); + } + }, err => { + if (err) { obs.error(err); obs.complete(); } + }); + handles.push({ event: 'child_removed', handle: remFn }); + + let chgFn = ref.on('child_changed', (child: any, prevKey: string) => { + array = onChildChanged(array, toValue(child), toKey, prevKey); + if (hasLoaded) { + obs.next(array); + } + }, err => { + if (err) { obs.error(err); obs.complete(); } + }); + handles.push({ event: 'child_changed', handle: chgFn }); return () => { // Loop through callback handles and dispose of each event with handle From b0759b2461238afd3ff469eb545735ab51ac2c6e Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 11 Feb 2017 16:48:45 +1000 Subject: [PATCH 2/2] test(database): add a child_added quirk test --- src/database/firebase_list_factory.spec.ts | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/database/firebase_list_factory.spec.ts b/src/database/firebase_list_factory.spec.ts index 5fda83fc3..c3c44d141 100644 --- a/src/database/firebase_list_factory.spec.ts +++ b/src/database/firebase_list_factory.spec.ts @@ -391,6 +391,32 @@ describe('FirebaseListFactory', () => { }); + it('should be resistant to non-asynchronous child_added quirks', (done: any) => { + + // If push is called (or set or update, too, I guess) immediately after + // an on or once listener is added, it appears that the on or once + // child_added listeners are invoked immediately - i.e. not + // asynchronously - and the list implementation needs to support that. + + questions.$ref.ref.push({ number: 1 }) + .then(() => { + let calls = []; + questions.$ref.ref.once('child_added', (snap) => calls.push('child_added:' + snap.val().number)); + skipAndTake(questions).subscribe( + (list) => { + expect(calls).toEqual(['child_added:2', 'pushed']); + expect(list.map(i => i.number)).toEqual([1, 2]); + done(); + }, + done.fail + ); + questions.push({ number: 2 }); + calls.push('pushed'); + }) + .catch(done.fail); + }); + + it('should emit a new value when a child moves', (done: any) => { let question = skipAndTake(questions, 1, 2) subscription = _do.call(question, (data: any) => {