Skip to content

Commit 14ccbad

Browse files
Use collectionGroupReadTimeIndex for multi-tab synchronization (#6073)
1 parent 69aa7b0 commit 14ccbad

17 files changed

+222
-267
lines changed

packages/firestore/src/core/bundle.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,10 @@ export interface BundledDocument {
4343
*/
4444
export type BundledDocuments = BundledDocument[];
4545

46-
export class BundleLoadResult {
47-
constructor(
48-
readonly progress: LoadBundleTaskProgress,
49-
readonly changedDocs: DocumentMap
50-
) {}
46+
export interface BundleLoadResult {
47+
readonly progress: LoadBundleTaskProgress;
48+
readonly changedCollectionGroups: Set<string>;
49+
readonly changedDocs: DocumentMap;
5150
}
5251

5352
/**

packages/firestore/src/core/bundle_impl.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { documentKeySet, DocumentKeySet } from '../model/collections';
2626
import { MutableDocument } from '../model/document';
2727
import { DocumentKey } from '../model/document_key';
28+
import { ResourcePath } from '../model/path';
2829
import {
2930
BundleMetadata as ProtoBundleMetadata,
3031
NamedQuery as ProtoNamedQuery
@@ -91,6 +92,8 @@ export class BundleLoader {
9192
private queries: ProtoNamedQuery[] = [];
9293
/** Batched documents to be saved into storage */
9394
private documents: BundledDocuments = [];
95+
/** The collection groups affected by this bundle. */
96+
private collectionGroups = new Set<string>();
9497

9598
constructor(
9699
private bundleMetadata: ProtoBundleMetadata,
@@ -120,6 +123,14 @@ export class BundleLoader {
120123
if (!element.payload.documentMetadata.exists) {
121124
++documentsLoaded;
122125
}
126+
const path = ResourcePath.fromString(
127+
element.payload.documentMetadata.name!
128+
);
129+
debugAssert(
130+
path.length >= 2,
131+
'The document name does not point to a document.'
132+
);
133+
this.collectionGroups.add(path.get(path.length - 2));
123134
} else if (element.payload.document) {
124135
debugAssert(
125136
this.documents.length > 0 &&
@@ -173,7 +184,7 @@ export class BundleLoader {
173184
);
174185
debugAssert(!!this.bundleMetadata.id, 'Bundle ID must be set.');
175186

176-
const changedDocuments = await localStoreApplyBundledDocuments(
187+
const changedDocs = await localStoreApplyBundledDocuments(
177188
this.localStore,
178189
new BundleConverterImpl(this.serializer),
179190
this.documents,
@@ -191,7 +202,11 @@ export class BundleLoader {
191202
}
192203

193204
this.progress.taskState = 'Success';
194-
return new BundleLoadResult({ ...this.progress }, changedDocuments);
205+
return {
206+
progress: this.progress,
207+
changedCollectionGroups: this.collectionGroups,
208+
changedDocs
209+
};
195210
}
196211
}
197212

packages/firestore/src/core/component_provider.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ import {
2222
IndexedDbPersistence
2323
} from '../local/indexeddb_persistence';
2424
import { LocalStore } from '../local/local_store';
25-
import {
26-
newLocalStore,
27-
localStoreSynchronizeLastDocumentChangeReadTime
28-
} from '../local/local_store_impl';
25+
import { newLocalStore } from '../local/local_store_impl';
2926
import { LruParams } from '../local/lru_garbage_collector';
3027
import { LruScheduler } from '../local/lru_garbage_collector_impl';
3128
import {
@@ -174,7 +171,6 @@ export class IndexedDbOfflineComponentProvider extends MemoryOfflineComponentPro
174171

175172
async initialize(cfg: ComponentConfiguration): Promise<void> {
176173
await super.initialize(cfg);
177-
await localStoreSynchronizeLastDocumentChangeReadTime(this.localStore);
178174

179175
await this.onlineComponentProvider.initialize(this, cfg);
180176

packages/firestore/src/core/query.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,21 @@ function queryMatchesBounds(query: Query, doc: Document): boolean {
526526
return true;
527527
}
528528

529+
/**
530+
* Returns the collection group that this query targets.
531+
*
532+
* PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
533+
* synchronization for query results.
534+
*/
535+
export function queryCollectionGroup(query: Query): string {
536+
return (
537+
query.collectionGroup ||
538+
(query.path.length % 2 === 1
539+
? query.path.lastSegment()
540+
: query.path.get(query.path.length - 2))
541+
);
542+
}
543+
529544
/**
530545
* Returns a new comparator function that can be used to compare two documents
531546
* based on the Query's ordering constraint.

packages/firestore/src/core/sync_engine_impl.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import {
9292
newQueryForPath,
9393
Query,
9494
queryEquals,
95+
queryCollectionGroup,
9596
queryToTarget,
9697
stringifyQuery
9798
} from './query';
@@ -1171,13 +1172,16 @@ async function synchronizeViewAndComputeSnapshot(
11711172
*/
11721173
// PORTING NOTE: Multi-Tab only.
11731174
export async function syncEngineSynchronizeWithChangedDocuments(
1174-
syncEngine: SyncEngine
1175+
syncEngine: SyncEngine,
1176+
collectionGroup: string
11751177
): Promise<void> {
11761178
const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl);
11771179

1178-
return localStoreGetNewDocumentChanges(syncEngineImpl.localStore).then(
1179-
changes =>
1180-
syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, changes)
1180+
return localStoreGetNewDocumentChanges(
1181+
syncEngineImpl.localStore,
1182+
collectionGroup
1183+
).then(changes =>
1184+
syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, changes)
11811185
);
11821186
}
11831187

@@ -1432,12 +1436,14 @@ export async function syncEngineApplyTargetState(
14321436
return;
14331437
}
14341438

1435-
if (syncEngineImpl.queriesByTarget.has(targetId)) {
1439+
const query = syncEngineImpl.queriesByTarget.get(targetId);
1440+
if (query && query.length > 0) {
14361441
switch (state) {
14371442
case 'current':
14381443
case 'not-current': {
14391444
const changes = await localStoreGetNewDocumentChanges(
1440-
syncEngineImpl.localStore
1445+
syncEngineImpl.localStore,
1446+
queryCollectionGroup(query[0])
14411447
);
14421448
const synthesizedRemoteEvent =
14431449
RemoteEvent.createSynthesizedRemoteEventForCurrentChange(
@@ -1565,16 +1571,17 @@ export function syncEngineLoadBundle(
15651571
const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl);
15661572

15671573
// eslint-disable-next-line @typescript-eslint/no-floating-promises
1568-
loadBundleImpl(syncEngineImpl, bundleReader, task).then(() => {
1569-
syncEngineImpl.sharedClientState.notifyBundleLoaded();
1574+
loadBundleImpl(syncEngineImpl, bundleReader, task).then(collectionGroups => {
1575+
syncEngineImpl.sharedClientState.notifyBundleLoaded(collectionGroups);
15701576
});
15711577
}
15721578

1579+
/** Loads a bundle and returns the list of affected collection groups. */
15731580
async function loadBundleImpl(
15741581
syncEngine: SyncEngineImpl,
15751582
reader: BundleReader,
15761583
task: LoadBundleTask
1577-
): Promise<void> {
1584+
): Promise<Set<string>> {
15781585
try {
15791586
const metadata = await reader.getMetadata();
15801587
const skip = await localStoreHasNewerBundle(
@@ -1584,7 +1591,7 @@ async function loadBundleImpl(
15841591
if (skip) {
15851592
await reader.close();
15861593
task._completeWith(bundleSuccessProgress(metadata));
1587-
return;
1594+
return Promise.resolve(new Set<string>());
15881595
}
15891596

15901597
task._updateProgress(bundleInitialProgress(metadata));
@@ -1609,9 +1616,6 @@ async function loadBundleImpl(
16091616
}
16101617

16111618
const result = await loader.complete();
1612-
// TODO(b/160876443): This currently raises snapshots with
1613-
// `fromCache=false` if users already listen to some queries and bundles
1614-
// has newer version.
16151619
await syncEngineEmitNewSnapsAndNotifyLocalStore(
16161620
syncEngine,
16171621
result.changedDocs,
@@ -1621,8 +1625,10 @@ async function loadBundleImpl(
16211625
// Save metadata, so loading the same bundle will skip.
16221626
await localStoreSaveBundle(syncEngine.localStore, metadata);
16231627
task._completeWith(result.progress);
1628+
return Promise.resolve(result.changedCollectionGroups);
16241629
} catch (e) {
16251630
logWarn(LOG_TAG, `Loading bundle failed with ${e}`);
16261631
task._failWith(e);
1632+
return Promise.resolve(new Set<string>());
16271633
}
16281634
}

packages/firestore/src/local/indexeddb_remote_document_cache.ts

Lines changed: 1 addition & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { MutableDocument } from '../model/document';
2626
import { DocumentKey } from '../model/document_key';
2727
import { IndexOffset } from '../model/field_index';
2828
import { ResourcePath } from '../model/path';
29-
import { debugAssert, debugCast, hardAssert } from '../util/assert';
29+
import { debugAssert, hardAssert } from '../util/assert';
3030
import { primitiveComparator } from '../util/misc';
3131
import { ObjectMap } from '../util/obj_map';
3232
import { SortedMap } from '../util/sorted_map';
@@ -41,14 +41,12 @@ import {
4141
DbRemoteDocumentGlobalKey,
4242
DbRemoteDocumentGlobalStore,
4343
DbRemoteDocumentKey,
44-
DbRemoteDocumentReadTimeIndex,
4544
DbRemoteDocumentStore,
4645
DbTimestampKey
4746
} from './indexeddb_sentinels';
4847
import { getStore } from './indexeddb_transaction';
4948
import {
5049
fromDbRemoteDocument,
51-
fromDbTimestampKey,
5250
LocalSerializer,
5351
toDbRemoteDocument,
5452
toDbTimestampKey
@@ -411,77 +409,6 @@ export function newIndexedDbRemoteDocumentCache(
411409
return new IndexedDbRemoteDocumentCacheImpl(serializer);
412410
}
413411

414-
/**
415-
* Returns the set of documents that have changed since the specified read
416-
* time.
417-
*/
418-
// PORTING NOTE: This is only used for multi-tab synchronization.
419-
export function remoteDocumentCacheGetNewDocumentChanges(
420-
remoteDocumentCache: IndexedDbRemoteDocumentCache,
421-
transaction: PersistenceTransaction,
422-
sinceReadTime: SnapshotVersion
423-
): PersistencePromise<{
424-
changedDocs: MutableDocumentMap;
425-
readTime: SnapshotVersion;
426-
}> {
427-
const remoteDocumentCacheImpl = debugCast(
428-
remoteDocumentCache,
429-
IndexedDbRemoteDocumentCacheImpl // We only support IndexedDb in multi-tab mode.
430-
);
431-
let changedDocs = mutableDocumentMap();
432-
433-
let lastReadTime = toDbTimestampKey(sinceReadTime);
434-
435-
const documentsStore = remoteDocumentsStore(transaction);
436-
const range = IDBKeyRange.lowerBound(lastReadTime, true);
437-
return documentsStore
438-
.iterate(
439-
{ index: DbRemoteDocumentReadTimeIndex, range },
440-
(_, dbRemoteDoc) => {
441-
// Unlike `getEntry()` and others, `getNewDocumentChanges()` parses
442-
// the documents directly since we want to keep sentinel deletes.
443-
const doc = fromDbRemoteDocument(
444-
remoteDocumentCacheImpl.serializer,
445-
dbRemoteDoc
446-
);
447-
changedDocs = changedDocs.insert(doc.key, doc);
448-
lastReadTime = dbRemoteDoc.readTime!;
449-
}
450-
)
451-
.next(() => {
452-
return {
453-
changedDocs,
454-
readTime: fromDbTimestampKey(lastReadTime)
455-
};
456-
});
457-
}
458-
459-
/**
460-
* Returns the read time of the most recently read document in the cache, or
461-
* SnapshotVersion.min() if not available.
462-
*/
463-
// PORTING NOTE: This is only used for multi-tab synchronization.
464-
export function remoteDocumentCacheGetLastReadTime(
465-
transaction: PersistenceTransaction
466-
): PersistencePromise<SnapshotVersion> {
467-
const documentsStore = remoteDocumentsStore(transaction);
468-
469-
// If there are no existing entries, we return SnapshotVersion.min().
470-
let readTime = SnapshotVersion.min();
471-
472-
return documentsStore
473-
.iterate(
474-
{ index: DbRemoteDocumentReadTimeIndex, reverse: true },
475-
(key, dbRemoteDoc, control) => {
476-
if (dbRemoteDoc.readTime) {
477-
readTime = fromDbTimestampKey(dbRemoteDoc.readTime);
478-
}
479-
control.done();
480-
}
481-
)
482-
.next(() => readTime);
483-
}
484-
485412
/**
486413
* Handles the details of adding and updating documents in the IndexedDbRemoteDocumentCache.
487414
*

packages/firestore/src/local/indexeddb_schema_converter.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,6 @@ import {
9393
DbRemoteDocumentGlobalStore,
9494
DbRemoteDocumentKey,
9595
DbRemoteDocumentKeyPath,
96-
DbRemoteDocumentReadTimeIndex,
97-
DbRemoteDocumentReadTimeIndexPath,
9896
DbRemoteDocumentStore,
9997
DbTargetDocumentDocumentTargetsIndex,
10098
DbTargetDocumentDocumentTargetsKeyPath,
@@ -532,11 +530,6 @@ function createRemoteDocumentCache(db: IDBDatabase): void {
532530
DbRemoteDocumentDocumentKeyIndex,
533531
DbRemoteDocumentDocumentKeyIndexPath
534532
);
535-
remoteDocumentStore.createIndex(
536-
DbRemoteDocumentReadTimeIndex,
537-
DbRemoteDocumentReadTimeIndexPath,
538-
{ unique: false }
539-
);
540533
remoteDocumentStore.createIndex(
541534
DbRemoteDocumentCollectionGroupIndex,
542535
DbRemoteDocumentCollectionGroupIndexPath

packages/firestore/src/local/indexeddb_sentinels.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,6 @@ export const DbRemoteDocumentKeyPath = [
148148
'documentId'
149149
];
150150

151-
/**
152-
* An index that provides access to all entries sorted by read time (which
153-
* corresponds to the last modification time of each row).
154-
*
155-
* This index is used to provide a changelog for Multi-Tab.
156-
*/
157-
export const DbRemoteDocumentReadTimeIndex = 'readTimeIndex';
158-
159-
// TODO(indexing): Consider re-working Multi-Tab to use the collectionGroupIndex
160-
export const DbRemoteDocumentReadTimeIndexPath = 'readTime';
161-
162151
/** An index that provides access to documents by key. */
163152
export const DbRemoteDocumentDocumentKeyIndex = 'documentKeyIndex';
164153

0 commit comments

Comments
 (0)