Skip to content

Commit 86e89a3

Browse files
committed
feat(afs): stateChanges() and snapshotChanges()
1 parent 328bbb7 commit 86e89a3

File tree

5 files changed

+203
-154
lines changed

5 files changed

+203
-154
lines changed

src/firestore/collection/changes.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,66 @@
11
import { fromCollectionRef } from '../observable/fromRef';
2-
import { Query, DocumentChangeType } from 'firestore';
2+
import { Query, DocumentChangeType, DocumentChange, DocumentSnapshot, QuerySnapshot } from 'firestore';
33
import { Observable } from 'rxjs/Observable';
44
import 'rxjs/add/observable/map';
5+
import 'rxjs/add/operator/scan';
56

6-
import { DocumentChangeAction } from '../interfaces';
7+
import { DocumentChangeAction, Action } from '../interfaces';
78

8-
export function changes(query: Query): Observable<DocumentChangeAction[]> {
9+
/**
10+
* Return a stream of document changes on a query. These results are not in sort order but in
11+
* order of occurence.
12+
* @param query
13+
*/
14+
export function docChanges(query: Query): Observable<DocumentChangeAction[]> {
915
return fromCollectionRef(query)
1016
.map(action =>
1117
action.payload.docChanges
1218
.map(change => ({ type: change.type, payload: change })));
1319
}
20+
21+
/**
22+
* Return a stream of document changes on a query. These results are in sort order.
23+
* @param query
24+
*/
25+
export function sortedChanges(query: Query, events: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
26+
return fromCollectionRef(query)
27+
.map(changes => changes.payload.docChanges)
28+
.scan((current, changes) => combineChanges(current, changes, events), [])
29+
.map(changes => changes.map(c => ({ type: c.type, payload: c })));
30+
}
31+
32+
/**
33+
* Combines the total result set from the current set of changes from an incoming set
34+
* of changes.
35+
* @param current
36+
* @param changes
37+
* @param events
38+
*/
39+
export function combineChanges(current: DocumentChange[], changes: DocumentChange[], events: DocumentChangeType[]) {
40+
let combined: DocumentChange[] = [];
41+
changes.forEach(change => {
42+
// skip unwanted change types
43+
if(events.indexOf(change.type) > -1) {
44+
combined = combineChange(combined, change);
45+
}
46+
});
47+
return combined;
48+
}
49+
50+
/**
51+
* Creates a new sorted array from a new change.
52+
* @param combined
53+
* @param change
54+
*/
55+
export function combineChange(combined: DocumentChange[], change: DocumentChange): DocumentChange[] {
56+
switch(change.type) {
57+
case 'added':
58+
return [...combined, change];
59+
case 'modified':
60+
return combined.map(x => x.doc.id === change.doc.id ? change : x);
61+
case 'removed':
62+
return combined.filter(x => x.doc.id !== change.doc.id);
63+
}
64+
return combined;
65+
}
66+

src/firestore/collection/collection.spec.ts

Lines changed: 84 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,7 @@ import 'rxjs/add/operator/skip';
1414
import { TestBed, inject } from '@angular/core/testing';
1515
import { COMMON_CONFIG } from '../test-config';
1616

17-
interface Stock {
18-
name: string;
19-
price: number;
20-
}
21-
22-
const FAKE_STOCK_DATA = { name: 'FAKE', price: 1 };
23-
24-
const randomName = (firestore): string => firestore.collection('a').doc().id;
25-
26-
const createRandomStocks = async (firestore: firestore.Firestore, collectionRef: firestore.CollectionReference, numberOfItems) => {
27-
// Create a batch to update everything at once
28-
const batch = firestore.batch();
29-
// Store the random names to delete them later
30-
let count = 0;
31-
let names: string[] = [];
32-
Array.from(Array(numberOfItems)).forEach((a, i) => {
33-
const name = randomName(firestore);
34-
batch.set(collectionRef.doc(name), FAKE_STOCK_DATA);
35-
names = [...names, name];
36-
});
37-
// Create the batch entries
38-
// Commit!
39-
await batch.commit();
40-
return names;
41-
}
17+
import { Stock, randomName, FAKE_STOCK_DATA, createRandomStocks, delayAdd, delayDelete, delayUpdate, deleteThemAll } from '../utils.spec';
4218

4319
describe('AngularFirestoreCollection', () => {
4420
let app: firebase.app.App;
@@ -63,29 +39,6 @@ describe('AngularFirestoreCollection', () => {
6339
done();
6440
});
6541

66-
function deleteThemAll(names, ref) {
67-
const promises = names.map(name => ref.doc(name).delete());
68-
return Promise.all(promises);
69-
}
70-
71-
function delayUpdate<T>(collection: AngularFirestoreCollection<T>, path, data, delay = 250) {
72-
setTimeout(() => {
73-
collection.doc(path).update(data);
74-
}, delay);
75-
}
76-
77-
function delayAdd<T>(collection: AngularFirestoreCollection<T>, path, data, delay = 250) {
78-
setTimeout(() => {
79-
collection.doc(path).set(data);
80-
}, delay);
81-
}
82-
83-
function delayDelete<T>(collection: AngularFirestoreCollection<T>, path, delay = 250) {
84-
setTimeout(() => {
85-
collection.doc(path).delete();
86-
}, delay);
87-
}
88-
8942
it('should get unwrapped snapshot', async (done: any) => {
9043
const randomCollectionName = randomName(afs.firestore);
9144
const ref = afs.firestore.collection(`${randomCollectionName}`);
@@ -114,15 +67,15 @@ describe('AngularFirestoreCollection', () => {
11467

11568
});
11669

117-
it('should get snapshot updates', async (done: any) => {
70+
it('should get stateChanges() updates', async (done: any) => {
11871
const randomCollectionName = randomName(afs.firestore);
11972
const ref = afs.firestore.collection(`${randomCollectionName}`);
12073
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
12174
const ITEMS = 10;
12275

12376
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
12477

125-
const sub = stocks.snapshotChanges().subscribe(data => {
78+
const sub = stocks.stateChanges().subscribe(data => {
12679
// unsub immediately as we will be deleting data at the bottom
12780
// and that will trigger another subscribe callback and fail
12881
// the test
@@ -140,84 +93,94 @@ describe('AngularFirestoreCollection', () => {
14093

14194
});
14295

143-
it('should listen to all changes by default', async (done) => {
144-
const randomCollectionName = randomName(afs.firestore);
145-
const ref = afs.firestore.collection(`${randomCollectionName}`);
146-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
147-
const ITEMS = 10;
148-
let count = 0;
149-
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
150-
const sub = stocks.snapshotChanges().subscribe(data => {
151-
count = count + 1;
152-
if(count === 1) {
153-
stocks.doc(names[0]).update({ price: 2});
154-
}
155-
if(count === 2) {
156-
expect(data.length).toEqual(1);
157-
expect(data[0].type).toEqual('modified');
158-
deleteThemAll(names, ref).then(done).catch(done.fail);
159-
}
160-
});
161-
});
162-
163-
it('should be able to filter change types - modified', async (done) => {
164-
const randomCollectionName = randomName(afs.firestore);
165-
const ref = afs.firestore.collection(`${randomCollectionName}`);
166-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
167-
const ITEMS = 10;
168-
let count = 0;
169-
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
170-
171-
const sub = stocks.snapshotChanges(['modified']).subscribe(data => {
172-
expect(data.length).toEqual(1);
173-
expect(data[0].payload.doc.data().price).toEqual(2);
174-
expect(data[0].type).toEqual('modified');
175-
deleteThemAll(names, ref).then(done).catch(done.fail);
176-
done();
177-
});
96+
fdescribe('snapshotChanges()', () => {
17897

179-
delayUpdate(stocks, names[0], { price: 2 });
180-
});
98+
it('should listen to all snapshotChanges() by default', async (done) => {
18199

182-
it('should be able to filter change types - added', async (done) => {
183-
const randomCollectionName = randomName(afs.firestore);
184-
const ref = afs.firestore.collection(`${randomCollectionName}`);
185-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
186-
const ITEMS = 10;
187-
let count = 0;
188-
let names = await createRandomStocks(afs.firestore, ref, ITEMS);
189-
190-
const sub = stocks.snapshotChanges(['added']).skip(1).subscribe(data => {
191-
expect(data.length).toEqual(1);
192-
expect(data[0].payload.doc.data().price).toEqual(2);
193-
expect(data[0].type).toEqual('added');
194-
deleteThemAll(names, ref).then(done).catch(done.fail);
195-
done();
196100
});
197101

198-
const nextId = ref.doc('a').id;
199-
names = names.concat([nextId]);
200-
delayAdd(stocks, nextId, { price: 2 });
201102
});
202103

203-
fit('should be able to filter change types - removed', async (done) => {
204-
const randomCollectionName = randomName(afs.firestore);
205-
const ref = afs.firestore.collection(`${randomCollectionName}`);
206-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
207-
const ITEMS = 10;
208-
let count = 0;
209-
let names = await createRandomStocks(afs.firestore, ref, ITEMS);
104+
describe('stateChanges()', () => {
105+
it('should listen to all stateChanges() by default', async (done) => {
106+
const randomCollectionName = randomName(afs.firestore);
107+
const ref = afs.firestore.collection(`${randomCollectionName}`);
108+
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
109+
const ITEMS = 10;
110+
let count = 0;
111+
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
112+
const sub = stocks.stateChanges().subscribe(data => {
113+
count = count + 1;
114+
if(count === 1) {
115+
stocks.doc(names[0]).update({ price: 2});
116+
}
117+
if(count === 2) {
118+
expect(data.length).toEqual(1);
119+
expect(data[0].type).toEqual('modified');
120+
deleteThemAll(names, ref).then(done).catch(done.fail);
121+
}
122+
});
123+
});
210124

211-
const sub = stocks.snapshotChanges(['removed']).subscribe(data => {
212-
debugger;
213-
expect(data.length).toEqual(1);
214-
expect(data[0].type).toEqual('removed');
215-
deleteThemAll(names, ref).then(done).catch(done.fail);
216-
done();
125+
it('should be able to filter stateChanges() types - modified', async (done) => {
126+
const randomCollectionName = randomName(afs.firestore);
127+
const ref = afs.firestore.collection(`${randomCollectionName}`);
128+
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
129+
const ITEMS = 10;
130+
let count = 0;
131+
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
132+
133+
const sub = stocks.stateChanges(['modified']).subscribe(data => {
134+
sub.unsubscribe();
135+
expect(data.length).toEqual(1);
136+
expect(data[0].payload.doc.data().price).toEqual(2);
137+
expect(data[0].type).toEqual('modified');
138+
deleteThemAll(names, ref).then(done).catch(done.fail);
139+
done();
140+
});
141+
142+
delayUpdate(stocks, names[0], { price: 2 });
217143
});
218-
219-
debugger;
220-
delayDelete(stocks, names[0], 400);
221-
});
144+
145+
it('should be able to filter stateChanges() types - added', async (done) => {
146+
const randomCollectionName = randomName(afs.firestore);
147+
const ref = afs.firestore.collection(`${randomCollectionName}`);
148+
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
149+
const ITEMS = 10;
150+
let count = 0;
151+
let names = await createRandomStocks(afs.firestore, ref, ITEMS);
152+
153+
const sub = stocks.stateChanges(['added']).skip(1).subscribe(data => {
154+
sub.unsubscribe();
155+
expect(data.length).toEqual(1);
156+
expect(data[0].payload.doc.data().price).toEqual(2);
157+
expect(data[0].type).toEqual('added');
158+
deleteThemAll(names, ref).then(done).catch(done.fail);
159+
done();
160+
});
161+
162+
const nextId = ref.doc('a').id;
163+
names = names.concat([nextId]);
164+
delayAdd(stocks, nextId, { price: 2 });
165+
});
166+
167+
it('should be able to filter stateChanges() types - removed', async (done) => {
168+
const randomCollectionName = randomName(afs.firestore);
169+
const ref = afs.firestore.collection(`${randomCollectionName}`);
170+
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
171+
const ITEMS = 10;
172+
let names = await createRandomStocks(afs.firestore, ref, ITEMS);
173+
174+
const sub = stocks.stateChanges(['removed']).subscribe(data => {
175+
sub.unsubscribe();
176+
expect(data.length).toEqual(1);
177+
expect(data[0].type).toEqual('removed');
178+
deleteThemAll(names, ref).then(done).catch(done.fail);
179+
done();
180+
});
181+
182+
delayDelete(stocks, names[0], 400);
183+
});
184+
});
222185

223186
});

src/firestore/collection/collection.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Injectable } from '@angular/core';
1313
import { FirebaseApp } from 'angularfire2';
1414

1515
import { QueryFn, AssociatedReference, DocumentChangeAction } from '../interfaces';
16-
import { changes } from './changes';
16+
import { docChanges, sortedChanges } from './changes';
1717
import { AngularFirestoreDocument } from '../document/document';
1818

1919
export function validateEventsArray(events?: DocumentChangeType[]) {
@@ -69,24 +69,30 @@ export class AngularFirestoreCollection<T> {
6969
* This method returns a stream of DocumentSnapshots which gives the ability to get
7070
* the data set back as array and/or the delta updates in the collection.
7171
*/
72-
snapshotChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
72+
stateChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
7373
if(!events || events.length === 0) {
74-
return changes(this.query);
74+
return docChanges(this.query);
7575
}
76-
return changes(this.query)
76+
return docChanges(this.query)
7777
.map(actions => {
7878
debugger;
7979
return actions.filter(change => events.indexOf(change.type) > -1);
8080
})
8181
.filter(changes => changes.length > 0);
8282
}
8383

84+
snapshotChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
85+
events = validateEventsArray(events);
86+
return sortedChanges(this.query, events);
87+
}
88+
89+
8490
/**
8591
* Listen to all documents in the collection and its possible query as an Observable.
8692
* This method returns a stream of unwrapped snapshots.
8793
*/
8894
valueChanges(events?: DocumentChangeType[]): Observable<T[]> {
89-
return this.snapshotChanges()
95+
return this.stateChanges()
9096
.map(actions => actions.map(a => a.payload.doc.data()) as T[]);
9197
}
9298

src/firestore/document/document.spec.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,7 @@ import { Subscription } from 'rxjs/Subscription';
1111
import { TestBed, inject } from '@angular/core/testing';
1212
import { COMMON_CONFIG } from '../test-config';
1313

14-
interface Stock {
15-
name: string;
16-
price: number;
17-
}
18-
19-
const FAKE_STOCK_DATA = { name: 'FAKE', price: 1 };
20-
21-
const randomName = (firestore): string => firestore.collection('a').doc().id;
22-
23-
const createRandomStocks = async (firestore: firestore.Firestore, collectionRef: firestore.CollectionReference, numberOfItems) => {
24-
// Create a batch to update everything at once
25-
const batch = firestore.batch();
26-
// Store the random names to delete them later
27-
let count = 0;
28-
let names: string[] = [];
29-
Array.from(Array(numberOfItems)).forEach((a, i) => {
30-
const name = randomName(firestore);
31-
batch.set(collectionRef.doc(name), FAKE_STOCK_DATA);
32-
names = [...names, name];
33-
});
34-
// Create the batch entries
35-
// Commit!
36-
await batch.commit();
37-
return names;
38-
}
14+
import { Stock, randomName, FAKE_STOCK_DATA } from '../utils.spec';
3915

4016
describe('AngularFirestoreDocument', () => {
4117
let app: firebase.app.App;

0 commit comments

Comments
 (0)