import { AbstractDatastore , BinaryFileData , Datastore , DatastoreCapabilities , DatastoreConsistency , DatastoreInitOpts , DatastoreOverview , DefaultWriteFileOpts , DeleteResult , DocMetaMutation , DocMetaSnapshotEvent , DocMetaSnapshotEventListener , DocMetaSnapshotOpts , DocMetaSnapshotResult , ErrorListener , FileMeta , GroupIDStr , InitResult , MutationType , SnapshotResult , WritableBinaryMetaDatastore , WriteController , WriteFileOpts , WriteFileProgress , WriteOpts } from './Datastore' ; import { Logger } from 'polar-shared/src/logger/Logger' ; import { DocMetaFileRef , DocMetaFileRefs , DocMetaRef } from './DocMetaRef' ; import { Backend } from 'polar-shared/src/datastore/Backend' ; import { DocFileMeta } from 'polar-shared/src/datastore/DocFileMeta' ; import { Firestore } from '../firebase/Firestore' ; import { IDocInfo } from 'polar-shared/src/metadata/IDocInfo' ; import { isPresent , Preconditions } from 'polar-shared/src/Preconditions' ; import firebase from 'firebase/app' ; import 'firebase/storage' ; import { Dictionaries } from 'polar-shared/src/util/Dictionaries' ; import { DatastoreMutation , DefaultDatastoreMutation } from './DatastoreMutation' ; import { NULL_FUNCTION } from 'polar-shared/src/util/Functions' ; import { DocMetas } from "../metadata/DocMetas" ; import { Percentages } from 'polar-shared/src/util/Percentages' ; import { Percentage , ProgressTracker } from 'polar-shared/src/util/ProgressTracker' ; import { AsyncProviders } from 'polar-shared/src/util/Providers' ; import { FilePaths } from 'polar-shared/src/util/FilePaths' ; import { FileHandle , FileHandles } from 'polar-shared/src/util/Files' ; import { Firebase , UserID } from '../firebase/Firebase' ; import { IEventDispatcher , SimpleReactor } from '../reactor/SimpleReactor' ; import { ProgressMessage } from '../ui/progress_bar/ProgressMessage' ; import { ProgressMessages } from '../ui/progress_bar/ProgressMessages' ; import { Stopwatches } from 'polar-shared/src/util/Stopwatches' ; import { URLs } from 'polar-shared/src/util/URLs' ; import { Datastores } from './Datastores' ; import { FirebaseDatastores } from './FirebaseDatastores' ; import { DocPermissions } from "./sharing/db/DocPermissions" ; import { Visibility } from "polar-shared/src/datastore/Visibility" ; import { FileRef } from "polar-shared/src/datastore/FileRef" ; import { Latch } from "polar-shared/src/util/Latch" ; import { GetFileOpts , NetworkLayers } from "polar-shared/src/datastore/IDatastore" ; import { SnapshotUnsubscriber } from 'polar-shared/src/util/Snapshots' ; import { IQuerySnapshot } from "polar-snapshot-cache/src/store/IQuerySnapshot" ; import { IDocumentChange } from "polar-snapshot-cache/src/store/IDocumentChange" ; import { IDocumentReference } from "polar-snapshot-cache/src/store/IDocumentReference" ; import { IDocumentSnapshot } from 'polar-snapshot-cache/src/store/IDocumentSnapshot' ; import { IFirestore } from "polar-snapshot-cache/src/store/IFirestore" ; const log = Logger . create ( ) ; let STORAGE_UPLOAD_ID = 0 ; export class FirebaseDatastore extends AbstractDatastore implements Datastore , WritableBinaryMetaDatastore { public readonly id = 'firebase' ; private app ? : firebase . app . App ; private firestore ? : IFirestore ; private storage ? : firebase . storage . Storage ; private initialized : boolean = false ; private primarySnapshot ? : SnapshotResult ; private uid : string = '' ; private readonly docMetaSnapshotEventDispatcher : IEventDispatcher < DocMetaSnapshotEvent > = new SimpleReactor ( ) ; constructor ( ) { super ( ) ; } public async init ( errorListener = NULL_FUNCTION , opts = { } ) { if ( this . initialized ) { return { } ; } log . notice ( "Initializing FirebaseDatastore..." ) ; this . app = firebase . app ( ) ; this . firestore = await Firestore . getInstance ( ) ; this . storage = firebase . storage ( ) ; this . uid = ( await Firebase . currentUserID ( ) ) ! ; if ( opts . noInitialSnapshot ) { log . debug ( "Skipping initial snapshot" ) ; } else if ( this . uid === undefined ) { log . debug ( "Skipping initial snapshot: no user" ) ; } else { log . debug ( "Performing initial snapshot" ) ; const snapshotListener = async ( event ) => this . docMetaSnapshotEventDispatcher . dispatchEvent ( event ) ; this . primarySnapshot = await this . snapshot ( snapshotListener , errorListener ) ; } this . initialized = true ; log . notice ( "Initializing FirebaseDatastore...done" ) ; return { } ; } public async snapshot ( docMetaSnapshotEventListener , errorListener = NULL_FUNCTION ) { const uid = this . uid ; const firestore = this . firestore ! ; const query = firestore . collection ( DatastoreCollection . DOC_META ) . where ( 'uid' , '==' , uid ) ; type BatchIDMap = { [ P in DatastoreConsistency ] : number ; } ; const batchIDs = { written : 0 , committed : 0 } ; const onNextForSnapshot = ( snapshot ) => { try { const consistency = this . toConsistency ( snapshot ) ; const batchID = batchIDs [ consistency ] ; this . handleDocMetaSnapshot ( snapshot , docMetaSnapshotEventListener , batchID ) ; batchIDs [ consistency ] ++ ; } catch ( e ) { log . error ( "Could not handle snapshot: " , e ) ; errorListener ( e ) ; } } ; const onSnapshotError = ( err ) => { log . error ( "Could not handle snapshot: " , err ) ; errorListener ( err ) ; } ; try { const stopwatch = Stopwatches . create ( ) ; const cachedSnapshot = await query . get ( { source : 'cache' } ) ; console . log ( "Initial cached snapshot duration: " + stopwatch . stop ( ) ) ; onNextForSnapshot ( cachedSnapshot ) ; } catch ( e ) { } const unsubscribe = query . onSnapshot ( { includeMetadataChanges : true } , onNextForSnapshot , onSnapshotError ) ; return { unsubscribe } ; } public async stop ( ) { if ( this . primarySnapshot && this . primarySnapshot . unsubscribe ) { this . primarySnapshot . unsubscribe ( ) ; } } /**
     * Return true if the DiskDatastore contains a document for the given
     * fingerprint
     */ public async contains ( fingerprint ) { const docMeta = await this . getDocMeta ( fingerprint ) ; return docMeta !== null ; } public async delete ( docMetaFileRef , datastoreMutation = new DefaultDatastoreMutation ( ) ) { const firestore = this . firestore ! ; log . info ( "delete: " , docMetaFileRef ) ; if ( docMetaFileRef . docFile && docMetaFileRef . docFile . name ) { await this . deleteFile ( Backend . STASH , docMetaFileRef . docFile ) ; } const id = FirebaseDatastores . computeDocMetaID ( docMetaFileRef . fingerprint , this . uid ) ; const docInfoRef = firestore . collection ( DatastoreCollection . DOC_INFO ) . doc ( id ) ; const docMetaRef = firestore ! . collection ( DatastoreCollection . DOC_META ) . doc ( id ) ; try { this . handleDatastoreMutations ( docMetaRef , datastoreMutation , 'delete' ) ; const commitPromise = Promise . all ( [ this . waitForCommit ( docMetaRef ) , this . waitForCommit ( docInfoRef ) ] ) ; const batch = firestore . batch ( ) ; batch . delete ( docInfoRef ) ; batch . delete ( docMetaRef ) ; await batch . commit ( ) ; await commitPromise ; return { } ; } finally { } } /**
     * Get the DocMeta we currently in the datastore for this given
     * fingerprint or null if it does not exist.
     */ public async getDocMeta ( fingerprint , opts = { } ) { const id = FirebaseDatastores . computeDocMetaID ( fingerprint , this . uid ) ; return await this . getDocMetaDirectly ( id , opts ) ; } public async getDocMetaSnapshot ( opts ) { const firestore = this . firestore ! ; const { fingerprint } = opts ; const id = FirebaseDatastores . computeDocMetaID ( fingerprint , this . uid ) ; const ref = firestore . collection ( DatastoreCollection . DOC_META ) . doc ( id ) ; let unsubscriber = NULL_FUNCTION ; const onNext = ( snapshot ) => { const source = snapshot . metadata . fromCache ? 'cache' : 'server' ; const hasPendingWrites = snapshot . metadata . hasPendingWrites ; console . log ( `DocMeta snapshot source=${ source }, hasPendingWrites: ${ hasPendingWrites }` ) ; const recordHolder = < RecordHolder < DocMetaHolder > | undefined > snapshot . data ( ) ; opts . onSnapshot ( { exists : snapshot . exists , data : recordHolder ?. value ?. value , hasPendingWrites , source , unsubscriber } ) ; } ; const onError = ( err ) => { if ( opts . onError ) { opts . onError ( { err , unsubscriber } ) ; } } ; unsubscriber = ref . onSnapshot ( { includeMetadataChanges : true } , snapshot => onNext ( snapshot ) , err => onError ( err ) ) ; return { unsubscriber } ; } /**
     * Get the DocMeta if from the raw docID encoded into the users account.
     */ public async getDocMetaDirectly ( id , opts = { } ) { const firestore = this . firestore ! ; const ref = firestore . collection ( DatastoreCollection . DOC_META ) . doc ( id ) ; const createSnapshot = async ( ) => { const preferredSource = opts . preferredSource || this . preferredSource ( ) ; if ( preferredSource === 'cache' ) { console . log ( "getDocMeta: cache+server" ) ; const cachePromise = ref . get ( { source : 'cache' } ) ; const serverPromise = ref . get ( { source : 'server' } ) ; const cacheResult = await cachePromise ; if ( cacheResult . exists ) { return cacheResult ; } return await serverPromise ; } else if ( isPresent ( opts . preferredSource ) ) { console . log ( "getDocMeta: " + opts . preferredSource ) ; return await ref . get ( { source : opts . preferredSource } ) ; } else { console . log ( "getDocMeta: standard" ) ; return await ref . get ( ) ; } } ; const snapshot = await createSnapshot ( ) ; const recordHolder = < RecordHolder < DocMetaHolder > | undefined > snapshot . data ( ) ; if ( ! recordHolder ) { log . warn ( "Could not get docMeta with id: " + id ) ; return null ; } return recordHolder . value . value ; } /**
     * We have to keep track of pending file writes because it's possible that
     * two systems race and attempt to write the same file at once.
     *
     * TODO: We should probably abort this in the future if the hashcodes for
     * the documents we're trying to write are present but not identical or the
     * visibility settings are different.  Either that or stack them so that
     * the second (with different settings) is performed after the first.
     */ private pendingFileWrites : { [ key ] : Latch < DocFileMeta > ; } = { } ; public async writeFile ( backend , ref , data , opts = new DefaultWriteFileOpts ( ) ) { log . debug ( `writeFile: ${ backend }: ` , ref ) ; const storagePath = FirebaseDatastores . computeStoragePath ( backend , ref , this . uid ) ; const pendingFileWriteKey = storagePath . path ; let latch = this . pendingFileWrites [ pendingFileWriteKey ] ; if ( latch ) { log . warn ( "Write already pending.  Going to return latch." ) ; return this . pendingFileWrites [ pendingFileWriteKey ] . get ( ) ; } latch = this . pendingFileWrites [ pendingFileWriteKey ] = new Latch ( ) ; try { const visibility = opts . visibility || Visibility . PRIVATE ; const storage = this . storage ! ; const fileRef = storage . ref ( ) . child ( storagePath . path ) ; if ( ! isPresent ( data ) ) { if ( opts . updateMeta ) { const meta = { visibility } ; await fileRef . updateMetadata ( meta ) ; log . info ( "File metadata updated with: " , meta ) ; return this . getFile ( backend , ref ) ; } else { throw new Error ( "No data present" ) ; } } if ( await this . containsFile ( backend , ref ) ) { return this . getFile ( backend , ref ) ; } let uploadTask ; const uid = this . uid ; const meta = { uid , visibility } ; const metadata = { customMetadata : meta } ; if ( storagePath . settings ) { metadata . contentType = storagePath . settings . contentType ; metadata . cacheControl = storagePath . settings . cacheControl ; } if ( typeof data === 'string' ) { uploadTask = fileRef . putString ( data , 'raw' , metadata ) ; } else if ( data instanceof Blob ) { uploadTask = fileRef . put ( data , metadata ) ; } else { if ( FileHandles . isFileHandle ( data ) ) { const fileHandle = < FileHandle > data ; const fileURL = FilePaths . toURL ( fileHandle . path ) ; const blob = await URLs . toBlob ( fileURL ) ; uploadTask = fileRef . put ( blob , metadata ) ; } else { uploadTask = fileRef . put ( Uint8Array . from ( < Buffer > data ) , metadata ) ; } } const started = Date . now ( ) ; const task = ProgressTracker . createNonce ( ) ; const progressID = 'firebase-upload-' + STORAGE_UPLOAD_ID ++ ; const controller = { pause : ( ) => uploadTask . pause ( ) , resume : ( ) => uploadTask . resume ( ) , cancel : ( ) => uploadTask . cancel ( ) } ; if ( opts . onController ) { opts . onController ( controller ) ; } uploadTask . on ( 'state_changed' , ( snapshotData ) => { const snapshot = snapshotData ; const now = Date . now ( ) ; const duration = now - started ; const percentage = Percentages . calculate ( snapshot . bytesTransferred , snapshot . totalBytes ) ; log . notice ( 'Upload is ' + percentage + '% done' ) ; const progress = { id : progressID , task , completed : snapshot . bytesTransferred , total : snapshot . totalBytes , duration , progress : < Percentage > percentage , timestamp : Date . now ( ) , name : `${ backend }/${ ref . name }` } ; ProgressMessages . broadcast ( progress ) ; if ( opts . progressListener ) { const writeFileProgress = { ref : { backend , ... ref } , ... progress , value : progress . progress , type : 'determinate' } ; opts . progressListener ( writeFileProgress ) ; } switch ( snapshot . state ) { case firebase . storage . TaskState . PAUSED : break ; case firebase . storage . TaskState . RUNNING : break ; } } ) ; const uploadTaskSnapshot = await uploadTask ; const { downloadURL , bytesTransferred } = uploadTaskSnapshot ; const result = { backend , ref , url : downloadURL ! } ; latch . resolve ( result ) ; delete this . pendingFileWrites [ pendingFileWriteKey ] ; return result ; } catch ( e ) { latch . reject ( e ) ; throw e ; } } public getFile ( backend , ref , opts = { } ) { Datastores . assertNetworkLayer ( this , opts . networkLayer ) ; log . debug ( "getFile" ) ; const storage = this . storage ! ; const storagePath = FirebaseDatastores . computeStoragePath ( backend , ref , this . uid ) ; const storageRef = storage . ref ( ) . child ( storagePath . path ) ; const downloadURL = DownloadURLs . computeDownloadURL ( backend , ref , storagePath , storageRef , opts ) ; const url = this . wrappedDownloadURL ( downloadURL ) ; return { backend , ref , url } ; } /**
     * Optionally wrap the download URL with a middleware URL which can perform
     * operations like authentication for the underlying download URL.
     */ private wrappedDownloadURL ( url ) { return url ; } public async containsFile ( backend , ref ) { const storagePath = FirebaseDatastores . computeStoragePath ( backend , ref , this . uid ) ; const storage = this . storage ! ; const storageRef = storage . ref ( ) . child ( storagePath . path ) ; const downloadURL = DownloadURLs . computeDownloadURL ( backend , ref , storagePath , storageRef , { } ) ; return DownloadURLs . checkExistence ( downloadURL ) ; } public async deleteFile ( backend , ref ) { log . debug ( "deleteFile: " , backend , ref ) ; try { const storage = this . storage ! ; const storagePath = FirebaseDatastores . computeStoragePath ( backend , ref , this . uid ) ; const fileRef = storage . ref ( ) . child ( storagePath . path ) ; await fileRef . delete ( ) ; } catch ( e ) { if ( e . code === "storage/object-not-found" ) { return ; } throw e ; } } public async write ( fingerprint , data , docInfo , opts = new DefaultWriteOpts ( ) ) { const firestore = this . firestore ! ; await this . handleWriteFile ( opts ) ; const datastoreMutation = opts . datastoreMutation || new DefaultDatastoreMutation ( ) ; const id = FirebaseDatastores . computeDocMetaID ( fingerprint , this . uid ) ; /**
         * Create our two main doc refs.
         */ const createDocRefs = ( ) => { const docMetaRef = firestore . collection ( DatastoreCollection . DOC_META ) . doc ( id ) ; const docInfoRef = firestore . collection ( DatastoreCollection . DOC_INFO ) . doc ( id ) ; return [ docMetaRef , docInfoRef ] ; } ; try { docInfo = Object . assign ( { } , Dictionaries . onlyDefinedProperties ( docInfo ) ) ; const createRecordPermission = async ( ) : Promise < RecordPermission > => { const docPermission = await DocPermissions . get ( id ) ; if ( docPermission ) { return { visibility : docPermission . visibility , groups : docPermission . groups } ; } return { visibility : docInfo . visibility || Visibility . PRIVATE } ; } ; const recordPermission = Dictionaries . onlyDefinedProperties ( await createRecordPermission ( ) ) ; const batch = firestore . batch ( ) ; const dataLen = data . length ; log . notice ( `Write of doc with id ${ id }, and data length ${ dataLen } and permission: ` , recordPermission ) ; const [ docMetaRef , docInfoRef ] = createDocRefs ( ) ; batch . set ( docMetaRef , this . createRecordHolderForDocMeta ( docInfo , data , recordPermission ) ) ; batch . set ( docInfoRef , this . createRecordHolderForDocInfo ( docInfo , recordPermission ) ) ; await batch . commit ( ) ; /**
             * This will verify that the data we have is written to the server
             * and not just the local cache.
             */ const waitForCommit = async ( ) => { const [ docMetaRef , docInfoRef ] = createDocRefs ( ) ; this . handleDatastoreMutations ( docMetaRef , datastoreMutation , 'write' ) ; const commitPromise = Promise . all ( [ this . waitForCommit ( docMetaRef ) , this . waitForCommit ( docInfoRef ) ] ) ; log . debug ( "write: Waiting for commit promise..." ) ; await commitPromise ; log . debug ( "write: Waiting for commit promise...done" ) ; } ; if ( opts . consistency === 'committed' ) { console . log ( "write: Waiting for commit..." ) ; await waitForCommit ( ) ; console . log ( "write: Waiting for commit...done" ) ; } } finally { } } public async overview ( ) { const docMetaRefs = await this . getDocMetaRefs ( ) ; const user = await Firebase . currentUserAsync ( ) ; return { nrDocs : docMetaRefs . length , created : user ! . metadata . creationTime } ; } public capabilities ( ) { return { networkLayers : NetworkLayers . WEB , permission : { mode : 'rw' } , snapshots : true } ; } /**
     * Create the document that we will store in for the DocMeta
     */ private createRecordHolderForDocMeta ( docInfo , docMeta , opts = new DefaultWriteOpts ( ) ) { const visibility = opts . visibility || Visibility . PRIVATE ; const uid = this . uid ; const id = FirebaseDatastores . computeDocMetaID ( docInfo . fingerprint , uid ) ; const docMetaHolder = { docInfo , value : docMeta } ; const recordHolder = { uid , id , visibility , groups : opts . groups || null , value : docMetaHolder } ; return recordHolder ; } private createRecordHolderForDocInfo ( docInfo , opts = new DefaultWriteOpts ( ) ) { const visibility = opts . visibility || Visibility . PRIVATE ; const uid = this . uid ; const id = FirebaseDatastores . computeDocMetaID ( docInfo . fingerprint , uid ) ; const recordHolder = { uid , id , visibility , groups : opts . groups || null , value : docInfo } ; return recordHolder ; } public async getDocMetaRefs ( ) { const firestore = this . firestore ! ; const uid = this . uid ; const snapshot = await firestore . collection ( DatastoreCollection . DOC_META ) . where ( 'uid' , '==' , uid ) . get ( ) ; if ( snapshot . empty ) { return [ ] ; } const result = [ ] ; for ( const doc of snapshot . docs ) { const recordHolder = < RecordHolder < DocMetaHolder > > doc . data ( ) ; const fingerprint = recordHolder . value . docInfo . fingerprint ; if ( recordHolder . value . value ) { result . push ( { fingerprint , docMetaProvider : ( ) => Promise . resolve ( DocMetas . deserialize ( recordHolder . value . value , fingerprint ) ) } ) ; } } return result ; } /**
     * Wait for the record to be fully committed to the remote datastore - not
     * just written to the local cache.
     */ private waitForCommit ( ref ) { return new Promise ( resolve => { const unsubscribeToSnapshot = ref . onSnapshot ( { includeMetadataChanges : true } , snapshot => { if ( ! snapshot . metadata . fromCache && ! snapshot . metadata . hasPendingWrites ) { unsubscribeToSnapshot ( ) ; resolve ( ) ; } } , ERR_HANDLER ) ; } ) ; } private handleDatastoreMutations ( ref , datastoreMutation , op ) { const unsubscribeToSnapshot = ref . onSnapshot ( { includeMetadataChanges : true } , snapshot => { if ( snapshot . metadata . fromCache && snapshot . metadata . hasPendingWrites ) { datastoreMutation . written . resolve ( true ) ; log . debug ( `Got written mutation with op: ${ op }` , ref ) ; } if ( ! snapshot . metadata . fromCache && ! snapshot . metadata . hasPendingWrites ) { datastoreMutation . written . resolve ( true ) ; datastoreMutation . committed . resolve ( true ) ; log . debug ( `Got committed mutation with op: ${ op }` , ref ) ; unsubscribeToSnapshot ( ) ; } } , ERR_HANDLER ) ; } /**
     * Called when new data is available from Firebase so we can solve promises,
     * add things to local stores, etc.
     *
     * ALL operations are done via snapshots which we create and subscribe to
     * on init().
     *
     * This solves to problems:
     *
     * 1. The most important, is that when data is added remotely (the user is
     *    on another machine and this machine-rejoins the network or detects
     *    changes) then content if pulled locally and added to the local
     *    datastore.
     *
     * 2. Local data is added via the same code path.  The code path is remote-
     *    first but then then immediately resolved from the cache and added
     *    locally.
     */ private handleDocMetaSnapshot ( snapshot , docMetaSnapshotEventListener , batchID ) { type DocMetaData = string | null ; const docMetaMutationFromRecord = ( record , mutationType = 'created' , fromCache , hasPendingWrites ) => { const id = record . id ; const docInfo = record . value . docInfo ; const dataProvider = async ( ) : Promise < DocMetaData > => { return record . value . value ; } ; const docMetaProvider = AsyncProviders . memoize ( async ( ) => { if ( mutationType === 'deleted' ) { throw new Error ( "Unable to read data when mutationType is 'deleted'" ) ; } const data = await dataProvider ( ) ; if ( ! data ) { console . warn ( "No data for fingerprint from dataProvider: " + docInfo . fingerprint ) ; return undefined ; } const docMetaID = FirebaseDatastores . computeDocMetaID ( docInfo . fingerprint , this . uid ) ; Preconditions . assertPresent ( data , `No data for docMeta with fingerprint: ${ docInfo . fingerprint }, docMetaID: ${ docMetaID }` ) ; return DocMetas . deserialize ( data ! , docInfo . fingerprint ) ; } ) ; const docMetaMutation = { id , fingerprint : docInfo . fingerprint , dataProvider , docMetaProvider , docInfoProvider : AsyncProviders . of ( docInfo ) , docMetaFileRefProvider : AsyncProviders . of ( DocMetaFileRefs . createFromDocInfo ( docInfo ) ) , mutationType , fromCache , hasPendingWrites } ; return docMetaMutation ; } ; const toDocMetaMutationFromDocChange = ( docChange ) => { const record = < RecordHolder < DocMetaHolder > > docChange . doc . data ( ) ; const fromCache = docChange . doc . metadata . fromCache ; const hasPendingWrites = docChange . doc . metadata . hasPendingWrites ; return docMetaMutationFromRecord ( record , toMutationType ( docChange . type ) , fromCache , hasPendingWrites ) ; } ; const consistency = snapshot . metadata . fromCache ? 'written' : 'committed' ; const docChanges = snapshot . docChanges ( ) ; const progressTracker = new ProgressTracker ( { total : docChanges . length , id : 'firebase-snapshot' } ) ; const docChangeDocMetaMutations = docChanges . map ( current => toDocMetaMutationFromDocChange ( current ) ) ; const docMetaMutations = [ ... docChangeDocMetaMutations ] ; docMetaSnapshotEventListener ( { datastore : this . id , consistency , progress : progressTracker . terminate ( ) , docMetaMutations , batch : { id : batchID , terminated : true , } } ) . catch ( err => log . error ( "Unable to dispatch event listener: " , err ) ) ; log . debug ( "onSnapshot... done" ) ; } private toConsistency ( snapshot ) { return snapshot . metadata . fromCache ? 'written' : 'committed' ; } public addDocMetaSnapshotEventListener ( docMetaSnapshotEventListener ) { const listener = ( event ) => { docMetaSnapshotEventListener ( event ) . catch ( err => console . error ( "Could not handle snapshot: " , err ) ) ; } ; this . docMetaSnapshotEventDispatcher . addEventListener ( event => listener ( event ) ) ; } private preferredSource ( ) { return 'default' ; } } type FirestoreSource = 'default' | 'server' | 'cache' ; export interface RecordPermission { readonly visibility : Visibility ; readonly groups ? : ReadonlyArray < GroupIDStr > | null ; } /**
 * Holds a data object literal by value. This contains the high level
 * information about a document including the ID and the visibility.  The value
 * object points to a more specific object which hold the actual data we need.
 */ export interface RecordHolder < T > extends RecordPermission { readonly uid : UserID ; readonly id : string ; readonly value : T ; } export interface DocMetaHolder { readonly docInfo : IDocInfo ; readonly value : string ; } export enum DatastoreCollection { DOC_INFO = "doc_info" , DOC_META = "doc_meta" } /**
 * The result of a FB database mutation.
 */ interface Mutation { } interface PendingMutationIndex { [ id ] : PendingMutation ; } interface PendingMutation { id : string ; type : 'delete' | 'write' ; } interface FirebaseDocMetaMutation extends DocMetaMutation { readonly id : string ; } /**
 * Convert a Firestore DocumentChangeType to a DocMutationType.  We prefer the
 * CRUD (create update delete) naming.
 */ function toMutationType ( docChangeType ) { switch ( docChangeType ) { case 'added' : return 'created' ; case 'modified' : return 'updated' ; case 'removed' : return 'deleted' ; } } export interface StoragePath { readonly path : string ; readonly settings ? : StorageSettings ; } export interface StorageSettings { readonly cacheControl : string ; readonly contentType : string ; } /**
 * A specific type of document ID derived from the fingerprint and only
 * available within Firebase.
 */ export type FirebaseDocMetaID = string ; export class DownloadURLs { public static async checkExistence ( url ) { return await URLs . existsWithGETUsingRange ( url ) ; } public static computeDownloadURL ( backend , ref , storagePath , storageRef , opts ) { return this . computeDownloadURLDirectly ( backend , ref , storagePath , opts ) ; } private static async computeDownloadURLWithStorageRef ( storageRef ) { try { return await storageRef . getDownloadURL ( ) ; } catch ( e ) { if ( e . code === "storage/object-not-found" ) { return undefined ; } throw e ; } } private static computeDownloadURLDirectly ( backend , ref , storagePath , opts ) { /**
         * Compute the storage path including the flip over whether we're
         * going to be public without any type of path conversion depending
         * on whether it's public or not.  Public URLs have a 1:1 mapping
         * where everything else might be in a different bucket or path
         * depending the storage computation function.
         */ const toPath = ( ) : string => { if ( backend === Backend . PUBLIC ) { return `${ backend }/${ ref . name }` ; } else { return storagePath . path ; } } ; const toURL = ( ) : string => { const path = toPath ( ) ; const project = process . env . POLAR_TEST_PROJECT || "polar-32b0f" ; return `https://storage.googleapis.com/${ project }.appspot.com/${ path }` ; } ; return toURL ( ) ; } } interface GetDocMetaOpts { readonly preferredSource ? : FirestoreSource ; } export class DefaultWriteOpts implements WriteOpts { public readonly consistency = 'written' ; public readonly visibility = Visibility . PRIVATE ; } const ERR_HANDLER = ( err ) => log . error ( "Could not create snapshot for account: " , err ) ;