diff --git a/packages/firestore/src/local/indexeddb_mutation_queue.ts b/packages/firestore/src/local/indexeddb_mutation_queue.ts index 3ce89384fcd..618bb36101e 100644 --- a/packages/firestore/src/local/indexeddb_mutation_queue.ts +++ b/packages/firestore/src/local/indexeddb_mutation_queue.ts @@ -25,6 +25,7 @@ import { BATCHID_UNKNOWN, MutationBatch } from '../model/mutation_batch'; import { ResourcePath } from '../model/path'; import { assert, fail } from '../util/assert'; import { primitiveComparator } from '../util/misc'; +import { SortedMap } from '../util/sorted_map'; import { SortedSet } from '../util/sorted_set'; import * as EncodedResourcePath from './encoded_resource_path'; @@ -46,6 +47,8 @@ import { PersistenceTransaction, ReferenceDelegate } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { SimpleDbStore, SimpleDbTransaction } from './simple_db'; +import { AnyJs } from '../../src/util/misc'; + /** A mutation queue for a specific user, backed by IndexedDB. */ export class IndexedDbMutationQueue implements MutationQueue { /** @@ -325,7 +328,7 @@ export class IndexedDbMutationQueue implements MutationQueue { getAllMutationBatchesAffectingDocumentKeys( transaction: PersistenceTransaction, - documentKeys: DocumentKeySet + documentKeys: SortedMap ): PersistencePromise { let uniqueBatchIDs = new SortedSet(primitiveComparator); diff --git a/packages/firestore/src/local/indexeddb_remote_document_cache.ts b/packages/firestore/src/local/indexeddb_remote_document_cache.ts index e70996c7d6d..87114e713ec 100644 --- a/packages/firestore/src/local/indexeddb_remote_document_cache.ts +++ b/packages/firestore/src/local/indexeddb_remote_document_cache.ts @@ -16,15 +16,20 @@ import { Query } from '../core/query'; import { + DocumentKeySet, documentKeySet, DocumentMap, documentMap, + DocumentSizeEntries, DocumentSizeEntry, MaybeDocumentMap, - maybeDocumentMap + maybeDocumentMap, + nullableMaybeDocumentMap, + NullableMaybeDocumentMap } from '../model/collections'; import { Document, MaybeDocument, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { SortedMap } from '../util/sorted_map'; import { SnapshotVersion } from '../core/snapshot_version'; import { assert, fail } from '../util/assert'; @@ -178,6 +183,110 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache { }); } + getEntries( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise { + let results = nullableMaybeDocumentMap(); + return this.forEachDbEntry( + transaction, + documentKeys, + (key, dbRemoteDoc) => { + if (dbRemoteDoc) { + results = results.insert( + key, + this.serializer.fromDbRemoteDocument(dbRemoteDoc) + ); + } else { + results = results.insert(key, null); + } + } + ).next(() => results); + } + + /** + * Looks up several entries in the cache. + * + * @param documentKeys The set of keys entries to look up. + * @return A map of MaybeDocuments indexed by key (if a document cannot be + * found, the key will be mapped to null) and a map of sizes indexed by + * key (zero if the key cannot be found). + */ + getSizedEntries( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise { + let results = nullableMaybeDocumentMap(); + let sizeMap = new SortedMap(DocumentKey.comparator); + return this.forEachDbEntry( + transaction, + documentKeys, + (key, dbRemoteDoc) => { + if (dbRemoteDoc) { + results = results.insert( + key, + this.serializer.fromDbRemoteDocument(dbRemoteDoc) + ); + sizeMap = sizeMap.insert(key, dbDocumentSize(dbRemoteDoc)); + } else { + results = results.insert(key, null); + sizeMap = sizeMap.insert(key, 0); + } + } + ).next(() => { + return { maybeDocuments: results, sizeMap }; + }); + } + + private forEachDbEntry( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet, + callback: (key: DocumentKey, doc: DbRemoteDocument | null) => void + ): PersistencePromise { + if (documentKeys.isEmpty()) { + return PersistencePromise.resolve(); + } + + const range = IDBKeyRange.bound( + documentKeys.first()!.path.toArray(), + documentKeys.last()!.path.toArray() + ); + const keyIter = documentKeys.getIterator(); + let nextKey: DocumentKey | null = keyIter.getNext(); + + return remoteDocumentsStore(transaction) + .iterate({ range }, (potentialKeyRaw, dbRemoteDoc, control) => { + const potentialKey = DocumentKey.fromSegments(potentialKeyRaw); + + // Go through keys not found in cache. + while (nextKey && DocumentKey.comparator(nextKey!, potentialKey) < 0) { + callback(nextKey!, null); + nextKey = keyIter.getNext(); + } + + if (nextKey && nextKey!.isEqual(potentialKey)) { + // Key found in cache. + callback(nextKey!, dbRemoteDoc); + nextKey = keyIter.hasNext() ? keyIter.getNext() : null; + } + + // Skip to the next key (if there is one). + if (nextKey) { + control.skip(nextKey!.path.toArray()); + } else { + control.done(); + } + }) + .next(() => { + // The rest of the keys are not in the cache. One case where `iterate` + // above won't go through them is when the cache is empty. + while (nextKey) { + callback(nextKey!, null); + nextKey = keyIter.hasNext() ? keyIter.getNext() : null; + } + }); + } + getDocumentsMatchingQuery( transaction: PersistenceTransaction, query: Query @@ -381,6 +490,13 @@ class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer { ): PersistencePromise { return this.documentCache.getSizedEntry(transaction, documentKey); } + + protected getAllFromCache( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise { + return this.documentCache.getSizedEntries(transaction, documentKeys); + } } export function isDocumentChangeMissingError(err: FirestoreError): boolean { diff --git a/packages/firestore/src/local/local_documents_view.ts b/packages/firestore/src/local/local_documents_view.ts index 7edae45dd68..9b5e77f28e0 100644 --- a/packages/firestore/src/local/local_documents_view.ts +++ b/packages/firestore/src/local/local_documents_view.ts @@ -21,7 +21,9 @@ import { DocumentMap, documentMap, MaybeDocumentMap, - maybeDocumentMap + maybeDocumentMap, + NullableMaybeDocumentMap, + nullableMaybeDocumentMap } from '../model/collections'; import { Document, MaybeDocument, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -74,6 +76,23 @@ export class LocalDocumentsView { }); } + // Returns the view of the given `docs` as they would appear after applying + // all mutations in the given `batches`. + private applyLocalMutationsToDocuments( + transaction: PersistenceTransaction, + docs: NullableMaybeDocumentMap, + batches: MutationBatch[] + ): NullableMaybeDocumentMap { + let results = nullableMaybeDocumentMap(); + docs.forEach((key, localView) => { + for (const batch of batches) { + localView = batch.applyToLocalView(key, localView); + } + results = results.insert(key, localView); + }); + return results; + } + /** * Gets the local view of the documents identified by `keys`. * @@ -83,29 +102,38 @@ export class LocalDocumentsView { getDocuments( transaction: PersistenceTransaction, keys: DocumentKeySet + ): PersistencePromise { + return this.remoteDocumentCache + .getEntries(transaction, keys) + .next(docs => this.getLocalViewOfDocuments(transaction, docs)); + } + + /** + * Similar to `getDocuments`, but creates the local view from the given + * `baseDocs` without retrieving documents from the local store. + */ + getLocalViewOfDocuments( + transaction: PersistenceTransaction, + baseDocs: NullableMaybeDocumentMap ): PersistencePromise { return this.mutationQueue - .getAllMutationBatchesAffectingDocumentKeys(transaction, keys) + .getAllMutationBatchesAffectingDocumentKeys(transaction, baseDocs) .next(batches => { - const promises = [] as Array>; + const docs = this.applyLocalMutationsToDocuments( + transaction, + baseDocs, + batches + ); let results = maybeDocumentMap(); - keys.forEach(key => { - promises.push( - this.getDocumentInternal(transaction, key, batches).next( - maybeDoc => { - // TODO(http://b/32275378): Don't conflate missing / deleted. - if (!maybeDoc) { - maybeDoc = new NoDocument( - key, - SnapshotVersion.forDeletedDoc() - ); - } - results = results.insert(key, maybeDoc); - } - ) - ); + docs.forEach((key, maybeDoc) => { + // TODO(http://b/32275378): Don't conflate missing / deleted. + if (!maybeDoc) { + maybeDoc = new NoDocument(key, SnapshotVersion.forDeletedDoc()); + } + results = results.insert(key, maybeDoc); }); - return PersistencePromise.waitFor(promises).next(() => results); + + return results; }); } diff --git a/packages/firestore/src/local/local_serializer.ts b/packages/firestore/src/local/local_serializer.ts index 0825e2a8fac..13532e34aa8 100644 --- a/packages/firestore/src/local/local_serializer.ts +++ b/packages/firestore/src/local/local_serializer.ts @@ -71,7 +71,9 @@ export class LocalSerializer { /** Encodes a document for storage locally. */ toDbRemoteDocument(maybeDoc: MaybeDocument): DbRemoteDocument { if (maybeDoc instanceof Document) { - const doc = this.remoteSerializer.toDocument(maybeDoc); + const doc = maybeDoc.proto + ? maybeDoc.proto + : this.remoteSerializer.toDocument(maybeDoc); const hasCommittedMutations = maybeDoc.hasCommittedMutations; return new DbRemoteDocument( /* unknownDocument= */ null, diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index 10511aa5a53..daa7b1ca574 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -23,6 +23,7 @@ import { DocumentKeySet, documentKeySet, DocumentMap, + maybeDocumentMap, MaybeDocumentMap } from '../model/collections'; import { MaybeDocument } from '../model/document'; @@ -466,11 +467,18 @@ export class LocalStore { } ); - let changedDocKeys = documentKeySet(); + let changedDocs = maybeDocumentMap(); + let updatedKeys = documentKeySet(); remoteEvent.documentUpdates.forEach((key, doc) => { - changedDocKeys = changedDocKeys.add(key); - promises.push( - documentBuffer.getEntry(txn, key).next(existingDoc => { + updatedKeys = updatedKeys.add(key); + }); + + // Each loop iteration only affects its "own" doc, so it's safe to get all the remote + // documents in advance in a single call. + promises.push( + documentBuffer.getEntries(txn, updatedKeys).next(existingDocs => { + remoteEvent.documentUpdates.forEach((key, doc) => { + const existingDoc = existingDocs.get(key); // If a document update isn't authoritative, make sure we don't // apply an old document version to the remote cache. We make an // exception for SnapshotVersion.MIN which can happen for @@ -484,6 +492,7 @@ export class LocalStore { doc.version.compareTo(existingDoc.version) >= 0 ) { documentBuffer.addEntry(doc); + changedDocs = changedDocs.insert(key, doc); } else { log.debug( LOG_TAG, @@ -495,14 +504,18 @@ export class LocalStore { doc.version ); } - }) - ); - if (remoteEvent.resolvedLimboDocuments.has(key)) { - promises.push( - this.persistence.referenceDelegate.updateLimboDocument(txn, key) - ); - } - }); + + if (remoteEvent.resolvedLimboDocuments.has(key)) { + promises.push( + this.persistence.referenceDelegate.updateLimboDocument( + txn, + key + ) + ); + } + }); + }) + ); // HACK: The only reason we allow a null snapshot version is so that we // can synthesize remote events when we get permission denied errors while @@ -532,7 +545,10 @@ export class LocalStore { return PersistencePromise.waitFor(promises) .next(() => documentBuffer.apply(txn)) .next(() => { - return this.localDocuments.getDocuments(txn, changedDocKeys); + return this.localDocuments.getLocalViewOfDocuments( + txn, + changedDocs + ); }); } ); diff --git a/packages/firestore/src/local/memory_mutation_queue.ts b/packages/firestore/src/local/memory_mutation_queue.ts index 442b57ec51c..89e338874a7 100644 --- a/packages/firestore/src/local/memory_mutation_queue.ts +++ b/packages/firestore/src/local/memory_mutation_queue.ts @@ -24,6 +24,7 @@ import { BATCHID_UNKNOWN, MutationBatch } from '../model/mutation_batch'; import { emptyByteString } from '../platform/platform'; import { assert } from '../util/assert'; import { primitiveComparator } from '../util/misc'; +import { SortedMap } from '../util/sorted_map'; import { SortedSet } from '../util/sorted_set'; import { MutationQueue } from './mutation_queue'; @@ -31,6 +32,8 @@ import { PersistenceTransaction, ReferenceDelegate } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { DocReference } from './reference_set'; +import { AnyJs } from '../../src/util/misc'; + export class MemoryMutationQueue implements MutationQueue { /** * The set of all mutations that have been sent but not yet been applied to @@ -203,7 +206,7 @@ export class MemoryMutationQueue implements MutationQueue { getAllMutationBatchesAffectingDocumentKeys( transaction: PersistenceTransaction, - documentKeys: DocumentKeySet + documentKeys: SortedMap ): PersistencePromise { let uniqueBatchIDs = new SortedSet(primitiveComparator); diff --git a/packages/firestore/src/local/memory_remote_document_cache.ts b/packages/firestore/src/local/memory_remote_document_cache.ts index 42c548aeb3b..9e43f0321b5 100644 --- a/packages/firestore/src/local/memory_remote_document_cache.ts +++ b/packages/firestore/src/local/memory_remote_document_cache.ts @@ -16,12 +16,16 @@ import { Query } from '../core/query'; import { + DocumentKeySet, documentKeySet, DocumentMap, documentMap, + DocumentSizeEntries, DocumentSizeEntry, MaybeDocumentMap, - maybeDocumentMap + maybeDocumentMap, + NullableMaybeDocumentMap, + nullableMaybeDocumentMap } from '../model/collections'; import { Document, MaybeDocument, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -107,6 +111,40 @@ export class MemoryRemoteDocumentCache implements RemoteDocumentCache { return PersistencePromise.resolve(this.docs.get(documentKey)); } + getEntries( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise { + let results = nullableMaybeDocumentMap(); + documentKeys.forEach(documentKey => { + const entry = this.docs.get(documentKey); + results = results.insert(documentKey, entry ? entry.maybeDocument : null); + }); + return PersistencePromise.resolve(results); + } + + /** + * Looks up several entries in the cache. + * + * @param documentKeys The set of keys entries to look up. + * @return A map of MaybeDocuments indexed by key (if a document cannot be + * found, the key will be mapped to null) and a map of sizes indexed by + * key (zero if the key cannot be found). + */ + getSizedEntries( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise { + let results = nullableMaybeDocumentMap(); + let sizeMap = new SortedMap(DocumentKey.comparator); + documentKeys.forEach(documentKey => { + const entry = this.docs.get(documentKey); + results = results.insert(documentKey, entry ? entry.maybeDocument : null); + sizeMap = sizeMap.insert(documentKey, entry ? entry.size : 0); + }); + return PersistencePromise.resolve({ maybeDocuments: results, sizeMap }); + } + getDocumentsMatchingQuery( transaction: PersistenceTransaction, query: Query @@ -203,4 +241,11 @@ export class MemoryRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer ): PersistencePromise { return this.documentCache.getSizedEntry(transaction, documentKey); } + + protected getAllFromCache( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise { + return this.documentCache.getSizedEntries(transaction, documentKeys); + } } diff --git a/packages/firestore/src/local/mutation_queue.ts b/packages/firestore/src/local/mutation_queue.ts index 91c48ed4e49..2633873586c 100644 --- a/packages/firestore/src/local/mutation_queue.ts +++ b/packages/firestore/src/local/mutation_queue.ts @@ -21,10 +21,13 @@ import { DocumentKeySet } from '../model/collections'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; import { MutationBatch } from '../model/mutation_batch'; +import { SortedMap } from '../util/sorted_map'; import { PersistenceTransaction } from './persistence'; import { PersistencePromise } from './persistence_promise'; +import { AnyJs } from '../../src/util/misc'; + /** A queue of mutations to apply to the remote store. */ export interface MutationQueue { /** Returns true if this queue contains no mutation batches. */ @@ -133,7 +136,7 @@ export interface MutationQueue { // TODO(mcg): This should really return an enumerator getAllMutationBatchesAffectingDocumentKeys( transaction: PersistenceTransaction, - documentKeys: DocumentKeySet + documentKeys: SortedMap ): PersistencePromise; /** diff --git a/packages/firestore/src/local/remote_document_cache.ts b/packages/firestore/src/local/remote_document_cache.ts index 7a37355cd17..2709e99a601 100644 --- a/packages/firestore/src/local/remote_document_cache.ts +++ b/packages/firestore/src/local/remote_document_cache.ts @@ -15,7 +15,12 @@ */ import { Query } from '../core/query'; -import { DocumentMap, MaybeDocumentMap } from '../model/collections'; +import { + DocumentKeySet, + DocumentMap, + MaybeDocumentMap, + NullableMaybeDocumentMap +} from '../model/collections'; import { MaybeDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -44,6 +49,18 @@ export interface RemoteDocumentCache { documentKey: DocumentKey ): PersistencePromise; + /** + * Looks up a set of entries in the cache. + * + * @param documentKeys The keys of the entries to look up. + * @return The cached Document or NoDocument entries indexed by key. If an entry is not cached, + * the corresponding key will be mapped to a null value. + */ + getEntries( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise; + /** * Executes a query against the cached Document entries. * diff --git a/packages/firestore/src/local/remote_document_change_buffer.ts b/packages/firestore/src/local/remote_document_change_buffer.ts index 27b8113993e..ac837e8ff6a 100644 --- a/packages/firestore/src/local/remote_document_change_buffer.ts +++ b/packages/firestore/src/local/remote_document_change_buffer.ts @@ -15,9 +15,12 @@ */ import { + DocumentKeySet, + DocumentSizeEntries, DocumentSizeEntry, maybeDocumentMap, - MaybeDocumentMap + MaybeDocumentMap, + NullableMaybeDocumentMap } from '../model/collections'; import { MaybeDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -52,6 +55,11 @@ export abstract class RemoteDocumentChangeBuffer { documentKey: DocumentKey ): PersistencePromise; + protected abstract getAllFromCache( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise; + protected abstract applyChanges( transaction: PersistenceTransaction ): PersistencePromise; @@ -99,6 +107,36 @@ export abstract class RemoteDocumentChangeBuffer { } } + /** + * Looks up several entries in the cache, forwarding to + * `RemoteDocumentCache.getEntry()`. + * + * @param transaction The transaction in which to perform any persistence + * operations. + * @param documentKeys The keys of the entries to look up. + * @return A map of cached `Document`s or `NoDocument`s, indexed by key. If an + * entry cannot be found, the corresponding key will be mapped to a null + * value. + */ + getEntries( + transaction: PersistenceTransaction, + documentKeys: DocumentKeySet + ): PersistencePromise { + // Record the size of everything we load from the cache so we can compute + // a delta later. + return this.getAllFromCache(transaction, documentKeys).next( + ({ maybeDocuments, sizeMap }) => { + // Note: `getAllFromCache` returns two maps instead of a single map from + // keys to `DocumentSizeEntry`s. This is to allow returning the + // `NullableMaybeDocumentMap` directly, without a conversion. + sizeMap.forEach((documentKey, size) => { + this.documentSizes.set(documentKey, size); + }); + return maybeDocuments; + } + ); + } + /** * Applies buffered changes to the underlying RemoteDocumentCache, using * the provided transaction. diff --git a/packages/firestore/src/model/collections.ts b/packages/firestore/src/model/collections.ts index cb19919a563..f07a4c22248 100644 --- a/packages/firestore/src/model/collections.ts +++ b/packages/firestore/src/model/collections.ts @@ -37,6 +37,20 @@ export function maybeDocumentMap(): MaybeDocumentMap { return EMPTY_MAYBE_DOCUMENT_MAP; } +export type NullableMaybeDocumentMap = SortedMap< + DocumentKey, + MaybeDocument | null +>; + +export function nullableMaybeDocumentMap(): NullableMaybeDocumentMap { + return maybeDocumentMap(); +} + +export type DocumentSizeEntries = { + maybeDocuments: NullableMaybeDocumentMap; + sizeMap: SortedMap; +}; + export type DocumentMap = SortedMap; const EMPTY_DOCUMENT_MAP = new SortedMap( DocumentKey.comparator diff --git a/packages/firestore/src/model/document.ts b/packages/firestore/src/model/document.ts index ff0b1266f33..44f3134fa0d 100644 --- a/packages/firestore/src/model/document.ts +++ b/packages/firestore/src/model/document.ts @@ -22,6 +22,8 @@ import { DocumentKey } from './document_key'; import { FieldValue, JsonObject, ObjectValue } from './field_value'; import { FieldPath } from './path'; +import * as api from '../protos/firestore_proto_api'; + export interface DocumentOptions { hasLocalMutations?: boolean; hasCommittedMutations?: boolean; @@ -59,7 +61,12 @@ export class Document extends MaybeDocument { key: DocumentKey, version: SnapshotVersion, readonly data: ObjectValue, - options: DocumentOptions + options: DocumentOptions, + /** + * Memoized serialized form of the document for optimization purposes (avoids repeated + * serialization). Might be undefined. + */ + readonly proto?: api.Document ) { super(key, version); this.hasLocalMutations = !!options.hasLocalMutations; diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 0961d135c0b..e37e9b25687 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -581,7 +581,7 @@ export class JsonProtoSerializer { const key = this.fromName(doc.found!.name!); const version = this.fromVersion(doc.found!.updateTime!); const fields = this.fromFields(doc.found!.fields || {}); - return new Document(key, version, fields, {}); + return new Document(key, version, fields, {}, doc.found!); } private fromMissing(result: api.BatchGetDocumentsResponse): NoDocument { @@ -726,7 +726,15 @@ export class JsonProtoSerializer { const key = this.fromName(entityChange.document!.name!); const version = this.fromVersion(entityChange.document!.updateTime!); const fields = this.fromFields(entityChange.document!.fields || {}); - const doc = new Document(key, version, fields, {}); + // The document may soon be re-serialized back to protos in order to store it in local + // persistence. Memoize the encoded form to avoid encoding it again. + const doc = new Document( + key, + version, + fields, + {}, + entityChange.document! + ); const updatedTargetIds = entityChange.targetIds || []; const removedTargetIds = entityChange.removedTargetIds || []; watchChange = new DocumentWatchChange( diff --git a/packages/firestore/src/util/sorted_set.ts b/packages/firestore/src/util/sorted_set.ts index 1c7da829016..b834df6a355 100644 --- a/packages/firestore/src/util/sorted_set.ts +++ b/packages/firestore/src/util/sorted_set.ts @@ -103,6 +103,14 @@ export class SortedSet { return iter.hasNext() ? iter.getNext().key : null; } + getIterator(): SortedSetIterator { + return new SortedSetIterator(this.data.getIterator()); + } + + getIteratorFrom(key: T): SortedSetIterator { + return new SortedSetIterator(this.data.getIteratorFrom(key)); + } + /** Inserts or updates an element */ add(elem: T): SortedSet { return this.copy(this.data.remove(elem).insert(elem, true)); @@ -160,3 +168,15 @@ export class SortedSet { return result; } } + +export class SortedSetIterator { + constructor(private iter: SortedMapIterator) {} + + getNext(): T { + return this.iter.getNext().key; + } + + hasNext(): boolean { + return this.iter.hasNext(); + } +} diff --git a/packages/firestore/test/unit/local/remote_document_cache.test.ts b/packages/firestore/test/unit/local/remote_document_cache.test.ts index 84d6dd418c7..91f6e88c671 100644 --- a/packages/firestore/test/unit/local/remote_document_cache.test.ts +++ b/packages/firestore/test/unit/local/remote_document_cache.test.ts @@ -38,7 +38,10 @@ import { DbRemoteDocumentChanges, DbRemoteDocumentChangesKey } from '../../../src/local/indexeddb_schema'; -import { MaybeDocumentMap } from '../../../src/model/collections'; +import { + documentKeySet, + MaybeDocumentMap +} from '../../../src/model/collections'; import { fail } from '../../../src/util/assert'; import * as persistenceHelpers from './persistence_test_helpers'; import { @@ -326,6 +329,53 @@ function genericRemoteDocumentCacheTests( }); }); + it('can set and read several documents', () => { + const docs = [ + doc(DOC_PATH, VERSION, DOC_DATA), + doc(LONG_DOC_PATH, VERSION, DOC_DATA) + ]; + const key1 = key(DOC_PATH); + const key2 = key(LONG_DOC_PATH); + return cache + .addEntries(docs) + .then(() => { + return cache.getEntries( + documentKeySet() + .add(key1) + .add(key2) + ); + }) + .then(read => { + expectEqual(read.get(key1), docs[0]); + expectEqual(read.get(key2), docs[1]); + }); + }); + + it('can set and read several documents including missing document', () => { + const docs = [ + doc(DOC_PATH, VERSION, DOC_DATA), + doc(LONG_DOC_PATH, VERSION, DOC_DATA) + ]; + const key1 = key(DOC_PATH); + const key2 = key(LONG_DOC_PATH); + const missingKey = key('foo/nonexistent'); + return cache + .addEntries(docs) + .then(() => { + return cache.getEntries( + documentKeySet() + .add(key1) + .add(key2) + .add(missingKey) + ); + }) + .then(read => { + expectEqual(read.get(key1), docs[0]); + expectEqual(read.get(key2), docs[1]); + expect(read.get(missingKey)).to.be.null; + }); + }); + it('can remove document', () => { return cache .addEntries([doc(DOC_PATH, VERSION, DOC_DATA)]) diff --git a/packages/firestore/test/unit/local/test_mutation_queue.ts b/packages/firestore/test/unit/local/test_mutation_queue.ts index e59a0507b2a..d4c583db520 100644 --- a/packages/firestore/test/unit/local/test_mutation_queue.ts +++ b/packages/firestore/test/unit/local/test_mutation_queue.ts @@ -23,6 +23,7 @@ import { DocumentKeySet } from '../../../src/model/collections'; import { DocumentKey } from '../../../src/model/document_key'; import { Mutation } from '../../../src/model/mutation'; import { MutationBatch } from '../../../src/model/mutation_batch'; +import { SortedMap } from '../../../src/util/sorted_map'; /** * A wrapper around a MutationQueue that automatically creates a @@ -144,13 +145,18 @@ export class TestMutationQueue { getAllMutationBatchesAffectingDocumentKeys( documentKeys: DocumentKeySet ): Promise { + let keyMap = new SortedMap(DocumentKey.comparator); + documentKeys.forEach(key => { + keyMap = keyMap.insert(key, null); + }); + return this.persistence.runTransaction( 'getAllMutationBatchesAffectingDocumentKeys', 'readonly', txn => { return this.queue.getAllMutationBatchesAffectingDocumentKeys( txn, - documentKeys + keyMap ); } ); diff --git a/packages/firestore/test/unit/local/test_remote_document_cache.ts b/packages/firestore/test/unit/local/test_remote_document_cache.ts index 8298cf3034e..bfe49a40757 100644 --- a/packages/firestore/test/unit/local/test_remote_document_cache.ts +++ b/packages/firestore/test/unit/local/test_remote_document_cache.ts @@ -26,7 +26,12 @@ import { import { PersistencePromise } from '../../../src/local/persistence_promise'; import { RemoteDocumentCache } from '../../../src/local/remote_document_cache'; import { RemoteDocumentChangeBuffer } from '../../../src/local/remote_document_change_buffer'; -import { DocumentMap, MaybeDocumentMap } from '../../../src/model/collections'; +import { + DocumentKeySet, + DocumentMap, + MaybeDocumentMap, + NullableMaybeDocumentMap +} from '../../../src/model/collections'; import { MaybeDocument } from '../../../src/model/document'; import { DocumentKey } from '../../../src/model/document_key'; @@ -83,6 +88,12 @@ export abstract class TestRemoteDocumentCache { }); } + getEntries(documentKeys: DocumentKeySet): Promise { + return this.persistence.runTransaction('getEntries', 'readonly', txn => { + return this.cache.getEntries(txn, documentKeys); + }); + } + getDocumentsMatchingQuery(query: Query): Promise { return this.persistence.runTransaction( 'getDocumentsMatchingQuery',