Skip to content

Commit ca58a29

Browse files
authored
[2/3] Query/Target split: make SyncEngine able to handle the mapping between Target and Query. (#2281)
1 parent 42dc051 commit ca58a29

File tree

3 files changed

+134
-93
lines changed

3 files changed

+134
-93
lines changed

packages/firestore/src/core/sync_engine.ts

Lines changed: 96 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
147147
private queryViewsByQuery = new ObjectMap<Query, QueryView>(q =>
148148
q.canonicalId()
149149
);
150-
private queryViewsByTarget: { [targetId: number]: QueryView } = {};
150+
private queriesByTarget: { [targetId: number]: Query[] } = {};
151151
private limboTargetsByKey = new SortedMap<DocumentKey, TargetId>(
152152
DocumentKey.comparator
153153
);
@@ -223,7 +223,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
223223
targetId = queryData.targetId;
224224
viewSnapshot = await this.initializeViewAndComputeSnapshot(
225225
query,
226-
queryData.targetId,
226+
targetId,
227227
status === 'current'
228228
);
229229
if (this.isPrimary) {
@@ -270,7 +270,10 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
270270

271271
const data = new QueryView(query, targetId, view);
272272
this.queryViewsByQuery.set(query, data);
273-
this.queryViewsByTarget[targetId] = data;
273+
if (!this.queriesByTarget[targetId]) {
274+
this.queriesByTarget[targetId] = [];
275+
}
276+
this.queriesByTarget[targetId].push(query);
274277
return viewChange.snapshot!;
275278
}
276279

@@ -302,6 +305,8 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
302305
const queryView = this.queryViewsByQuery.get(query)!;
303306
assert(!!queryView, 'Trying to unlisten on query not found:' + query);
304307

308+
// TODO(wuandy): Note this does not handle the case where multiple queries
309+
// map to one target, and user request to unlisten on of the queries.
305310
if (this.isPrimary) {
306311
// We need to remove the local query target first to allow us to verify
307312
// whether any other client is still interested in this target.
@@ -312,18 +317,18 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
312317

313318
if (!targetRemainsActive) {
314319
await this.localStore
315-
.releaseQuery(query, /*keepPersistedQueryData=*/ false)
320+
.releaseTarget(queryView.targetId, /*keepPersistedQueryData=*/ false)
316321
.then(() => {
317322
this.sharedClientState.clearQueryState(queryView.targetId);
318323
this.remoteStore.unlisten(queryView.targetId);
319-
this.removeAndCleanupQuery(queryView);
324+
this.removeAndCleanupTarget(queryView.targetId);
320325
})
321326
.catch(ignoreIfPrimaryLeaseLoss);
322327
}
323328
} else {
324-
this.removeAndCleanupQuery(queryView);
325-
await this.localStore.releaseQuery(
326-
query,
329+
this.removeAndCleanupTarget(queryView.targetId);
330+
await this.localStore.releaseTarget(
331+
queryView.targetId,
327332
/*keepPersistedQueryData=*/ true
328333
);
329334
}
@@ -495,13 +500,10 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
495500
);
496501
return this.applyRemoteEvent(event);
497502
} else {
498-
const queryView = this.queryViewsByTarget[targetId];
499-
assert(!!queryView, 'Unknown targetId: ' + targetId);
500503
await this.localStore
501-
.releaseQuery(queryView.query, /* keepPersistedQueryData */ false)
502-
.then(() => this.removeAndCleanupQuery(queryView))
504+
.releaseTarget(targetId, /* keepPersistedQueryData */ false)
505+
.then(() => this.removeAndCleanupTarget(targetId, err))
503506
.catch(ignoreIfPrimaryLeaseLoss);
504-
this.syncEngineListener!.onWatchError(queryView.query, err);
505507
}
506508
}
507509

@@ -681,17 +683,30 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
681683
}
682684
}
683685

