Skip to content

fix(afs): fixing the metadata in snapshotChanges and more #2670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sample/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { MessagingComponent } from './messaging/messaging.component';
import { FunctionsComponent } from './functions/functions.component';
import { FirestoreOfflineComponent } from './firestore-offline/firestore-offline.component';
import { FirestoreOfflineModule } from './firestore-offline/firestore-offline.module';
import { UpboatsComponent } from './upboats/upboats.component';

@NgModule({
declarations: [
Expand All @@ -50,6 +51,7 @@ import { FirestoreOfflineModule } from './firestore-offline/firestore-offline.mo
AuthComponent,
MessagingComponent,
FunctionsComponent,
UpboatsComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
Expand Down
1 change: 1 addition & 0 deletions sample/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { FirebaseApp } from '@angular/fire';
<app-database></app-database>
<app-firestore></app-firestore>
<app-firestore-offline></app-firestore-offline>
<app-upboats></app-upboats>
<app-storage></app-storage>
<app-auth></app-auth>
<app-remote-config></app-remote-config>
Expand Down
8 changes: 8 additions & 0 deletions sample/src/app/protected-lazy/protected-lazy.component.html
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
<p>protected-lazy works!</p>

<ul>
<li *ngFor="let item of snapshot | async">
<pre>data(): {{ item.payload.doc.data() | json }},
id: {{ item.payload.doc.id }},
metadata: {{ item.payload.doc.metadata | json }}</pre>
</li>
</ul>
9 changes: 8 additions & 1 deletion sample/src/app/protected-lazy/protected-lazy.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { DocumentChangeAction } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { AngularFirestoreOffline } from '../firestore-offline/firestore-offline.module';

@Component({
selector: 'app-protected-lazy',
Expand All @@ -7,7 +10,11 @@ import { Component, OnInit } from '@angular/core';
})
export class ProtectedLazyComponent implements OnInit {

constructor() { }
public snapshot: Observable<DocumentChangeAction<unknown>[]>;

constructor(private afs: AngularFirestoreOffline) {
this.snapshot = afs.collection('test').snapshotChanges();
}

ngOnInit(): void {
}
Expand Down
Empty file.
11 changes: 11 additions & 0 deletions sample/src/app/upboats/upboats.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<ul>
<li *ngFor="let animal of animals | async">
<span>{{ animal.name }}</span>
<button (click)="upboat(animal.id)">👍</button>
<span>{{ animal.upboats }}</span>
<button (click)="downboat(animal.id)">👎</button>
<span *ngIf="animal.hasPendingWrites">🕒</span>
</li>
</ul>

<button (click)="newAnimal()">New animal</button>
25 changes: 25 additions & 0 deletions sample/src/app/upboats/upboats.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { UpboatsComponent } from './upboats.component';

describe('UpboatsComponent', () => {
let component: UpboatsComponent;
let fixture: ComponentFixture<UpboatsComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ UpboatsComponent ]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(UpboatsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
65 changes: 65 additions & 0 deletions sample/src/app/upboats/upboats.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map, startWith, tap } from 'rxjs/operators';
import { AngularFirestoreOffline } from '../firestore-offline/firestore-offline.module';
import firebase from 'firebase/app';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { trace } from '@angular/fire/performance';

type Animal = { name: string, upboats: number, id: string, hasPendingWrites: boolean };

@Component({
selector: 'app-upboats',
templateUrl: './upboats.component.html',
styleUrls: ['./upboats.component.css']
})
export class UpboatsComponent implements OnInit {

public animals: Observable<Animal[]>;

constructor(private firestore: AngularFirestoreOffline, state: TransferState) {
const collection = firestore.collection<Animal>('animals', ref =>
ref.orderBy('upboats', 'desc').orderBy('updatedAt', 'desc')
);
const key = makeStateKey(collection.ref.path);
const existing = state.get(key, undefined);
this.animals = collection.snapshotChanges().pipe(
trace('animals'),
map(it => it.map(change => ({
...change.payload.doc.data(),
id: change.payload.doc.id,
hasPendingWrites: change.payload.doc.metadata.hasPendingWrites
}))),
existing ? startWith(existing) : tap(it => state.set(key, it))
);
}

ngOnInit(): void {
}

upboat(id: string) {
// TODO add rule
this.firestore.doc(`animals/${id}`).update({
upboats: firebase.firestore.FieldValue.increment(1),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
});
}

downboat(id: string) {
// TODO add rule
this.firestore.doc(`animals/${id}`).update({
upboats: firebase.firestore.FieldValue.increment(-1),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
});
}

newAnimal() {
// TODO add rule
this.firestore.collection('animals').add({
name: prompt('Can haz name?'),
upboats: 1,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
});
}

}
54 changes: 46 additions & 8 deletions src/firestore/collection/changes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fromCollectionRef } from '../observable/fromRef';
import { Observable, SchedulerLike } from 'rxjs';
import { map, scan } from 'rxjs/operators';
import { distinctUntilChanged, map, pairwise, scan, startWith } from 'rxjs/operators';

import { DocumentChange, DocumentChangeAction, DocumentChangeType, Query } from '../interfaces';

Expand All @@ -25,8 +25,29 @@ export function sortedChanges<T>(
scheduler?: SchedulerLike): Observable<DocumentChangeAction<T>[]> {
return fromCollectionRef(query, scheduler)
.pipe(
map(changes => changes.payload.docChanges()),
scan((current, changes) => combineChanges(current, changes, events), []),
startWith(undefined),
pairwise(),
scan((current, [priorChanges, changes]) => {
const docChanges = changes.payload.docChanges();
const ret = combineChanges(current, docChanges, events);
// docChanges({ includeMetadataChanges: true }) does't include metadata changes... wat?
if (events.indexOf('modified') > -1 && priorChanges &&
JSON.stringify(priorChanges.payload.metadata) !== JSON.stringify(changes.payload.metadata)) {
return ret.map(it => {
const partOfDocChanges = !!docChanges.find(d => d.doc.ref.isEqual(it.doc.ref));
return {
// if it's not one of the changed docs that means we already saw it's order change
// so this is purely metadata, so don't move the doc
oldIndex: partOfDocChanges ? it.oldIndex : it.newIndex,
newIndex: it.newIndex,
type: 'modified',
doc: changes.payload.docs.find(d => d.ref.isEqual(it.doc.ref))
};
});
}
return ret;
}, []),
distinctUntilChanged(), // cut down on unneed change cycles
map(changes => changes.map(c => ({ type: c.type, payload: c } as DocumentChangeAction<T>))));
}

Expand All @@ -44,6 +65,21 @@ export function combineChanges<T>(current: DocumentChange<T>[], changes: Documen
return current;
}

/**
* Splice arguments on top of a sliced array, to break top-level ===
* this is useful for change-detection
*/
function sliceAndSplice<T>(
original: T[],
start: number,
deleteCount: number,
...args: T[]
): T[] {
const returnArray = original.slice();
returnArray.splice(start, deleteCount, ...args);
return returnArray;
}

/**
* Creates a new sorted array from a new change.
*/
Expand All @@ -53,24 +89,26 @@ export function combineChange<T>(combined: DocumentChange<T>[], change: Document
if (combined[change.newIndex] && combined[change.newIndex].doc.ref.isEqual(change.doc.ref)) {
// Not sure why the duplicates are getting fired
} else {
combined.splice(change.newIndex, 0, change);
return sliceAndSplice(combined, change.newIndex, 0, change);
}
break;
case 'modified':
if (combined[change.oldIndex] == null || combined[change.oldIndex].doc.ref.isEqual(change.doc.ref)) {
// When an item changes position we first remove it
// and then add it's new position
if (change.oldIndex !== change.newIndex) {
combined.splice(change.oldIndex, 1);
combined.splice(change.newIndex, 0, change);
const copiedArray = combined.slice();
copiedArray.splice(change.oldIndex, 1);
copiedArray.splice(change.newIndex, 0, change);
return copiedArray;
} else {
combined.splice(change.newIndex, 1, change);
return sliceAndSplice(combined, change.newIndex, 1, change);
}
}
break;
case 'removed':
if (combined[change.oldIndex] && combined[change.oldIndex].doc.ref.isEqual(change.doc.ref)) {
combined.splice(change.oldIndex, 1);
return sliceAndSplice(combined, change.oldIndex, 1);
}
break;
}
Expand Down