diff --git a/packages/firestore/src/core/sync_engine.ts b/packages/firestore/src/core/sync_engine.ts index acfe6945d57..c49311ffa1d 100644 --- a/packages/firestore/src/core/sync_engine.ts +++ b/packages/firestore/src/core/sync_engine.ts @@ -682,8 +682,7 @@ export class SyncEngine implements RemoteSyncer { this.queriesByTarget.delete(targetId); if (this.isPrimaryClient) { - const limboKeys = this.limboDocumentRefs.referencesForId(targetId); - this.limboDocumentRefs.removeReferencesForId(targetId); + const limboKeys = this.limboDocumentRefs.removeReferencesForId(targetId); limboKeys.forEach(limboKey => { const isReferenced = this.limboDocumentRefs.containsKey(limboKey); if (!isReferenced) { diff --git a/packages/firestore/src/local/indexeddb_mutation_queue.ts b/packages/firestore/src/local/indexeddb_mutation_queue.ts index 860323f77c1..3672ff5da28 100644 --- a/packages/firestore/src/local/indexeddb_mutation_queue.ts +++ b/packages/firestore/src/local/indexeddb_mutation_queue.ts @@ -519,7 +519,7 @@ export class IndexedDbMutationQueue implements MutationQueue { return PersistencePromise.forEach( removedDocuments, (key: DocumentKey) => { - return this.referenceDelegate.removeMutationReference( + return this.referenceDelegate.markPotentiallyOrphaned( transaction, key ); diff --git a/packages/firestore/src/local/indexeddb_persistence.ts b/packages/firestore/src/local/indexeddb_persistence.ts index dabc3435025..b031fc41460 100644 --- a/packages/firestore/src/local/indexeddb_persistence.ts +++ b/packages/firestore/src/local/indexeddb_persistence.ts @@ -70,7 +70,6 @@ import { ReferenceDelegate } from './persistence'; import { PersistencePromise } from './persistence_promise'; -import { ReferenceSet } from './reference_set'; import { ClientId } from './shared_client_state'; import { TargetData } from './target_data'; import { SimpleDb, SimpleDbStore, SimpleDbTransaction } from './simple_db'; @@ -1070,8 +1069,6 @@ function clientMetadataStore( /** Provides LRU functionality for IndexedDB persistence. */ export class IndexedDbLruDelegate implements ReferenceDelegate, LruDelegate { - private inMemoryPins: ReferenceSet | null = null; - readonly garbageCollector: LruGarbageCollector; constructor(private readonly db: IndexedDbPersistence, params: LruParams) { @@ -1081,14 +1078,14 @@ export class IndexedDbLruDelegate implements ReferenceDelegate, LruDelegate { getSequenceNumberCount( txn: PersistenceTransaction ): PersistencePromise { - const docCountPromise = this.orphanedDocmentCount(txn); + const docCountPromise = this.orphanedDocumentCount(txn); const targetCountPromise = this.db.getTargetCache().getTargetCount(txn); return targetCountPromise.next(targetCount => docCountPromise.next(docCount => targetCount + docCount) ); } - private orphanedDocmentCount( + private orphanedDocumentCount( txn: PersistenceTransaction ): PersistencePromise { let orphanedCount = 0; @@ -1113,12 +1110,9 @@ export class IndexedDbLruDelegate implements ReferenceDelegate, LruDelegate { ); } - setInMemoryPins(inMemoryPins: ReferenceSet): void { - this.inMemoryPins = inMemoryPins; - } - addReference( txn: PersistenceTransaction, + targetId: TargetId, key: DocumentKey ): PersistencePromise { return writeSentinelKey(txn, key); @@ -1126,6 +1120,7 @@ export class IndexedDbLruDelegate implements ReferenceDelegate, LruDelegate { removeReference( txn: PersistenceTransaction, + targetId: TargetId, key: DocumentKey ): PersistencePromise { return writeSentinelKey(txn, key); @@ -1141,7 +1136,7 @@ export class IndexedDbLruDelegate implements ReferenceDelegate, LruDelegate { .removeTargets(txn, upperBound, activeTargetIds); } - removeMutationReference( + markPotentiallyOrphaned( txn: PersistenceTransaction, key: DocumentKey ): PersistencePromise { @@ -1158,11 +1153,7 @@ export class IndexedDbLruDelegate implements ReferenceDelegate, LruDelegate { txn: PersistenceTransaction, docKey: DocumentKey ): PersistencePromise { - if (this.inMemoryPins!.containsKey(docKey)) { - return PersistencePromise.resolve(true); - } else { - return mutationQueuesContainKey(txn, docKey); - } + return mutationQueuesContainKey(txn, docKey); } removeOrphanedDocuments( diff --git a/packages/firestore/src/local/indexeddb_target_cache.ts b/packages/firestore/src/local/indexeddb_target_cache.ts index 3f5fea5ae48..e7dc66d7ebb 100644 --- a/packages/firestore/src/local/indexeddb_target_cache.ts +++ b/packages/firestore/src/local/indexeddb_target_cache.ts @@ -284,7 +284,7 @@ export class IndexedDbTargetCache implements TargetCache { keys.forEach(key => { const path = encodeResourcePath(key.path); promises.push(store.put(new DbTargetDocument(targetId, path))); - promises.push(this.referenceDelegate.addReference(txn, key)); + promises.push(this.referenceDelegate.addReference(txn, targetId, key)); }); return PersistencePromise.waitFor(promises); } @@ -301,7 +301,7 @@ export class IndexedDbTargetCache implements TargetCache { const path = encodeResourcePath(key.path); return PersistencePromise.waitFor([ store.delete([targetId, path]), - this.referenceDelegate.removeReference(txn, key) + this.referenceDelegate.removeReference(txn, targetId, key) ]); }); } diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index 7c3ce2b0057..ff5004fb286 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -56,7 +56,6 @@ import { import { PersistencePromise } from './persistence_promise'; import { TargetCache } from './target_cache'; import { QueryEngine } from './query_engine'; -import { ReferenceSet } from './reference_set'; import { RemoteDocumentCache } from './remote_document_cache'; import { RemoteDocumentChangeBuffer } from './remote_document_change_buffer'; import { ClientId } from './shared_client_state'; @@ -165,11 +164,6 @@ export class LocalStore { */ protected localDocuments: LocalDocumentsView; - /** - * The set of document references maintained by any local views. - */ - private localViewReferences = new ReferenceSet(); - /** Maps a target to its `TargetData`. */ protected targetCache: TargetCache; @@ -206,9 +200,6 @@ export class LocalStore { persistence.started, 'LocalStore was passed an unstarted persistence implementation' ); - this.persistence.referenceDelegate.setInMemoryPins( - this.localViewReferences - ); this.mutationQueue = persistence.getMutationQueue(initialUser); this.remoteDocuments = persistence.getRemoteDocumentCache(); this.targetCache = persistence.getTargetCache(); @@ -704,49 +695,56 @@ export class LocalStore { * Notify local store of the changed views to locally pin documents. */ notifyLocalViewChanges(viewChanges: LocalViewChanges[]): Promise { - for (const viewChange of viewChanges) { - const targetId = viewChange.targetId; - - this.localViewReferences.addReferences(viewChange.addedKeys, targetId); - this.localViewReferences.removeReferences( - viewChange.removedKeys, - targetId - ); - - if (!viewChange.fromCache) { - const targetData = this.targetDataByTarget.get(targetId); - debugAssert( - targetData !== null, - `Can't set limbo-free snapshot version for unknown target: ${targetId}` - ); - - // Advance the last limbo free snapshot version - const lastLimboFreeSnapshotVersion = targetData.snapshotVersion; - const updatedTargetData = targetData.withLastLimboFreeSnapshotVersion( - lastLimboFreeSnapshotVersion - ); - this.targetDataByTarget = this.targetDataByTarget.insert( - targetId, - updatedTargetData - ); - } - } - return this.persistence.runTransaction( - 'notifyLocalViewChanges', - 'readwrite', - txn => { + return this.persistence + .runTransaction('notifyLocalViewChanges', 'readwrite', txn => { return PersistencePromise.forEach( viewChanges, (viewChange: LocalViewChanges) => { return PersistencePromise.forEach( - viewChange.removedKeys, + viewChange.addedKeys, (key: DocumentKey) => - this.persistence.referenceDelegate.removeReference(txn, key) + this.persistence.referenceDelegate.addReference( + txn, + viewChange.targetId, + key + ) + ).next(() => + PersistencePromise.forEach( + viewChange.removedKeys, + (key: DocumentKey) => + this.persistence.referenceDelegate.removeReference( + txn, + viewChange.targetId, + key + ) + ) ); } ); - } - ); + }) + .then(() => { + for (const viewChange of viewChanges) { + const targetId = viewChange.targetId; + + if (!viewChange.fromCache) { + const targetData = this.targetDataByTarget.get(targetId); + debugAssert( + targetData !== null, + `Can't set limbo-free snapshot version for unknown target: ${targetId}` + ); + + // Advance the last limbo free snapshot version + const lastLimboFreeSnapshotVersion = targetData.snapshotVersion; + const updatedTargetData = targetData.withLastLimboFreeSnapshotVersion( + lastLimboFreeSnapshotVersion + ); + this.targetDataByTarget = this.targetDataByTarget.insert( + targetId, + updatedTargetData + ); + } + } + }); } /** @@ -869,26 +867,11 @@ export class LocalStore { const mode = keepPersistedTargetData ? 'readwrite' : 'readwrite-primary'; return this.persistence .runTransaction('Release target', mode, txn => { - // References for documents sent via Watch are automatically removed - // when we delete a target's data from the reference delegate. - // Since this does not remove references for locally mutated documents, - // we have to remove the target associations for these documents - // manually. - // This operation needs to be run inside the transaction since EagerGC - // uses the local view references during the transaction's commit. - // Fortunately, the operation is safe to be re-run in case the - // transaction fails since there are no side effects if the target has - // already been removed. - const removed = this.localViewReferences.removeReferencesForId( - targetId - ); - if (!keepPersistedTargetData) { - return PersistencePromise.forEach(removed, (key: DocumentKey) => - this.persistence.referenceDelegate.removeReference(txn, key) - ).next(() => { - this.persistence.referenceDelegate.removeTarget(txn, targetData!); - }); + return this.persistence.referenceDelegate.removeTarget( + txn, + targetData! + ); } else { return PersistencePromise.resolve(); } diff --git a/packages/firestore/src/local/memory_mutation_queue.ts b/packages/firestore/src/local/memory_mutation_queue.ts index 63de7419e5f..ce55e8be25b 100644 --- a/packages/firestore/src/local/memory_mutation_queue.ts +++ b/packages/firestore/src/local/memory_mutation_queue.ts @@ -299,7 +299,7 @@ export class MemoryMutationQueue implements MutationQueue { return PersistencePromise.forEach(batch.mutations, (mutation: Mutation) => { const ref = new DocReference(mutation.key, batch.batchId); references = references.delete(ref); - return this.referenceDelegate.removeMutationReference( + return this.referenceDelegate.markPotentiallyOrphaned( transaction, mutation.key ); diff --git a/packages/firestore/src/local/memory_persistence.ts b/packages/firestore/src/local/memory_persistence.ts index 1ded2738f3a..391f8686565 100644 --- a/packages/firestore/src/local/memory_persistence.ts +++ b/packages/firestore/src/local/memory_persistence.ts @@ -29,7 +29,7 @@ import { LruParams } from './lru_garbage_collector'; import { ListenSequence } from '../core/listen_sequence'; -import { ListenSequenceNumber } from '../core/types'; +import { ListenSequenceNumber, TargetId } from '../core/types'; import { estimateByteSize } from '../model/values'; import { MemoryIndexManager } from './memory_index_manager'; import { MemoryMutationQueue } from './memory_mutation_queue'; @@ -184,7 +184,9 @@ export interface MemoryReferenceDelegate extends ReferenceDelegate { } export class MemoryEagerDelegate implements MemoryReferenceDelegate { - private inMemoryPins: ReferenceSet | null = null; + /** Tracks all documents that are active in Query views. */ + private localViewReferences: ReferenceSet = new ReferenceSet(); + /** The list of documents that are potentially GCed after each transaction. */ private _orphanedDocuments: Set | null = null; private constructor(private readonly persistence: MemoryPersistence) {} @@ -201,27 +203,27 @@ export class MemoryEagerDelegate implements MemoryReferenceDelegate { } } - setInMemoryPins(inMemoryPins: ReferenceSet): void { - this.inMemoryPins = inMemoryPins; - } - addReference( txn: PersistenceTransaction, + targetId: TargetId, key: DocumentKey ): PersistencePromise { + this.localViewReferences.addReference(key, targetId); this.orphanedDocuments.delete(key); return PersistencePromise.resolve(); } removeReference( txn: PersistenceTransaction, + targetId: TargetId, key: DocumentKey ): PersistencePromise { + this.localViewReferences.removeReference(key, targetId); this.orphanedDocuments.add(key); return PersistencePromise.resolve(); } - removeMutationReference( + markPotentiallyOrphaned( txn: PersistenceTransaction, key: DocumentKey ): PersistencePromise { @@ -233,6 +235,10 @@ export class MemoryEagerDelegate implements MemoryReferenceDelegate { txn: PersistenceTransaction, targetData: TargetData ): PersistencePromise { + const orphaned = this.localViewReferences.removeReferencesForId( + targetData.targetId + ); + orphaned.forEach(key => this.orphanedDocuments.add(key)); const cache = this.persistence.getTargetCache(); return cache .getMatchingKeysForTargetId(txn, targetData.targetId) @@ -290,15 +296,15 @@ export class MemoryEagerDelegate implements MemoryReferenceDelegate { key: DocumentKey ): PersistencePromise { return PersistencePromise.or([ + () => + PersistencePromise.resolve(this.localViewReferences.containsKey(key)), () => this.persistence.getTargetCache().containsKey(txn, key), - () => this.persistence.mutationQueuesContainKey(txn, key), - () => PersistencePromise.resolve(this.inMemoryPins!.containsKey(key)) + () => this.persistence.mutationQueuesContainKey(txn, key) ]); } } export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate { - private inMemoryPins: ReferenceSet | null = null; private orphanedSequenceNumbers: ObjectMap< DocumentKey, ListenSequenceNumber @@ -371,10 +377,6 @@ export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate { ); } - setInMemoryPins(inMemoryPins: ReferenceSet): void { - this.inMemoryPins = inMemoryPins; - } - removeTargets( txn: PersistenceTransaction, upperBound: ListenSequenceNumber, @@ -403,7 +405,7 @@ export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate { return p.next(() => changeBuffer.apply(txn)).next(() => count); } - removeMutationReference( + markPotentiallyOrphaned( txn: PersistenceTransaction, key: DocumentKey ): PersistencePromise { @@ -421,6 +423,7 @@ export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate { addReference( txn: PersistenceTransaction, + targetId: TargetId, key: DocumentKey ): PersistencePromise { this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber); @@ -429,6 +432,7 @@ export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate { removeReference( txn: PersistenceTransaction, + targetId: TargetId, key: DocumentKey ): PersistencePromise { this.orphanedSequenceNumbers.set(key, txn.currentSequenceNumber); @@ -458,7 +462,6 @@ export class MemoryLruDelegate implements ReferenceDelegate, LruDelegate { ): PersistencePromise { return PersistencePromise.or([ () => this.persistence.mutationQueuesContainKey(txn, key), - () => PersistencePromise.resolve(this.inMemoryPins!.containsKey(key)), () => this.persistence.getTargetCache().containsKey(txn, key), () => { const orphanedAt = this.orphanedSequenceNumbers.get(key); diff --git a/packages/firestore/src/local/memory_target_cache.ts b/packages/firestore/src/local/memory_target_cache.ts index a915c795ffc..316c655c084 100644 --- a/packages/firestore/src/local/memory_target_cache.ts +++ b/packages/firestore/src/local/memory_target_cache.ts @@ -191,14 +191,7 @@ export class MemoryTargetCache implements TargetCache { targetId: TargetId ): PersistencePromise { this.references.addReferences(keys, targetId); - const referenceDelegate = this.persistence.referenceDelegate; - const promises: Array> = []; - if (referenceDelegate) { - keys.forEach(key => { - promises.push(referenceDelegate.addReference(txn, key)); - }); - } - return PersistencePromise.waitFor(promises); + return PersistencePromise.resolve(); } removeMatchingKeys( @@ -211,7 +204,7 @@ export class MemoryTargetCache implements TargetCache { const promises: Array> = []; if (referenceDelegate) { keys.forEach(key => { - promises.push(referenceDelegate.removeReference(txn, key)); + promises.push(referenceDelegate.markPotentiallyOrphaned(txn, key)); }); } return PersistencePromise.waitFor(promises); diff --git a/packages/firestore/src/local/persistence.ts b/packages/firestore/src/local/persistence.ts index 43a2dfcc5fe..146b0331738 100644 --- a/packages/firestore/src/local/persistence.ts +++ b/packages/firestore/src/local/persistence.ts @@ -16,14 +16,13 @@ */ import { User } from '../auth/user'; -import { ListenSequenceNumber } from '../core/types'; +import { ListenSequenceNumber, TargetId } from '../core/types'; import { DocumentKey } from '../model/document_key'; import { IndexManager } from './index_manager'; import { LocalStore } from './local_store'; import { MutationQueue } from './mutation_queue'; import { PersistencePromise } from './persistence_promise'; import { TargetCache } from './target_cache'; -import { ReferenceSet } from './reference_set'; import { RemoteDocumentCache } from './remote_document_cache'; import { TargetData } from './target_data'; @@ -85,21 +84,17 @@ export type PrimaryStateListener = (isPrimary: boolean) => Promise; * generate sequence numbers (getCurrentSequenceNumber()). */ export interface ReferenceDelegate { - /** - * Registers a ReferenceSet of documents that should be considered 'referenced' and not eligible - * for removal during garbage collection. - */ - setInMemoryPins(pins: ReferenceSet): void; - /** Notify the delegate that the given document was added to a target. */ addReference( txn: PersistenceTransaction, + targetId: TargetId, doc: DocumentKey ): PersistencePromise; /** Notify the delegate that the given document was removed from a target. */ removeReference( txn: PersistenceTransaction, + targetId: TargetId, doc: DocumentKey ): PersistencePromise; @@ -112,8 +107,11 @@ export interface ReferenceDelegate { targetData: TargetData ): PersistencePromise; - /** Notify the delegate that a document is no longer being mutated by the user. */ - removeMutationReference( + /** + * Notify the delegate that a document may no longer be part of any views or + * have any mutations associated. + */ + markPotentiallyOrphaned( txn: PersistenceTransaction, doc: DocumentKey ): PersistencePromise; diff --git a/packages/firestore/test/unit/local/lru_garbage_collector.test.ts b/packages/firestore/test/unit/local/lru_garbage_collector.test.ts index 8d6c455efd6..9825e318655 100644 --- a/packages/firestore/test/unit/local/lru_garbage_collector.test.ts +++ b/packages/firestore/test/unit/local/lru_garbage_collector.test.ts @@ -36,7 +36,6 @@ import { import { PersistencePromise } from '../../../src/local/persistence_promise'; import { TargetCache } from '../../../src/local/target_cache'; -import { ReferenceSet } from '../../../src/local/reference_set'; import { RemoteDocumentCache } from '../../../src/local/remote_document_cache'; import { TargetData, TargetPurpose } from '../../../src/local/target_data'; import { documentKeySet } from '../../../src/model/collections'; @@ -122,7 +121,6 @@ function genericLruGarbageCollectorTests( txn => PersistencePromise.resolve(txn.currentSequenceNumber) ); const referenceDelegate = persistence.referenceDelegate; - referenceDelegate.setInMemoryPins(new ReferenceSet()); // eslint-disable-next-line @typescript-eslint/no-explicit-any garbageCollector = ((referenceDelegate as any) as LruDelegate) .garbageCollector; @@ -175,7 +173,7 @@ function genericLruGarbageCollectorTests( txn: PersistenceTransaction, key: DocumentKey ): PersistencePromise { - return persistence.referenceDelegate.removeMutationReference(txn, key); + return persistence.referenceDelegate.markPotentiallyOrphaned(txn, key); } function markDocumentEligibleForGC(key: DocumentKey): Promise { diff --git a/packages/firestore/test/unit/local/mutation_queue.test.ts b/packages/firestore/test/unit/local/mutation_queue.test.ts index 36415e7e52a..23247523e9b 100644 --- a/packages/firestore/test/unit/local/mutation_queue.test.ts +++ b/packages/firestore/test/unit/local/mutation_queue.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import { User } from '../../../src/auth/user'; import { Query } from '../../../src/core/query'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { Persistence } from '../../../src/local/persistence'; -import { ReferenceSet } from '../../../src/local/reference_set'; import { documentKeySet } from '../../../src/model/collections'; import { MutationBatch } from '../../../src/model/mutation_batch'; import { @@ -72,7 +71,6 @@ function genericMutationQueueTests(): void { addEqualityMatcher(); beforeEach(() => { - persistence.referenceDelegate.setInMemoryPins(new ReferenceSet()); mutationQueue = new TestMutationQueue( persistence, persistence.getMutationQueue(new User('user')) diff --git a/packages/firestore/test/unit/local/target_cache.test.ts b/packages/firestore/test/unit/local/target_cache.test.ts index 4aeddd94a65..35aaeaa6cfb 100644 --- a/packages/firestore/test/unit/local/target_cache.test.ts +++ b/packages/firestore/test/unit/local/target_cache.test.ts @@ -21,7 +21,6 @@ import { SnapshotVersion } from '../../../src/core/snapshot_version'; import { TargetId } from '../../../src/core/types'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { Persistence } from '../../../src/local/persistence'; -import { ReferenceSet } from '../../../src/local/reference_set'; import { TargetData, TargetPurpose } from '../../../src/local/target_data'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { @@ -143,7 +142,6 @@ function genericTargetCacheTests( let persistence: Persistence; beforeEach(async () => { persistence = await persistencePromise(); - persistence.referenceDelegate.setInMemoryPins(new ReferenceSet()); cache = new TestTargetCache(persistence, persistence.getTargetCache()); }); diff --git a/packages/firestore/test/unit/specs/listen_spec.test.ts b/packages/firestore/test/unit/specs/listen_spec.test.ts index eeca9d8b21d..8062e8aae45 100644 --- a/packages/firestore/test/unit/specs/listen_spec.test.ts +++ b/packages/firestore/test/unit/specs/listen_spec.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,40 @@ describeSpec('Listens:', [], () => { } ); + specTest( + 'Documents outside of view are cleared when listen is removed.', + ['eager-gc', 'exclusive'], + '', + () => { + const filteredQuery = Query.atPath(path('collection')).addFilter( + filter('matches', '==', true) + ); + const unfilteredQuery = Query.atPath(path('collection')); + const docA = doc('collection/a', 1000, { matches: true }); + const docB = doc('collection/b', 1000, { matches: true }); + return ( + spec() + .userSets('collection/a', { matches: false }) + .userListens(filteredQuery) + .watchAcksFull(filteredQuery, 1000, docA, docB) + // DocA doesn't match because of a pending mutation + .expectEvents(filteredQuery, { added: [docB] }) + .userSets('collection/b', { matches: false }) + // DocB doesn't match because of a pending mutation + .expectEvents(filteredQuery, { removed: [docB] }) + .userUnlistens(filteredQuery) + // Should get no events since documents are filtered + .userListens(filteredQuery) + .userUnlistens(filteredQuery) + .writeAcks('collection/a', 2000) + .writeAcks('collection/b', 3000) + // Should get no events since documents were GCed + .userListens(unfilteredQuery) + .userUnlistens(unfilteredQuery) + ); + } + ); + specTest('Contents of query update when new data is received.', [], () => { const query = Query.atPath(path('collection')); const docA = doc('collection/a', 1000, { key: 'a' });