From 2ceab8fcaf946ab159490366567e69dc2efbb55e Mon Sep 17 00:00:00 2001 From: Patrick Michalina Date: Wed, 4 Apr 2018 20:05:00 -0500 Subject: [PATCH] feat: introduce rtdb server-client caching --- src/core/angularfire2.ts | 2 ++ src/core/firebase.app.module.ts | 5 +++-- src/database/database-server.module.ts | 8 ++++++++ src/database/database.module.ts | 3 ++- src/database/database.spec.ts | 12 +++++++++--- src/database/database.ts | 22 +++++++++++++++++++--- src/database/list/create-reference.ts | 2 +- src/database/object/create-reference.ts | 11 ++++++++--- src/database/public_api.ts | 1 + 9 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 src/database/database-server.module.ts diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index a6933fb38..ba814b552 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -17,6 +17,8 @@ export const FirebaseAppConfig = new InjectionToken('angularfir // Put in database.ts when we drop database-depreciated export const RealtimeDatabaseURL = new InjectionToken('angularfire2.realtimeDatabaseURL'); +export const UniversalDatabaseTransferStateKeyPrefix = new InjectionToken('angularfire2.dbTransferStateKey'); + export class FirebaseZoneScheduler { constructor(public zone: NgZone) {} schedule(...args: any[]): Subscription { diff --git a/src/core/firebase.app.module.ts b/src/core/firebase.app.module.ts index 969a30364..526f9edd0 100644 --- a/src/core/firebase.app.module.ts +++ b/src/core/firebase.app.module.ts @@ -1,6 +1,6 @@ import { InjectionToken, NgZone, NgModule } from '@angular/core'; -import { FirebaseAppConfig, FirebaseAppName } from './angularfire2'; +import { FirebaseAppConfig, FirebaseAppName, UniversalDatabaseTransferStateKeyPrefix } from './angularfire2'; import firebase from '@firebase/app'; import { FirebaseApp as _FirebaseApp, FirebaseOptions } from '@firebase/app-types'; @@ -42,7 +42,8 @@ export class AngularFireModule { ngModule: AngularFireModule, providers: [ { provide: FirebaseAppConfig, useValue: config }, - { provide: FirebaseAppName, useValue: appName } + { provide: FirebaseAppName, useValue: appName }, + { provide: UniversalDatabaseTransferStateKeyPrefix, useValue: 'RTDB' } ] } } diff --git a/src/database/database-server.module.ts b/src/database/database-server.module.ts new file mode 100644 index 000000000..af34955c2 --- /dev/null +++ b/src/database/database-server.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core'; +import { AngularFireDatabase } from './database'; +import '@firebase/database'; + +@NgModule({ + providers: [ AngularFireDatabase ] +}) +export class AngularFireDatabaseServerModule { } diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 58aa66767..9206e4853 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -1,8 +1,9 @@ import { NgModule } from '@angular/core'; import { AngularFireDatabase } from './database'; +import { TransferState } from '@angular/platform-browser'; import '@firebase/database'; @NgModule({ - providers: [ AngularFireDatabase ] + providers: [ AngularFireDatabase, TransferState ] }) export class AngularFireDatabaseModule { } diff --git a/src/database/database.spec.ts b/src/database/database.spec.ts index af0c4314a..bbf90a4cf 100644 --- a/src/database/database.spec.ts +++ b/src/database/database.spec.ts @@ -2,7 +2,8 @@ import { FirebaseApp, FirebaseAppConfig, AngularFireModule, FirebaseAppName } fr import { AngularFireDatabase, AngularFireDatabaseModule, RealtimeDatabaseURL } from 'angularfire2/database'; import { TestBed, inject } from '@angular/core/testing'; import { COMMON_CONFIG } from './test-config'; -import { NgZone } from '@angular/core'; +import { NgZone, ApplicationRef } from '@angular/core'; +import { TransferState } from '@angular/platform-browser'; // generate random string to test fidelity of naming const FIREBASE_APP_NAME = (Math.random() + 1).toString(36).substring(7); @@ -11,6 +12,8 @@ describe('AngularFireDatabase', () => { let app: FirebaseApp; let db: AngularFireDatabase; let zone: NgZone + let ts: TransferState + let appRef: ApplicationRef beforeEach(() => { TestBed.configureTestingModule({ @@ -19,10 +22,13 @@ describe('AngularFireDatabase', () => { AngularFireDatabaseModule ] }); - inject([FirebaseApp, AngularFireDatabase, NgZone], (app_: FirebaseApp, _db: AngularFireDatabase, _zone: NgZone) => { + inject([FirebaseApp, AngularFireDatabase, NgZone, TransferState, ApplicationRef], + (app_: FirebaseApp, _db: AngularFireDatabase, _zone: NgZone, _ts: TransferState, _appRef: ApplicationRef) => { app = app_; db = _db; zone = _zone; + ts = _ts; + appRef = _appRef })(); }); @@ -42,7 +48,7 @@ describe('AngularFireDatabase', () => { }); it('should accept a Firebase App in the constructor', () => { - const __db = new AngularFireDatabase(app.options, app.name, null!, zone); + const __db = new AngularFireDatabase(app.options, app.name, null!, 'RTDB', ts, appRef, zone); expect(__db instanceof AngularFireDatabase).toEqual(true); }); diff --git a/src/database/database.ts b/src/database/database.ts index 1bb7b85ae..e813b52ef 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject, Optional, NgZone } from '@angular/core'; +import { Injectable, Inject, Optional, NgZone, ApplicationRef } from '@angular/core'; import { FirebaseDatabase } from '@firebase/database-types'; import { PathReference, DatabaseQuery, DatabaseReference, DatabaseSnapshot, ChildEvent, ListenEvent, QueryFn, AngularFireList, AngularFireObject } from './interfaces'; import { getRef } from './utils'; @@ -6,17 +6,23 @@ import { InjectionToken } from '@angular/core'; import { FirebaseOptions } from '@firebase/app-types'; import { createListReference } from './list/create-reference'; import { createObjectReference } from './object/create-reference'; -import { FirebaseAppConfig, FirebaseAppName, RealtimeDatabaseURL, _firebaseAppFactory, FirebaseZoneScheduler } from 'angularfire2'; +import { FirebaseAppConfig, FirebaseAppName, RealtimeDatabaseURL, UniversalDatabaseTransferStateKeyPrefix, _firebaseAppFactory, FirebaseZoneScheduler } from 'angularfire2'; +import { makeStateKey, TransferState } from '@angular/platform-browser'; +import 'rxjs/add/operator/take'; @Injectable() export class AngularFireDatabase { public readonly database: FirebaseDatabase; public readonly scheduler: FirebaseZoneScheduler; + private readFromCache = true; constructor( @Inject(FirebaseAppConfig) config:FirebaseOptions, @Optional() @Inject(FirebaseAppName) name:string, @Optional() @Inject(RealtimeDatabaseURL) databaseURL:string, + @Inject(UniversalDatabaseTransferStateKeyPrefix) private cacheKeyPrefix:string, + private ts: TransferState, + appRef: ApplicationRef, zone: NgZone ) { this.scheduler = new FirebaseZoneScheduler(zone); @@ -24,10 +30,12 @@ export class AngularFireDatabase { const app = _firebaseAppFactory(config, name); return app.database(databaseURL || undefined); }); + appRef.isStable.filter(Boolean).take(1).subscribe(() => this.readFromCache = false) } list(pathOrRef: PathReference, queryFn?: QueryFn): AngularFireList { const ref = getRef(this.database, pathOrRef); + const ssrCached = this.readFromCache && this.getValueFromServerRequest(pathOrRef.toString()) || undefined let query: DatabaseQuery = ref; if(queryFn) { query = queryFn(ref); @@ -37,13 +45,21 @@ export class AngularFireDatabase { object(pathOrRef: PathReference): AngularFireObject { const ref = getRef(this.database, pathOrRef); - return createObjectReference(ref, this); + const ssrCached = this.readFromCache && this.getValueFromServerRequest(pathOrRef.toString()) || undefined + return createObjectReference(ref, this, ssrCached); } createPushId() { return this.database.ref().push().key; } + private getValueFromServerRequest(path: string) { + return this.ts.get(this.getTsCacheKey(path), undefined); + } + + private getTsCacheKey(path: string) { + return makeStateKey(`${this.cacheKeyPrefix}.${path}`); + } } export { diff --git a/src/database/list/create-reference.ts b/src/database/list/create-reference.ts index 878515a44..dac907940 100644 --- a/src/database/list/create-reference.ts +++ b/src/database/list/create-reference.ts @@ -6,7 +6,7 @@ import { createDataOperationMethod } from './data-operation'; import { createRemoveMethod } from './remove'; import { AngularFireDatabase } from '../database'; -export function createListReference(query: DatabaseQuery, afDatabase: AngularFireDatabase): AngularFireList { +export function createListReference(query: DatabaseQuery, afDatabase: AngularFireDatabase, ssrCached?: T[]): AngularFireList { return { query, update: createDataOperationMethod>(query.ref, 'update'), diff --git a/src/database/object/create-reference.ts b/src/database/object/create-reference.ts index bc2f2fbd2..a368c85ac 100644 --- a/src/database/object/create-reference.ts +++ b/src/database/object/create-reference.ts @@ -1,8 +1,9 @@ import { DatabaseQuery, AngularFireObject } from '../interfaces'; import { createObjectSnapshotChanges } from './snapshot-changes'; import { AngularFireDatabase } from '../database'; +import 'rxjs/add/operator/startWith'; -export function createObjectReference(query: DatabaseQuery, afDatabase: AngularFireDatabase): AngularFireObject { +export function createObjectReference(query: DatabaseQuery, afDatabase: AngularFireDatabase, ssrCached?: T): AngularFireObject { return { query, snapshotChanges() { @@ -12,10 +13,14 @@ export function createObjectReference(query: DatabaseQuery, afDatabase: Angul update(data: Partial) { return query.ref.update(data as any) as Promise; }, set(data: T) { return query.ref.set(data) as Promise; }, remove() { return query.ref.remove() as Promise; }, - valueChanges() { + valueChanges() { const snapshotChanges$ = createObjectSnapshotChanges(query)(); - return afDatabase.scheduler.keepUnstableUntilFirst(snapshotChanges$) + const baseObs = afDatabase.scheduler.keepUnstableUntilFirst(snapshotChanges$) .map(action => action.payload.exists() ? action.payload.val() as T : null) + + return ssrCached + ? baseObs.startWith(ssrCached as any) // typescript issue? + : baseObs }, } } diff --git a/src/database/public_api.ts b/src/database/public_api.ts index bac23e359..0a24a227f 100644 --- a/src/database/public_api.ts +++ b/src/database/public_api.ts @@ -6,3 +6,4 @@ export * from './list/state-changes'; export * from './list/audit-trail'; export * from './observable/fromRef'; export * from './database.module' +export * from './database-server.module' \ No newline at end of file