Skip to content

Commit eda1c41

Browse files
authoredOct 13, 2017
Fixing null set with RTDB, better typings, and more (#1264)
* Once now closes out the subscription * Handle multiple subscriptions to the AngularFireDatabase * Empty set should return null * Better batching of first load, using `once` (needed step to support Universal) * Handle ordered child_added events * Don't error out on unsubscribe of a `once` * Clean up the tests to use `take(...)` and `add(...)` * Cleaning up the types Closes #1220, closes #1246.
1 parent 2ff8d1d commit eda1c41

24 files changed

+347
-315
lines changed
 

‎src/database/database.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
22
import { database } from 'firebase/app';
33
import 'firebase/database';
44
import { FirebaseApp } from 'angularfire2';
5-
import { PathReference, DatabaseQuery, DatabaseReference, DatabaseSnapshot, ChildEvent, ListenEvent, SnapshotChange, QueryFn, AngularFireList, AngularFireObject } from './interfaces';
5+
import { PathReference, DatabaseQuery, DatabaseReference, DatabaseSnapshot, ChildEvent, ListenEvent, QueryFn, AngularFireList, AngularFireObject } from './interfaces';
66
import { getRef } from './utils';
77
import { createListReference } from './list/create-reference';
88
import { createObjectReference } from './object/create-reference';
@@ -41,8 +41,7 @@ export {
4141
DatabaseReference,
4242
DatabaseSnapshot,
4343
ChildEvent,
44-
ListenEvent,
45-
SnapshotChange,
44+
ListenEvent,
4645
QueryFn,
4746
AngularFireList,
4847
AngularFireObject,

‎src/database/interfaces.ts

+11-22
Original file line numberDiff line numberDiff line change
@@ -12,51 +12,40 @@ export interface AngularFireList<T> {
1212
update(item: FirebaseOperation, data: T): Promise<void>;
1313
set(item: FirebaseOperation, data: T): Promise<void>;
1414
push(data: T): firebase.database.ThenableReference;
15-
remove(item?: FirebaseOperation): Promise<any>;
15+
remove(item?: FirebaseOperation): Promise<void>;
1616
}
1717

1818
export interface AngularFireObject<T> {
1919
query: DatabaseQuery;
2020
valueChanges<T>(): Observable<T | null>;
21-
snapshotChanges<T>(): Observable<SnapshotAction>;
22-
update(data: T): Promise<any>;
21+
snapshotChanges(): Observable<SnapshotAction>;
22+
update(data: Partial<T>): Promise<void>;
2323
set(data: T): Promise<void>;
24-
remove(): Promise<any>;
24+
remove(): Promise<void>;
2525
}
2626

2727
export interface FirebaseOperationCases {
28-
stringCase: () => Promise<void | any>;
29-
firebaseCase?: () => Promise<void | any>;
30-
snapshotCase?: () => Promise<void | any>;
31-
unwrappedSnapshotCase?: () => Promise<void | any>;
28+
stringCase: () => Promise<void>;
29+
firebaseCase?: () => Promise<void>;
30+
snapshotCase?: () => Promise<void>;
31+
unwrappedSnapshotCase?: () => Promise<void>;
3232
}
3333

3434
export type QueryFn = (ref: DatabaseReference) => DatabaseQuery;
3535
export type ChildEvent = 'child_added' | 'child_removed' | 'child_changed' | 'child_moved';
3636
export type ListenEvent = 'value' | ChildEvent;
3737

38-
export type SnapshotChange = {
39-
event: string;
40-
snapshot: DatabaseSnapshot | null;
41-
prevKey: string | undefined;
42-
}
43-
4438
export interface Action<T> {
45-
type: string;
39+
type: ListenEvent;
4640
payload: T;
4741
};
4842

4943
export interface AngularFireAction<T> extends Action<T> {
50-
prevKey: string | undefined;
44+
prevKey: string | null | undefined;
5145
key: string | null;
5246
}
5347

54-
export interface SnapshotPrevKey {
55-
snapshot: DatabaseSnapshot | null;
56-
prevKey: string | undefined;
57-
}
58-
59-
export type SnapshotAction = AngularFireAction<DatabaseSnapshot | null>;
48+
export type SnapshotAction = AngularFireAction<DatabaseSnapshot>;
6049

6150
export type Primitive = number | string | boolean;
6251

‎src/database/list/audit-trail.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'rxjs/add/operator/skip';
99
const rando = () => (Math.random() + 1).toString(36).substring(7);
1010
const FIREBASE_APP_NAME = rando();
1111

12-
describe('stateChanges', () => {
12+
describe('auditTrail', () => {
1313
let app: FirebaseApp;
1414
let db: AngularFireDatabase;
1515
let createRef: (path: string) => firebase.database.Reference;
@@ -56,7 +56,7 @@ describe('stateChanges', () => {
5656

5757
const { changes } = prepareAuditTrail();
5858
changes.subscribe(actions => {
59-
const data = actions.map(a => a.payload!.val());
59+
const data = actions.map(a => a.payload.val());
6060
expect(data).toEqual(items);
6161
done();
6262
});

‎src/database/list/audit-trail.ts

+48-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { DatabaseQuery, ChildEvent, AngularFireAction, SnapshotAction } from '../interfaces';
1+
import { DatabaseQuery, ChildEvent, DatabaseSnapshot, AngularFireAction, SnapshotAction } from '../interfaces';
22
import { stateChanges } from './state-changes';
3-
import { waitForLoaded } from './loaded';
43
import { Observable } from 'rxjs/Observable';
54
import { database } from 'firebase/app';
5+
import { fromRef } from '../observable/fromRef';
6+
7+
68
import 'rxjs/add/operator/skipWhile';
79
import 'rxjs/add/operator/withLatestFrom';
810
import 'rxjs/add/operator/map';
@@ -16,3 +18,47 @@ export function auditTrail(query: DatabaseQuery, events?: ChildEvent[]): Observa
1618
.scan((current, action) => [...current, action], []);
1719
return waitForLoaded(query, auditTrail$);
1820
}
21+
22+
interface LoadedMetadata {
23+
data: AngularFireAction<database.DataSnapshot>;
24+
lastKeyToLoad: any;
25+
}
26+
27+
function loadedData(query: DatabaseQuery): Observable<LoadedMetadata> {
28+
// Create an observable of loaded values to retrieve the
29+
// known dataset. This will allow us to know what key to
30+
// emit the "whole" array at when listening for child events.
31+
return fromRef(query, 'value')
32+
.map(data => {
33+
// Store the last key in the data set
34+
let lastKeyToLoad;
35+
// Loop through loaded dataset to find the last key
36+
data.payload.forEach(child => {
37+
lastKeyToLoad = child.key; return false;
38+
});
39+
// return data set and the current last key loaded
40+
return { data, lastKeyToLoad };
41+
});
42+
}
43+
44+
function waitForLoaded(query: DatabaseQuery, action$: Observable<SnapshotAction[]>) {
45+
const loaded$ = loadedData(query);
46+
return loaded$
47+
.withLatestFrom(action$)
48+
// Get the latest values from the "loaded" and "child" datasets
49+
// We can use both datasets to form an array of the latest values.
50+
.map(([loaded, actions]) => {
51+
// Store the last key in the data set
52+
let lastKeyToLoad = loaded.lastKeyToLoad;
53+
// Store all child keys loaded at this point
54+
const loadedKeys = actions.map(snap => snap.key);
55+
return { actions, lastKeyToLoad, loadedKeys }
56+
})
57+
// This is the magical part, only emit when the last load key
58+
// in the dataset has been loaded by a child event. At this point
59+
// we can assume the dataset is "whole".
60+
.skipWhile(meta => meta.loadedKeys.indexOf(meta.lastKeyToLoad) === -1)
61+
// Pluck off the meta data because the user only cares
62+
// to iterate through the snapshots
63+
.map(meta => meta.actions);
64+
}

‎src/database/list/changes.spec.ts

+67-32
Original file line numberDiff line numberDiff line change
@@ -43,69 +43,104 @@ describe('listChanges', () => {
4343

4444
describe('events', () => {
4545

46-
it('should stream child_added events', (done) => {
46+
it('should stream value at first', (done) => {
4747
const someRef = ref(rando());
48-
someRef.set(batch);
4948
const obs = listChanges(someRef, ['child_added']);
50-
const sub = obs.skip(2).subscribe(changes => {
51-
const data = changes.map(change => change.payload!.val());
49+
const sub = obs.take(1).subscribe(changes => {
50+
const data = changes.map(change => change.payload.val());
5251
expect(data).toEqual(items);
53-
done();
54-
});
52+
}).add(done);
53+
someRef.set(batch);
5554
});
5655

57-
it('should process a new child_added event', (done) => {
56+
it('should process a new child_added event', done => {
5857
const aref = ref(rando());
59-
aref.set(batch);
6058
const obs = listChanges(aref, ['child_added']);
61-
const sub = obs.skip(3).subscribe(changes => {
62-
const data = changes.map(change => change.payload!.val());
59+
const sub = obs.skip(1).take(1).subscribe(changes => {
60+
const data = changes.map(change => change.payload.val());
6361
expect(data[3]).toEqual({ name: 'anotha one' });
64-
done();
65-
});
62+
}).add(done);
63+
aref.set(batch);
6664
aref.push({ name: 'anotha one' });
6765
});
6866

69-
it('should process a new child_removed event', (done) => {
67+
it('should stream in order events', (done) => {
7068
const aref = ref(rando());
69+
const obs = listChanges(aref.orderByChild('name'), ['child_added']);
70+
const sub = obs.take(1).subscribe(changes => {
71+
const names = changes.map(change => change.payload.val().name);
72+
expect(names[0]).toEqual('one');
73+
expect(names[1]).toEqual('two');
74+
expect(names[2]).toEqual('zero');
75+
}).add(done);
7176
aref.set(batch);
72-
const obs = listChanges(aref, ['child_added','child_removed'])
77+
});
7378

74-
const sub = obs.skip(3).subscribe(changes => {
75-
const data = changes.map(change => change.payload!.val());
79+
it('should stream in order events w/child_added', (done) => {
80+
const aref = ref(rando());
81+
const obs = listChanges(aref.orderByChild('name'), ['child_added']);
82+
const sub = obs.skip(1).take(1).subscribe(changes => {
83+
const names = changes.map(change => change.payload.val().name);
84+
expect(names[0]).toEqual('anotha one');
85+
expect(names[1]).toEqual('one');
86+
expect(names[2]).toEqual('two');
87+
expect(names[3]).toEqual('zero');
88+
}).add(done);
89+
aref.set(batch);
90+
aref.push({ name: 'anotha one' });
91+
});
92+
93+
it('should stream events filtering', (done) => {
94+
const aref = ref(rando());
95+
const obs = listChanges(aref.orderByChild('name').equalTo('zero'), ['child_added']);
96+
obs.skip(1).take(1).subscribe(changes => {
97+
const names = changes.map(change => change.payload.val().name);
98+
expect(names[0]).toEqual('zero');
99+
expect(names[1]).toEqual('zero');
100+
}).add(done);
101+
aref.set(batch);
102+
aref.push({ name: 'zero' });
103+
});
104+
105+
it('should process a new child_removed event', done => {
106+
const aref = ref(rando());
107+
const obs = listChanges(aref, ['child_added','child_removed']);
108+
const sub = obs.skip(1).take(1).subscribe(changes => {
109+
const data = changes.map(change => change.payload.val());
76110
expect(data.length).toEqual(items.length - 1);
77-
done();
111+
}).add(done);
112+
app.database().goOnline();
113+
aref.set(batch).then(() => {
114+
aref.child(items[0].key).remove();
78115
});
79-
const childR = aref.child(items[0].key);
80-
childR.remove().then(console.log);
81116
});
82117

83118
it('should process a new child_changed event', (done) => {
84119
const aref = ref(rando());
85-
aref.set(batch);
86120
const obs = listChanges(aref, ['child_added','child_changed'])
87-
const sub = obs.skip(3).subscribe(changes => {
88-
const data = changes.map(change => change.payload!.val());
89-
expect(data[0].name).toEqual('lol');
90-
done();
121+
const sub = obs.skip(1).take(1).subscribe(changes => {
122+
const data = changes.map(change => change.payload.val());
123+
expect(data[1].name).toEqual('lol');
124+
}).add(done);
125+
app.database().goOnline();
126+
aref.set(batch).then(() => {
127+
aref.child(items[1].key).update({ name: 'lol'});
91128
});
92-
const childR = aref.child(items[0].key);
93-
childR.update({ name: 'lol'});
94129
});
95130

96131
it('should process a new child_moved event', (done) => {
97132
const aref = ref(rando());
98-
aref.set(batch);
99133
const obs = listChanges(aref, ['child_added','child_moved'])
100-
const sub = obs.skip(3).subscribe(changes => {
101-
const data = changes.map(change => change.payload!.val());
134+
const sub = obs.skip(1).take(1).subscribe(changes => {
135+
const data = changes.map(change => change.payload.val());
102136
// We moved the first item to the last item, so we check that
103137
// the new result is now the last result
104138
expect(data[data.length - 1]).toEqual(items[0]);
105-
done();
139+
}).add(done);
140+
app.database().goOnline();
141+
aref.set(batch).then(() => {
142+
aref.child(items[0].key).setPriority('a', () => {});
106143
});
107-
const childR = aref.child(items[0].key);
108-
childR.setPriority('a', () => {});
109144
});
110145

111146
});

‎src/database/list/changes.ts

+85-28
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,93 @@
11
import { fromRef } from '../observable/fromRef';
22
import { Observable } from 'rxjs/Observable';
3-
import { DatabaseQuery, ChildEvent, SnapshotChange, AngularFireAction, SnapshotAction } from '../interfaces';
4-
import { positionFor, positionAfter } from './utils';
3+
import { DatabaseQuery, ChildEvent, AngularFireAction, SnapshotAction } from '../interfaces';
4+
import { isNil } from '../utils';
5+
56
import 'rxjs/add/operator/scan';
67
import 'rxjs/add/observable/merge';
8+
import 'rxjs/add/observable/of';
9+
import 'rxjs/add/operator/switchMap';
10+
import 'rxjs/add/operator/filter';
11+
import 'rxjs/add/operator/delay';
12+
import 'rxjs/add/operator/distinctUntilChanged';
713

8-
// TODO(davideast): check safety of ! operator in scan
914
export function listChanges<T>(ref: DatabaseQuery, events: ChildEvent[]): Observable<SnapshotAction[]> {
10-
const childEvent$ = events.map(event => fromRef(ref, event));
11-
return Observable.merge(...childEvent$)
12-
.scan((current, action) => {
13-
const { payload, type, prevKey, key } = action;
14-
switch (action.type) {
15-
case 'child_added':
16-
return [...current, action];
17-
case 'child_removed':
18-
// ! is okay here because only value events produce null results
19-
return current.filter(x => x.payload!.key !== payload!.key);
20-
case 'child_changed':
21-
return current.map(x => x.payload!.key === key ? action : x);
22-
case 'child_moved':
23-
const curPos = positionFor(current, payload!.key)
24-
if(curPos > -1) {
25-
const data = current.splice(curPos, 1)[0];
26-
const newPost = positionAfter(current, prevKey);
27-
current.splice(newPost, 0, data);
28-
return current;
29-
}
30-
return current;
31-
// default will also remove null results
32-
default:
33-
return current;
15+
return fromRef(ref, 'value', 'once').switchMap(snapshotAction => {
16+
const childEvent$ = [Observable.of(snapshotAction)];
17+
events.forEach(event => childEvent$.push(fromRef(ref, event)));
18+
return Observable.merge(...childEvent$).scan(buildView, [])
19+
})
20+
.distinctUntilChanged();
21+
}
22+
23+
function positionFor(changes: SnapshotAction[], key) {
24+
const len = changes.length;
25+
for(let i=0; i<len; i++) {
26+
if(changes[i].payload.key === key) {
27+
return i;
3428
}
35-
}, []);
29+
}
30+
return -1;
3631
}
32+
33+
function positionAfter(changes: SnapshotAction[], prevKey?: string) {
34+
if(isNil(prevKey)) {
35+
return 0;
36+
} else {
37+
const i = positionFor(changes, prevKey);
38+
if( i === -1) {
39+
return changes.length;
40+
} else {
41+
return i + 1;
42+
}
43+
}
44+
}
45+
46+
function buildView(current, action) {
47+
const { payload, type, prevKey, key } = action;
48+
const currentKeyPosition = positionFor(current, key);
49+
const afterPreviousKeyPosition = positionAfter(current, prevKey);
50+
switch (action.type) {
51+
case 'value':
52+
if (action.payload && action.payload.exists()) {
53+
let prevKey = null;
54+
action.payload.forEach(payload => {
55+
const action = {payload, type: 'value', prevKey, key: payload.key};
56+
prevKey = payload.key;
57+
current = [...current, action];
58+
return false;
59+
});
60+
}
61+
return current;
62+
case 'child_added':
63+
if (currentKeyPosition > -1) {
64+
// check that the previouskey is what we expect, else reorder
65+
const previous = current[currentKeyPosition - 1];
66+
if ((previous && previous.key || null) != prevKey) {
67+
current = current.filter(x => x.payload.key !== payload.key);
68+
current.splice(afterPreviousKeyPosition, 0, action);
69+
}
70+
} else if (prevKey == null) {
71+
return [action, ...current];
72+
} else {
73+
current = current.slice()
74+
current.splice(afterPreviousKeyPosition, 0, action);
75+
}
76+
return current;
77+
case 'child_removed':
78+
return current.filter(x => x.payload.key !== payload.key);
79+
case 'child_changed':
80+
return current.map(x => x.payload.key === key ? action : x);
81+
case 'child_moved':
82+
if(currentKeyPosition > -1) {
83+
const data = current.splice(currentKeyPosition, 1)[0];
84+
current = current.slice()
85+
current.splice(afterPreviousKeyPosition, 0, data);
86+
return current;
87+
}
88+
return current;
89+
// default will also remove null results
90+
default:
91+
return current;
92+
}
93+
}

‎src/database/list/create-reference.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DatabaseQuery, AngularFireList, ChildEvent } from '../interfaces';
2-
import { createLoadedChanges, loadedSnapshotChanges } from './loaded';
2+
import { snapshotChanges } from './snapshot-changes';
33
import { createStateChanges } from './state-changes';
44
import { createAuditTrail } from './audit-trail';
55
import { createDataOperationMethod } from './data-operation';
@@ -12,12 +12,14 @@ export function createListReference<T>(query: DatabaseQuery): AngularFireList<T>
1212
set: createDataOperationMethod<T>(query.ref, 'set'),
1313
push: (data: T) => query.ref.push(data),
1414
remove: createRemoveMethod(query.ref),
15-
snapshotChanges: createLoadedChanges(query),
15+
snapshotChanges(events?: ChildEvent[]) {
16+
return snapshotChanges(query, events);
17+
},
1618
stateChanges: createStateChanges(query),
1719
auditTrail: createAuditTrail(query),
1820
valueChanges<T>(events?: ChildEvent[]) {
19-
return loadedSnapshotChanges(query, events)
20-
.map(actions => actions.map(a => a.payload!.val()));
21+
return snapshotChanges(query, events)
22+
.map(actions => actions.map(a => a.payload.val()));
2123
}
2224
}
2325
}

‎src/database/list/loaded.spec.ts

-54
This file was deleted.

‎src/database/list/loaded.ts

-70
This file was deleted.

‎src/database/list/snapshot-changes.spec.ts

+69-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AngularFireDatabase, AngularFireDatabaseModule, snapshotChanges, ChildE
44
import { TestBed, inject } from '@angular/core/testing';
55
import { COMMON_CONFIG } from '../test-config';
66
import 'rxjs/add/operator/skip';
7+
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
78

89
// generate random string to test fidelity of naming
910
const rando = () => (Math.random() + 1).toString(36).substring(7);
@@ -44,7 +45,6 @@ describe('snapshotChanges', () => {
4445
function prepareSnapshotChanges(opts: { events?: ChildEvent[], skip: number } = { skip: 0 }) {
4546
const { events, skip } = opts;
4647
const aref = createRef(rando());
47-
aref.set(batch);
4848
const snapChanges = snapshotChanges(aref, events);
4949
return {
5050
snapChanges: snapChanges.skip(skip),
@@ -53,40 +53,92 @@ describe('snapshotChanges', () => {
5353
}
5454

5555
it('should listen to all events by default', (done) => {
56-
const { snapChanges } = prepareSnapshotChanges({ skip: 2 });
57-
const sub = snapChanges.subscribe(actions => {
56+
const { snapChanges, ref } = prepareSnapshotChanges();
57+
snapChanges.take(1).subscribe(actions => {
5858
const data = actions.map(a => a.payload!.val());
5959
expect(data).toEqual(items);
60-
done();
61-
sub.unsubscribe();
62-
});
60+
}).add(done);
61+
ref.set(batch);
6362
});
6463

65-
it('should listen to only child_added events', (done) => {
66-
const { snapChanges } = prepareSnapshotChanges({ events: ['child_added'], skip: 2 });
67-
const sub = snapChanges.subscribe(actions => {
64+
it('should handle multiple subscriptions (hot)', (done) => {
65+
const { snapChanges, ref } = prepareSnapshotChanges();
66+
const sub = snapChanges.subscribe(() => {}).add(done);
67+
snapChanges.take(1).subscribe(actions => {
6868
const data = actions.map(a => a.payload!.val());
6969
expect(data).toEqual(items);
70-
done();
71-
sub.unsubscribe();
70+
}).add(sub);
71+
ref.set(batch);
72+
});
73+
74+
it('should handle multiple subscriptions (warm)', done => {
75+
const { snapChanges, ref } = prepareSnapshotChanges();
76+
snapChanges.take(1).subscribe(() => {}).add(() => {
77+
snapChanges.take(1).subscribe(actions => {
78+
const data = actions.map(a => a.payload!.val());
79+
expect(data).toEqual(items);
80+
}).add(done);
7281
});
82+
ref.set(batch);
83+
});
84+
85+
it('should listen to only child_added events', (done) => {
86+
const { snapChanges, ref } = prepareSnapshotChanges({ events: ['child_added'], skip: 0 });
87+
snapChanges.take(1).subscribe(actions => {
88+
const data = actions.map(a => a.payload!.val());
89+
expect(data).toEqual(items);
90+
}).add(done);
91+
ref.set(batch);
7392
});
7493

7594
it('should listen to only child_added, child_changed events', (done) => {
7695
const { snapChanges, ref } = prepareSnapshotChanges({
7796
events: ['child_added', 'child_changed'],
78-
skip: 3
97+
skip: 1
7998
});
8099
const name = 'ligatures';
81-
const sub = snapChanges.subscribe(actions => {
100+
snapChanges.take(1).subscribe(actions => {
82101
const data = actions.map(a => a.payload!.val());;
83102
const copy = [...items];
84103
copy[0].name = name;
85104
expect(data).toEqual(copy);
86-
done();
87-
sub.unsubscribe();
105+
}).add(done);
106+
app.database().goOnline();
107+
ref.set(batch).then(() => {
108+
ref.child(items[0].key).update({ name })
88109
});
89-
ref.child(items[0].key).update({ name });
90-
});
110+
});
111+
112+
it('should handle empty sets', done => {
113+
const aref = createRef(rando());
114+
aref.set({});
115+
snapshotChanges(aref).take(1).subscribe(data => {
116+
expect(data.length).toEqual(0);
117+
}).add(done);
118+
});
119+
120+
it('should handle dynamic queries that return empty sets', done => {
121+
const ITEMS = 10;
122+
let count = 0;
123+
let firstIndex = 0;
124+
let namefilter$ = new BehaviorSubject<number|null>(null);
125+
const aref = createRef(rando());
126+
aref.set(batch);
127+
namefilter$.switchMap(name => {
128+
const filteredRef = name ? aref.child('name').equalTo(name) : aref
129+
return snapshotChanges(filteredRef);
130+
}).take(2).subscribe(data => {
131+
count = count + 1;
132+
// the first time should all be 'added'
133+
if(count === 1) {
134+
expect(Object.keys(data).length).toEqual(3);
135+
namefilter$.next(-1);
136+
}
137+
// on the second round, we should have filtered out everything
138+
if(count === 2) {
139+
expect(Object.keys(data).length).toEqual(0);
140+
}
141+
}).add(done);
142+
});
91143

92144
});

‎src/database/list/snapshot-changes.ts

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { database } from 'firebase/app';
55
import { validateEventsArray } from './utils';
66
import 'rxjs/add/operator/map';
77

8-
// TODO(davideast): Test safety of ! unwrap
98
export function snapshotChanges(query: DatabaseQuery, events?: ChildEvent[]): Observable<SnapshotAction[]> {
109
events = validateEventsArray(events);
1110
return listChanges(query, events!);

‎src/database/list/state-changes.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('stateChanges', () => {
5757
const { changes } = prepareStateChanges({ skip: 2 });
5858
changes.subscribe(action => {
5959
expect(action.key).toEqual('2');
60-
expect(action.payload!.val()).toEqual(items[items.length - 1]);
60+
expect(action.payload.val()).toEqual(items[items.length - 1]);
6161
done();
6262
});
6363

‎src/database/list/utils.ts

+1-25
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,8 @@
11
import { isNil } from '../utils';
2-
import { SnapshotAction } from '../interfaces';
32

43
export function validateEventsArray(events?: any[]) {
54
if(isNil(events) || events!.length === 0) {
65
events = ['child_added', 'child_removed', 'child_changed', 'child_moved'];
76
}
87
return events;
9-
}
10-
11-
export function positionFor(changes: SnapshotAction[], key) {
12-
const len = changes.length;
13-
for(let i=0; i<len; i++) {
14-
if(changes[i].payload!.key === key) {
15-
return i;
16-
}
17-
}
18-
return -1;
19-
}
20-
21-
export function positionAfter(changes: SnapshotAction[], prevKey?: string) {
22-
if(isNil(prevKey)) {
23-
return 0;
24-
} else {
25-
const i = positionFor(changes, prevKey);
26-
if( i === -1) {
27-
return changes.length;
28-
} else {
29-
return i + 1;
30-
}
31-
}
32-
}
8+
}

‎src/database/list/value-changes.ts

-13
This file was deleted.

‎src/database/object/create-reference.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ export function createObjectReference<T>(query: DatabaseQuery): AngularFireObjec
55
return {
66
query,
77
snapshotChanges: createObjectSnapshotChanges(query),
8-
update(data: T) { return query.ref.update(data) as Promise<any>; },
9-
set(data: T) { return query.ref.set(data) as Promise<any>; },
10-
remove() { return query.ref.remove() as Promise<any>; },
8+
update(data: Partial<T>) { return query.ref.update(data) as Promise<void>; },
9+
set(data: T) { return query.ref.set(data) as Promise<void>; },
10+
remove() { return query.ref.remove() as Promise<void>; },
1111
valueChanges<T>() {
1212
return createObjectSnapshotChanges(query)()
13-
.map(action => action.payload ? action.payload.val() as T : null)
13+
.map(action => action.payload.exists() ? action.payload.val() as T : null)
1414
},
1515
}
1616
}

‎src/database/object/snapshot-changes.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DatabaseQuery, AngularFireAction, SnapshotAction } from '../interfaces'
44
import { database } from 'firebase/app';
55

66
export function createObjectSnapshotChanges(query: DatabaseQuery) {
7-
return function snapshotChanges<T>(): Observable<SnapshotAction> {
7+
return function snapshotChanges(): Observable<SnapshotAction> {
88
return fromRef(query, 'value');
99
}
1010
}

‎src/database/object/value-changes.ts

-10
This file was deleted.

‎src/database/observable/fromRef.spec.ts

+26-9
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ describe('fromRef', () => {
5353
expect(count).toEqual(0);
5454
});
5555

56+
it('it should should handle non-existence', (done) => {
57+
const itemRef = ref(rando());
58+
itemRef.set({});
59+
const obs = fromRef(itemRef, 'value');
60+
const sub = obs.take(1).subscribe(change => {
61+
expect(change.payload.exists()).toEqual(false);
62+
expect(change.payload.val()).toEqual(null);
63+
}).add(done);
64+
});
65+
66+
it('once should complete', (done) => {
67+
const itemRef = ref(rando());
68+
itemRef.set(batch);
69+
const obs = fromRef(itemRef, 'value', 'once');
70+
obs.subscribe(change => {}, () => {}, done);
71+
});
72+
5673
it('it should listen and then unsubscribe', (done) => {
5774
const itemRef = ref(rando());
5875
itemRef.set(batch);
@@ -80,7 +97,7 @@ describe('fromRef', () => {
8097
count = count + 1;
8198
const { type, payload } = change;
8299
expect(type).toEqual('child_added');
83-
expect(payload!.val()).toEqual(batch[payload!.key!]);
100+
expect(payload.val()).toEqual(batch[payload.key!]);
84101
if (count === items.length) {
85102
done();
86103
sub.unsubscribe();
@@ -98,8 +115,8 @@ describe('fromRef', () => {
98115
const sub = obs.subscribe(change => {
99116
const { type, payload } = change;
100117
expect(type).toEqual('child_changed');
101-
expect(payload!.key).toEqual(key);
102-
expect(payload!.val()).toEqual({ key, name });
118+
expect(payload.key).toEqual(key);
119+
expect(payload.val()).toEqual({ key, name });
103120
sub.unsubscribe();
104121
done();
105122
});
@@ -115,8 +132,8 @@ describe('fromRef', () => {
115132
const sub = obs.subscribe(change => {
116133
const { type, payload } = change;
117134
expect(type).toEqual('child_removed');
118-
expect(payload!.key).toEqual(key);
119-
expect(payload!.val()).toEqual({ key, name });
135+
expect(payload.key).toEqual(key);
136+
expect(payload.val()).toEqual({ key, name });
120137
sub.unsubscribe();
121138
done();
122139
});
@@ -132,8 +149,8 @@ describe('fromRef', () => {
132149
const sub = obs.subscribe(change => {
133150
const { type, payload } = change;
134151
expect(type).toEqual('child_moved');
135-
expect(payload!.key).toEqual(key);
136-
expect(payload!.val()).toEqual({ key, name });
152+
expect(payload.key).toEqual(key);
153+
expect(payload.val()).toEqual({ key, name });
137154
sub.unsubscribe();
138155
done();
139156
});
@@ -147,7 +164,7 @@ describe('fromRef', () => {
147164
const sub = obs.subscribe(change => {
148165
const { type, payload } = change;
149166
expect(type).toEqual('value');
150-
expect(payload!.val()).toEqual(batch);
167+
expect(payload.val()).toEqual(batch);
151168
done();
152169
sub.unsubscribe();
153170
expect(sub.closed).toEqual(true);
@@ -161,7 +178,7 @@ describe('fromRef', () => {
161178
const obs = fromRef(query, 'value');
162179
const sub = obs.subscribe(change => {
163180
let child;
164-
change.payload!.forEach(snap => { child = snap.val(); return true; });
181+
change.payload.forEach(snap => { child = snap.val(); return true; });
165182
expect(child).toEqual(items[0]);
166183
done();
167184
});

‎src/database/observable/fromRef.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,42 @@
1-
import { DatabaseQuery, DatabaseSnapshot, ListenEvent, SnapshotPrevKey, AngularFireAction } from '../interfaces';
1+
import { DatabaseQuery, DatabaseSnapshot, ListenEvent, AngularFireAction } from '../interfaces';
22
import { Observable } from 'rxjs/Observable';
33
import { observeOn } from 'rxjs/operator/observeOn';
44
import { ZoneScheduler } from 'angularfire2';
55
import 'rxjs/add/operator/map';
66
import 'rxjs/add/operator/delay';
7+
import 'rxjs/add/operator/share';
8+
9+
interface SnapshotPrevKey {
10+
snapshot: DatabaseSnapshot;
11+
prevKey: string | null | undefined;
12+
}
713

814
/**
915
* Create an observable from a Database Reference or Database Query.
1016
* @param ref Database Reference
1117
* @param event Listen event type ('value', 'added', 'changed', 'removed', 'moved')
1218
*/
13-
export function fromRef(ref: DatabaseQuery, event: ListenEvent, listenType = 'on'): Observable<AngularFireAction<DatabaseSnapshot | null>> {
14-
const ref$ = new Observable<SnapshotPrevKey | null | undefined>(subscriber => {
19+
export function fromRef(ref: DatabaseQuery, event: ListenEvent, listenType = 'on'): Observable<AngularFireAction<DatabaseSnapshot>> {
20+
const ref$ = new Observable<SnapshotPrevKey>(subscriber => {
1521
const fn = ref[listenType](event, (snapshot, prevKey) => {
16-
subscriber.next({ snapshot, prevKey })
22+
subscriber.next({ snapshot, prevKey });
23+
if (listenType == 'once') { subscriber.complete(); }
1724
}, subscriber.error.bind(subscriber));
18-
return { unsubscribe() { ref.off(event, fn)} }
25+
if (listenType == 'on') {
26+
return { unsubscribe() { ref.off(event, fn)} };
27+
} else {
28+
return { unsubscribe() { } };
29+
}
1930
})
2031
.map((payload: SnapshotPrevKey) => {
2132
const { snapshot, prevKey } = payload;
2233
let key: string | null = null;
23-
if(snapshot) { key = snapshot.key; }
34+
if (snapshot.exists()) { key = snapshot.key; }
2435
return { type: event, payload: snapshot, prevKey, key };
2536
})
2637
// Ensures subscribe on observable is async. This handles
2738
// a quirk in the SDK where on/once callbacks can happen
2839
// synchronously.
2940
.delay(0);
30-
return observeOn.call(ref$, new ZoneScheduler(Zone.current));
41+
return observeOn.call(ref$, new ZoneScheduler(Zone.current)).share();
3142
}

‎src/database/public_api.ts

-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@ export * from './list/create-reference';
44
export * from './list/snapshot-changes';
55
export * from './list/state-changes';
66
export * from './list/audit-trail';
7-
export * from './list/loaded';
87
export * from './observable/fromRef';
98
export * from './database.module'

‎src/database/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function getRef(app: FirebaseApp, pathRef: PathReference): DatabaseRefere
3333
: app.database().ref(pathRef as string);
3434
}
3535

36-
export function checkOperationCases(item: FirebaseOperation, cases: FirebaseOperationCases) : Promise<void | any> {
36+
export function checkOperationCases(item: FirebaseOperation, cases: FirebaseOperationCases) : Promise<void> {
3737
if (isString(item)) {
3838
return cases.stringCase();
3939
} else if (isFirebaseRef(item)) {

‎src/firestore/document/document.spec.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,10 @@ describe('AngularFirestoreDocument', () => {
5757
const stock = new AngularFirestoreDocument<Stock>(ref);
5858
await stock.set(FAKE_STOCK_DATA);
5959
const obs$ = stock.valueChanges();
60-
const sub = obs$.catch(e => { console.log(e); return e; })
61-
.take(1) // this will unsubscribe after the first
62-
.subscribe(async (data: Stock) => {
63-
sub.unsubscribe();
64-
expect(JSON.stringify(data)).toBe(JSON.stringify(FAKE_STOCK_DATA));
65-
stock.delete().then(done).catch(done.fail);
66-
});
60+
obs$.take(1).subscribe(async (data: Stock) => {
61+
expect(JSON.stringify(data)).toBe(JSON.stringify(FAKE_STOCK_DATA));
62+
stock.delete().then(done).catch(done.fail);
63+
});
6764
});
6865

6966
});

‎src/root.spec.js

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export * from './packages-dist/database/database.spec';
88
export * from './packages-dist/database/utils.spec';
99
export * from './packages-dist/database/observable/fromRef.spec';
1010
export * from './packages-dist/database/list/changes.spec';
11-
export * from './packages-dist/database/list/loaded.spec';
1211
export * from './packages-dist/database/list/snapshot-changes.spec';
1312
export * from './packages-dist/database/list/state-changes.spec';
1413
export * from './packages-dist/database/list/audit-trail.spec';

‎tools/build.js

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const GLOBALS = {
3434
'rxjs/add/operator/scan': 'Rx.Observable.prototype',
3535
'rxjs/add/operator/skip': 'Rx.Observable.prototype',
3636
'rxjs/add/operator/do': 'Rx.Observable.prototype',
37+
'rxjs/add/operator/distinctUntilChanged': 'Rx.Observable.prototype',
3738
'rxjs/add/operator/filter': 'Rx.Observable.prototype',
3839
'rxjs/add/operator/skipUntil': 'Rx.Observable.prototype',
3940
'rxjs/add/operator/skipWhile': 'Rx.Observable.prototype',

0 commit comments

Comments
 (0)
Please sign in to comment.