684-
private removeAndCleanupQuery(queryView: QueryView): void {
685-
this.sharedClientState.removeLocalQueryTarget(queryView.targetId);
686+
private removeAndCleanupTarget(
687+
targetId: number,
688+
error: Error | null = null
689+
): void {
690+
this.sharedClientState.removeLocalQueryTarget(targetId);
691+
692+
assert(
693+
this.queriesByTarget[targetId] &&
694+
this.queriesByTarget[targetId].length !== 0,
695+
`There are no queries mapped to target id ${targetId}`
696+
);
697+
698+
for (const query of this.queriesByTarget[targetId]) {
699+
this.queryViewsByQuery.delete(query);
700+
if (error) {
701+
this.syncEngineListener!.onWatchError(query, error);
702+
}
703+
}
686704

687-
this.queryViewsByQuery.delete(queryView.query);
688-
delete this.queryViewsByTarget[queryView.targetId];
705+
delete this.queriesByTarget[targetId];
689706

690707
if (this.isPrimary) {
691-
const limboKeys = this.limboDocumentRefs.referencesForId(
692-
queryView.targetId
693-
);
694-
this.limboDocumentRefs.removeReferencesForId(queryView.targetId);
708+
const limboKeys = this.limboDocumentRefs.referencesForId(targetId);
709+
this.limboDocumentRefs.removeReferencesForId(targetId);
695710
limboKeys.forEach(limboKey => {
696711
const isReferenced = this.limboDocumentRefs.containsKey(limboKey);
697712
if (!isReferenced) {
@@ -710,6 +725,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
710725
// This target already got removed, because the query failed.
711726
return;
712727
}
728+
713729
this.remoteStore.unlisten(limboTargetId);
714730
this.limboTargetsByKey = this.limboTargetsByKey.remove(key);
715731
delete this.limboResolutionsByTarget[limboTargetId];
@@ -885,13 +901,19 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
885901
const activeTargets: TargetId[] = [];
886902

887903
let p = Promise.resolve();
888-
objUtils.forEachNumber(this.queryViewsByTarget, (targetId, queryView) => {
904+
objUtils.forEachNumber(this.queriesByTarget, (targetId, _) => {
889905
if (this.sharedClientState.isLocalQueryTarget(targetId)) {
890906
activeTargets.push(targetId);
891907
} else {
892-
p = p.then(() => this.unlisten(queryView.query));
908+
p = p.then(() => {
909+
this.removeAndCleanupTarget(targetId);
910+
return this.localStore.releaseTarget(
911+
targetId,
912+
/*keepPersistedQueryData=*/ true
913+
);
914+
});
893915
}
894-
this.remoteStore.unlisten(queryView.targetId);
916+
this.remoteStore.unlisten(targetId);
895917
});
896918
await p;
897919

@@ -926,27 +948,34 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
926948
const newViewSnapshots: ViewSnapshot[] = [];
927949
for (const targetId of targets) {
928950
let queryData: QueryData;
929-
const queryView = this.queryViewsByTarget[targetId];
930-
if (queryView) {
951+
const queries = this.queriesByTarget[targetId];
952+
953+
if (queries && queries.length !== 0) {
931954
// For queries that have a local View, we need to update their state
932955
// in LocalStore (as the resume token and the snapshot version
933956
// might have changed) and reconcile their views with the persisted
934957
// state (the list of syncedDocuments may have gotten out of sync).
935-
await this.localStore.releaseQuery(
936-
queryView.query,
958+
await this.localStore.releaseTarget(
959+
targetId,
937960
/*keepPersistedQueryData=*/ true
938961
);
939-
queryData = await this.localStore.allocateQuery(queryView.query);
940-
const viewChange = await this.synchronizeViewAndComputeSnapshot(
941-
queryView
942-
);
943-
if (viewChange.snapshot) {
944-
newViewSnapshots.push(viewChange.snapshot);
962+
queryData = await this.localStore.allocateTarget(queries[0].toTarget());
963+
964+
for (const query of queries) {
965+
const queryView = this.queryViewsByQuery.get(query);
966+
assert(!!queryView, `No query view found for ${query}`);
967+
968+
const viewChange = await this.synchronizeViewAndComputeSnapshot(
969+
queryView!
970+
);
971+
if (viewChange.snapshot) {
972+
newViewSnapshots.push(viewChange.snapshot);
973+
}
945974
}
946975
} else {
947976
assert(
948977
this.isPrimary === true,
949-
'A secondary tab should never have an active query without an active view.'
978+
'A secondary tab should never have an active target without an active query.'
950979
);
951980
// For queries that never executed on this client, we need to
952981
// allocate the target in LocalStore and initialize a new View.
@@ -959,8 +988,10 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
959988
/*current=*/ false
960989
);
961990
}
962-
activeQueries.push(queryData);
991+
992+
activeQueries.push(queryData!);
963993
}
994+
964995
this.syncEngineListener!.onWatchChange(newViewSnapshots);
965996
return activeQueries;
966997
}
@@ -983,7 +1014,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
9831014
return;
9841015
}
9851016

986-
if (this.queryViewsByTarget[targetId]) {
1017+
if (this.queriesByTarget[targetId]) {
9871018
switch (state) {
9881019
case 'current':
9891020
case 'not-current': {
@@ -999,13 +1030,11 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
9991030
break;
10001031
}
10011032
case 'rejected': {
1002-
const queryView = this.queryViewsByTarget[targetId];
1003-
this.removeAndCleanupQuery(queryView);
1004-
await this.localStore.releaseQuery(
1005-
queryView.query,
1006-
/*keepPersistedQueryData=*/ true
1033+
await this.localStore.releaseTarget(
1034+
targetId,
1035+
/* keepPersistedQueryData */ true
10071036
);
1008-
this.syncEngineListener!.onWatchError(queryView.query, error!);
1037+
this.removeAndCleanupTarget(targetId, error);
10091038
break;
10101039
}
10111040
default:
@@ -1025,7 +1054,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
10251054

10261055
for (const targetId of added) {
10271056
assert(
1028-
!this.queryViewsByTarget[targetId],
1057+
!this.queriesByTarget[targetId],
10291058
'Trying to add an already active target'
10301059
);
10311060
const target = await this.localStore.getTarget(targetId);
@@ -1040,18 +1069,20 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
10401069
}
10411070

10421071
for (const targetId of removed) {
1043-
const queryView = this.queryViewsByTarget[targetId];
1044-
// Check that the query is still active since the query might have been
1072+
// Check that the target is still active since the target might have been
10451073
// removed if it has been rejected by the backend.
1046-
if (queryView) {
1047-
await this.localStore
1048-
.releaseQuery(queryView.query, /*keepPersistedQueryData=*/ false)
1049-
.then(() => {
1050-
this.remoteStore.unlisten(targetId);
1051-
this.removeAndCleanupQuery(queryView);
1052-
})
1053-
.catch(ignoreIfPrimaryLeaseLoss);
1074+
if (!this.queriesByTarget[targetId]) {
1075+
continue;
10541076
}
1077+
1078+
// Release queries that are still active.
1079+
await this.localStore
1080+
.releaseTarget(targetId, /* keepPersistedQueryData */ false)
1081+
.then(() => {
1082+
this.remoteStore.unlisten(targetId);
1083+
this.removeAndCleanupTarget(targetId);
1084+
})
1085+
.catch(ignoreIfPrimaryLeaseLoss);
10551086
}
10561087
}
10571088

@@ -1074,9 +1105,17 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
10741105
if (limboResolution && limboResolution.receivedDocument) {
10751106
return documentKeySet().add(limboResolution.key);
10761107
} else {
1077-
return this.queryViewsByTarget[targetId]
1078-
? this.queryViewsByTarget[targetId].view.syncedDocuments
1079-
: documentKeySet();
1108+
let keySet = documentKeySet();
1109+
const queries = this.queriesByTarget[targetId];
1110+
if (!queries) {
1111+
return keySet;
1112+
}
1113+
for (const query of queries) {
1114+
const queryView = this.queryViewsByQuery.get(query);
1115+
assert(!!queryView, `No query view found for ${query}`);
1116+
keySet = keySet.unionWith(queryView!.view.syncedDocuments);
1117+
}
1118+
return keySet;
10801119
}
10811120
}
10821121
}

packages/firestore/src/local/local_store.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -885,11 +885,6 @@ export class LocalStore {
885885
}
886886
}
887887

888-
//TODO(wuandy): Delete this temp mothod, it's for change isolation only.
889-
releaseQuery(query: Query, keepPersistedQueryData: boolean): Promise<void> {
890-
return this.releaseTarget(query.toTarget(), keepPersistedQueryData);
891-
}
892-
893888
/**
894889
* Unpin all the documents associated with the given query. If
895890
* `keepPersistedQueryData` is set to false and Eager GC enabled, the method
@@ -899,24 +894,17 @@ export class LocalStore {
899894
*/
900895
// PORTING NOTE: `keepPersistedQueryData` is multi-tab only.
901896
releaseTarget(
902-
target: Target,
897+
targetId: number,
903898
keepPersistedQueryData: boolean
904899
): Promise<void> {
905-
let targetId: number;
900+
const queryData = this.queryDataByTarget.get(targetId)!;
901+
assert(!!queryData, 'Tried to release nonexistent target: ' + targetId);
906902

907903
const mode = keepPersistedQueryData
908904
? 'readwrite-idempotent'
909905
: 'readwrite-primary-idempotent';
910906
return this.persistence
911907
.runTransaction('Release query', mode, txn => {
912-
const cachedTargetId = this.targetIdByTarget.get(target);
913-
assert(
914-
cachedTargetId !== undefined,
915-
'Tried to release nonexistent target: ' + target
916-
);
917-
targetId = cachedTargetId!;
918-
const queryData = this.queryDataByTarget.get(targetId)!;
919-
920908
// References for documents sent via Watch are automatically removed
921909
// when we delete a query's target data from the reference delegate.
922910
// Since this does not remove references for locally mutated documents,
@@ -943,7 +931,7 @@ export class LocalStore {
943931
})
944932
.then(() => {
945933
this.queryDataByTarget = this.queryDataByTarget.remove(targetId);
946-
this.targetIdByTarget.delete(target);
934+
this.targetIdByTarget.delete(queryData.target);
947935
});
948936
}
949937

0 commit comments

Comments
 (0)