Skip to content

Commit 9f3c47b

Browse files
cartantdavideast
authored andcommitted
fix(database): store unwrapped snapshots
Store either unwrapped snapshots or preserved snapshots; don't store preserved snapshots and call utils.unwrapMapFn when emitting. Closes #791
1 parent 57fcc6b commit 9f3c47b

File tree

2 files changed

+87
-48
lines changed

2 files changed

+87
-48
lines changed

src/database/firebase_list_factory.spec.ts

+63-28
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,7 @@ function queryTest(observable: Observable<any>, subject: Subject<any>, done: any
4949
}
5050

5151
describe('FirebaseListFactory', () => {
52-
var subscription: Subscription;
53-
var questions: FirebaseListObservable<any>;
54-
var questionsSnapshotted: FirebaseListObservable<any>;
55-
var ref: any;
56-
var refSnapshotted: any;
57-
var val1: any;
58-
var val2: any;
59-
var val3: any;
52+
6053
var app: firebase.app.App;
6154

6255
beforeEach(() => {
@@ -351,13 +344,22 @@ describe('FirebaseListFactory', () => {
351344
expect(observable.update instanceof Function).toBe(true);
352345
expect(observable.remove instanceof Function).toBe(true);
353346
});
354-
355-
356347
});
357348

358349
describe('methods', () => {
359350

351+
var toKey;
352+
var val1: any;
353+
var val2: any;
354+
var val3: any;
355+
var questions: FirebaseListObservable<any>;
356+
var questionsSnapshotted: FirebaseListObservable<any>;
357+
var ref: any;
358+
var refSnapshotted: any;
359+
var subscription: Subscription;
360+
360361
beforeEach((done: any) => {
362+
toKey = (val) => val.key;
361363
val1 = { key: 'key1' };
362364
val2 = { key: 'key2' };
363365
val3 = { key: 'key3' };
@@ -368,6 +370,7 @@ describe('FirebaseListFactory', () => {
368370
refSnapshotted = questionsSnapshotted.$ref;
369371
});
370372

373+
371374
afterEach((done: any) => {
372375
if (subscription && !subscription.closed) {
373376
subscription.unsubscribe();
@@ -434,6 +437,46 @@ describe('FirebaseListFactory', () => {
434437
});
435438

436439

440+
it('should re-emit identical instances of unchanged children', (done: any) => {
441+
442+
let prev;
443+
444+
take.call(questions, 2).subscribe(
445+
(list) => {
446+
if (prev) {
447+
expect(list[0]).toBe(prev[0]);
448+
done();
449+
} else {
450+
prev = list;
451+
questions.push({ name: 'bob' });
452+
}
453+
},
454+
done.fail
455+
);
456+
questions.push({ name: 'alice' });
457+
});
458+
459+
460+
it('should re-emit identical instances of unchanged children as snapshots', (done: any) => {
461+
462+
let prev;
463+
464+
take.call(questionsSnapshotted, 2).subscribe(
465+
(list) => {
466+
if (prev) {
467+
expect(list[0]).toBe(prev[0]);
468+
done();
469+
} else {
470+
prev = list;
471+
questionsSnapshotted.push({ name: 'bob' });
472+
}
473+
},
474+
done.fail
475+
);
476+
questionsSnapshotted.push({ name: 'alice' });
477+
});
478+
479+
437480
it('should call off on all events when disposed', (done: any) => {
438481
const questionRef = firebase.database().ref().child('questions');
439482
var firebaseSpy = spyOn(questionRef, 'off').and.callThrough();
@@ -447,65 +490,57 @@ describe('FirebaseListFactory', () => {
447490

448491

449492
describe('onChildAdded', () => {
493+
450494
it('should add the child after the prevKey', () => {
451-
expect(onChildAdded([val1, val2], val3, 'key1')).toEqual([val1, val3, val2]);
495+
expect(onChildAdded([val1, val2], val3, toKey, 'key1')).toEqual([val1, val3, val2]);
452496
});
453497

454498

455499
it('should not mutate the input array', () => {
456500
var inputArr = [val1];
457-
expect(onChildAdded(inputArr, val2, 'key1')).not.toEqual(inputArr);
501+
expect(onChildAdded(inputArr, val2, toKey, 'key1')).not.toEqual(inputArr);
458502
});
459503
});
460504

461505

462506
describe('onChildChanged', () => {
507+
463508
it('should move the child after the specified prevKey', () => {
464-
expect(onChildChanged([val1, val2], val1, 'key2')).toEqual([val2, val1]);
509+
expect(onChildChanged([val1, val2], val1, toKey, 'key2')).toEqual([val2, val1]);
465510
});
466511

467512

468513
it('should move the child to the beginning if prevKey is null', () => {
469514
expect(
470-
onChildChanged([val1, val2, val3], val2, null)
515+
onChildChanged([val1, val2, val3], val2, toKey, null)
471516
).toEqual([val2, val1, val3]);
472517
});
473518

474519
it('should not duplicate the first item if it is the one that changed', () => {
475520
expect(
476-
onChildChanged([val1, val2, val3], val1, null)
521+
onChildChanged([val1, val2, val3], val1, toKey, null)
477522
).not.toEqual([val1, val1, val2, val3]);
478523
});
479524

480525
it('should not mutate the input array', () => {
481526
var inputArr = [val1, val2];
482-
expect(onChildChanged(inputArr, val1, 'key2')).not.toEqual(inputArr);
527+
expect(onChildChanged(inputArr, val1, toKey, 'key2')).not.toEqual(inputArr);
483528
});
484529

485530

486531
it('should update the child', () => {
487532
expect(
488-
onChildUpdated([val1, val2, val3], { key: 'newkey' }, 'key1').map(v => v.key)
533+
onChildUpdated([val1, val2, val3], { key: 'newkey' }, toKey, 'key1').map(v => v.key)
489534
).toEqual(['key1', 'newkey', 'key3']);
490535
});
491536
});
492537

493538

494539
describe('onChildRemoved', () => {
495-
var val1: any;
496-
var val2: any;
497-
var val3: any;
498-
499-
beforeEach(() => {
500-
val1 = { key: () => 'key1' };
501-
val2 = { key: () => 'key2' };
502-
val3 = { key: () => 'key3' };
503-
});
504-
505540

506541
it('should remove the child', () => {
507542
expect(
508-
onChildRemoved([val1, val2, val3], val2)
543+
onChildRemoved([val1, val2, val3], val2, toKey)
509544
).toEqual([val1, val3]);
510545
});
511546
});

src/database/firebase_list_factory.ts

+24-20
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,16 @@ 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+
const toValue = preserveSnapshot ? (snapshot => snapshot) : utils.unwrapMapFn;
112+
const toKey = preserveSnapshot ? (value => value.key) : (value => value.$key);
111113
// Keep track of callback handles for calling ref.off(event, handle)
112114
const handles = [];
113115
const listObs = new FirebaseListObservable(ref, (obs: Observer<any[]>) => {
114116
ref.once('value')
115117
.then((snap) => {
116118
let initialArray = [];
117119
snap.forEach(child => {
118-
initialArray.push(child)
120+
initialArray.push(toValue(child))
119121
});
120122
return initialArray;
121123
})
@@ -127,7 +129,7 @@ function firebaseListObservable(ref: firebase.database.Reference | firebase.data
127129
if (!isInitiallyEmpty) {
128130
// The last key in the initial array tells us where
129131
// to begin listening in realtime
130-
lastKey = initialArray[initialArray.length - 1].key;
132+
lastKey = toKey(initialArray[initialArray.length - 1]);
131133
}
132134

133135
const addFn = ref.on('child_added', (child: any, prevKey: string) => {
@@ -137,18 +139,18 @@ function firebaseListObservable(ref: firebase.database.Reference | firebase.data
137139
if (!isInitiallyEmpty && !hasInitialLoad) {
138140
if (child.key === lastKey) {
139141
hasInitialLoad = true;
140-
obs.next(preserveSnapshot ? initialArray : initialArray.map(utils.unwrapMapFn));
142+
obs.next(initialArray);
141143
return;
142144
}
143145
}
144146

145147
if (hasInitialLoad) {
146-
initialArray = onChildAdded(initialArray, child, prevKey);
148+
initialArray = onChildAdded(initialArray, toValue(child), toKey, prevKey);
147149
}
148150

149151
// only emit the array after the initial load
150152
if (hasInitialLoad) {
151-
obs.next(preserveSnapshot ? initialArray : initialArray.map(utils.unwrapMapFn));
153+
obs.next(initialArray);
152154
}
153155
}, err => {
154156
if (err) { obs.error(err); obs.complete(); }
@@ -157,20 +159,20 @@ function firebaseListObservable(ref: firebase.database.Reference | firebase.data
157159
handles.push({ event: 'child_added', handle: addFn });
158160

159161
let remFn = ref.on('child_removed', (child: any) => {
160-
initialArray = onChildRemoved(initialArray, child)
162+
initialArray = onChildRemoved(initialArray, toValue(child), toKey);
161163
if (hasInitialLoad) {
162-
obs.next(preserveSnapshot ? initialArray : initialArray.map(utils.unwrapMapFn));
164+
obs.next(initialArray);
163165
}
164166
}, err => {
165167
if (err) { obs.error(err); obs.complete(); }
166168
});
167169
handles.push({ event: 'child_removed', handle: remFn });
168170

169171
let chgFn = ref.on('child_changed', (child: any, prevKey: string) => {
170-
initialArray = onChildChanged(initialArray, child, prevKey)
172+
initialArray = onChildChanged(initialArray, toValue(child), toKey, prevKey)
171173
if (hasInitialLoad) {
172174
// This also manages when the only change is prevKey change
173-
obs.next(preserveSnapshot ? initialArray : initialArray.map(utils.unwrapMapFn));
175+
obs.next(initialArray);
174176
}
175177
}, err => {
176178
if (err) { obs.error(err); obs.complete(); }
@@ -199,49 +201,51 @@ function firebaseListObservable(ref: firebase.database.Reference | firebase.data
199201
return observeOn.call(listObs, new utils.ZoneScheduler(Zone.current));
200202
}
201203

202-
export function onChildAdded(arr:any[], child:any, prevKey:string): any[] {
204+
export function onChildAdded(arr:any[], child:any, toKey:(element:any)=>string, prevKey:string): any[] {
203205
if (!arr.length) {
204206
return [child];
205207
}
206-
207208
return arr.reduce((accumulator:firebase.database.DataSnapshot[], curr:firebase.database.DataSnapshot, i:number) => {
208209
if (!prevKey && i===0) {
209210
accumulator.push(child);
210211
}
211212
accumulator.push(curr);
212-
if (prevKey && prevKey === curr.key) {
213+
if (prevKey && prevKey === toKey(curr)) {
213214
accumulator.push(child);
214215
}
215216
return accumulator;
216217
}, []);
217218
}
218219

219-
export function onChildChanged(arr:any[], child:any, prevKey:string): any[] {
220+
export function onChildChanged(arr:any[], child:any, toKey:(element:any)=>string, prevKey:string): any[] {
221+
const childKey = toKey(child);
220222
return arr.reduce((accumulator:any[], val:any, i:number) => {
223+
const valKey = toKey(val);
221224
if (!prevKey && i==0) {
222225
accumulator.push(child);
223-
if (val.key !== child.key) {
226+
if (valKey !== childKey) {
224227
accumulator.push(val);
225228
}
226-
} else if(val.key === prevKey) {
229+
} else if(valKey === prevKey) {
227230
accumulator.push(val);
228231
accumulator.push(child);
229-
} else if (val.key !== child.key) {
232+
} else if (valKey !== childKey) {
230233
accumulator.push(val);
231234
}
232235
return accumulator;
233236
}, []);
234237
}
235238

236-
export function onChildRemoved(arr:any[], child:any): any[] {
237-
return arr.filter(c => c.key !== child.key);
239+
export function onChildRemoved(arr:any[], child:any, toKey:(element:any)=>string): any[] {
240+
let childKey = toKey(child);
241+
return arr.filter(c => toKey(c) !== childKey);
238242
}
239243

240-
export function onChildUpdated(arr:any[], child:any, prevKey:string): any[] {
244+
export function onChildUpdated(arr:any[], child:any, toKey:(element:any)=>string, prevKey:string): any[] {
241245
return arr.map((v, i, arr) => {
242246
if(!prevKey && !i) {
243247
return child;
244-
} else if (i > 0 && arr[i-1].key === prevKey) {
248+
} else if (i > 0 && toKey(arr[i-1]) === prevKey) {
245249
return child;
246250
} else {
247251
return v;

0 commit comments

Comments
 (0)