Skip to content

Commit fce594b

Browse files
authored
fix(afs): stateChanges and auditLog correctly emit metadata changes (#2684)
* `stateChanges` and `auditLog` will show metadata updates correctly now, rather than just `[]` * `snapshotChanges` uses same codepath as `stateChanges` and `auditLog` * filter out empty arrays from `stateChanges`, even when no options provided
1 parent d36544f commit fce594b

File tree

3 files changed

+38
-28
lines changed

3 files changed

+38
-28
lines changed

sample/src/app/app.component.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ApplicationRef, Component } from '@angular/core';
22
import { FirebaseApp } from '@angular/fire';
3+
import { debounceTime } from 'rxjs/operators';
34

45
@Component({
56
selector: 'app-root',
@@ -25,6 +26,6 @@ import { FirebaseApp } from '@angular/fire';
2526
})
2627
export class AppComponent {
2728
constructor(public readonly firebaseApp: FirebaseApp, appRef: ApplicationRef) {
28-
appRef.isStable.subscribe(it => console.log('isStable', it));
29+
appRef.isStable.pipe(debounceTime(200)).subscribe(it => console.log('isStable', it));
2930
}
3031
}

src/firestore/collection/changes.ts

+35-27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { fromCollectionRef } from '../observable/fromRef';
22
import { Observable, SchedulerLike } from 'rxjs';
33
import { distinctUntilChanged, map, pairwise, scan, startWith } from 'rxjs/operators';
4-
54
import { DocumentChange, DocumentChangeAction, DocumentChangeType, Query } from '../interfaces';
65

76
/**
@@ -11,9 +10,37 @@ import { DocumentChange, DocumentChangeAction, DocumentChangeType, Query } from
1110
export function docChanges<T>(query: Query, scheduler?: SchedulerLike): Observable<DocumentChangeAction<T>[]> {
1211
return fromCollectionRef(query, scheduler)
1312
.pipe(
14-
map(action =>
15-
action.payload.docChanges()
16-
.map(change => ({ type: change.type, payload: change } as DocumentChangeAction<T>))));
13+
startWith(undefined),
14+
pairwise(),
15+
map(([priorAction, action]) => {
16+
const docChanges = action.payload.docChanges();
17+
const actions = docChanges.map(change => ({ type: change.type, payload: change }));
18+
// the metadata has changed from the prior emission
19+
if (priorAction && JSON.stringify(priorAction.payload.metadata) !== JSON.stringify(action.payload.metadata)) {
20+
// go through all the docs in payload and figure out which ones changed
21+
action.payload.docs.forEach((currentDoc, currentIndex) => {
22+
const docChange = docChanges.find(d => d.doc.ref.isEqual(currentDoc.ref));
23+
const priorDoc = priorAction?.payload.docs.find(d => d.ref.isEqual(currentDoc.ref));
24+
if (docChange && JSON.stringify(docChange.doc.metadata) === JSON.stringify(currentDoc.metadata) ||
25+
!docChange && priorDoc && JSON.stringify(priorDoc.metadata) === JSON.stringify(currentDoc.metadata)) {
26+
// document doesn't appear to have changed, don't log another action
27+
} else {
28+
// since the actions are processed in order just push onto the array
29+
actions.push({
30+
type: 'modified',
31+
payload: {
32+
oldIndex: currentIndex,
33+
newIndex: currentIndex,
34+
type: 'modified',
35+
doc: currentDoc
36+
}
37+
});
38+
}
39+
});
40+
}
41+
return actions as DocumentChangeAction<T>[];
42+
}),
43+
);
1744
}
1845

1946
/**
@@ -23,30 +50,9 @@ export function sortedChanges<T>(
2350
query: Query,
2451
events: DocumentChangeType[],
2552
scheduler?: SchedulerLike): Observable<DocumentChangeAction<T>[]> {
26-
return fromCollectionRef(query, scheduler)
53+
return docChanges<T>(query, scheduler)
2754
.pipe(
28-
startWith(undefined),
29-
pairwise(),
30-
scan((current, [priorChanges, changes]) => {
31-
const docChanges = changes.payload.docChanges();
32-
const ret = combineChanges(current, docChanges, events);
33-
// docChanges({ includeMetadataChanges: true }) does't include metadata changes... wat?
34-
if (events.indexOf('modified') > -1 && priorChanges &&
35-
JSON.stringify(priorChanges.payload.metadata) !== JSON.stringify(changes.payload.metadata)) {
36-
return ret.map(it => {
37-
const partOfDocChanges = !!docChanges.find(d => d.doc.ref.isEqual(it.doc.ref));
38-
return {
39-
// if it's not one of the changed docs that means we already saw it's order change
40-
// so this is purely metadata, so don't move the doc
41-
oldIndex: partOfDocChanges ? it.oldIndex : it.newIndex,
42-
newIndex: it.newIndex,
43-
type: 'modified',
44-
doc: changes.payload.docs.find(d => d.ref.isEqual(it.doc.ref))
45-
};
46-
});
47-
}
48-
return ret;
49-
}, []),
55+
scan((current, changes) => combineChanges<T>(current, changes.map(it => it.payload), events), []),
5056
distinctUntilChanged(), // cut down on unneed change cycles
5157
map(changes => changes.map(c => ({ type: c.type, payload: c } as DocumentChangeAction<T>))));
5258
}
@@ -82,6 +88,8 @@ function sliceAndSplice<T>(
8288

8389
/**
8490
* Creates a new sorted array from a new change.
91+
* Build our own because we allow filtering of action types ('added', 'removed', 'modified') before scanning
92+
* and so we have greater control over change detection (by breaking ===)
8593
*/
8694
export function combineChange<T>(combined: DocumentChange<T>[], change: DocumentChange<T>): DocumentChange<T>[] {
8795
switch (change.type) {

src/firestore/collection/collection.ts

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class AngularFirestoreCollection<T = DocumentData> {
6161
stateChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
6262
if (!events || events.length === 0) {
6363
return docChanges<T>(this.query, this.afs.schedulers.outsideAngular).pipe(
64+
filter(changes => changes.length > 0),
6465
this.afs.keepUnstableUntilFirst
6566
);
6667
}

0 commit comments

Comments
 (0)