Skip to content

Commit

Permalink
fix(database): retrieve initial list content once (#820)
Browse files Browse the repository at this point in the history
* fix(database): retrieve initial list content once

Closes #819

* test(database): add a child_added quirk test
  • Loading branch information
cartant authored and davideast committed Feb 15, 2017
1 parent 561e7b7 commit 5c5ff7b
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 74 deletions.
26 changes: 26 additions & 0 deletions src/database/firebase_list_factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
131 changes: 57 additions & 74 deletions src/database/firebase_list_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {

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<any[]>) => {
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
Expand Down

0 comments on commit 5c5ff7b

Please sign in to comment.