Skip to content

Commit eda1c41

Browse files
authored
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
});

0 commit comments

Comments
 (0)