Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1f69e8a
feat(firestore): port remote ID fix from firestore
MarkDuckworth May 7, 2026
4a16861
PR feedback wrt performance
MarkDuckworth May 7, 2026
e326701
Add RemoteTargetData class and fixed the remote store port to match web
MarkDuckworth May 12, 2026
39cc411
clean raiseWatchSnapshot
MarkDuckworth May 12, 2026
c96ccd5
Make RemoteEvent generic to support different target id types
MarkDuckworth May 12, 2026
495aee6
reverting unnecessary mapping
MarkDuckworth May 12, 2026
8ec6d68
Update spec test runner to match js
MarkDuckworth May 13, 2026
8932962
Merge branch 'main' into markduckworth/remote-store-remote-id
MarkDuckworth May 19, 2026
8044181
SpecTestCase implementation cleanup
MarkDuckworth May 27, 2026
9204a1c
Merge branch 'markduckworth/remote-store-remote-id' of github.com:fir…
MarkDuckworth May 27, 2026
302f28b
cleanup
MarkDuckworth May 27, 2026
45a5ab8
Copy in latest spec tests from web. Fix SpecTestCase and MockDatastore
MarkDuckworth May 27, 2026
beb0b54
Port 9985 from JS
MarkDuckworth May 28, 2026
97d091e
Merge branch 'main' of github.com:firebase/firebase-android-sdk into …
MarkDuckworth May 29, 2026
8962da5
Merge branch 'main' into markduckworth/remote-store-remote-id
MarkDuckworth May 29, 2026
b9e0850
Merge branch 'main' into markduckworth/remote-store-remote-id
MarkDuckworth Jun 8, 2026
327e5f3
Merge branch 'main' into markduckworth/remote-store-remote-id
MarkDuckworth Jun 9, 2026
c676daf
Merge branch 'markduckworth/remote-store-remote-id' of github.com:fir…
MarkDuckworth Jun 10, 2026
b0831a9
Converted RemoteTargetId and TargetData implementations to Kotlin
MarkDuckworth Jun 10, 2026
7d418b6
Merge branch 'main' into markduckworth/remote-store-remote-id
MarkDuckworth Jun 12, 2026
81230fe
Merge branch 'main' into markduckworth/remote-store-remote-id
MarkDuckworth Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,14 @@ public int listen(QueryOrPipeline query, boolean shouldListenToRemote) {
TargetData targetData = localStore.allocateTarget(query.toTargetOrPipeline());

ViewSnapshot viewSnapshot =
initializeViewAndComputeSnapshot(
query, targetData.getTargetId(), targetData.getResumeToken());
initializeViewAndComputeSnapshot(query, targetData.targetId, targetData.resumeToken);
syncEngineListener.onViewSnapshots(Collections.singletonList(viewSnapshot));

if (shouldListenToRemote) {
remoteStore.listen(targetData);
}

return targetData.getTargetId();
return targetData.targetId;
}

private ViewSnapshot initializeViewAndComputeSnapshot(
Expand Down Expand Up @@ -366,7 +365,7 @@ public Task<Map<String, Value>> runAggregateQuery(

/** Called by FirestoreClient to notify us of a new remote event. */
@Override
public void handleRemoteEvent(RemoteEvent event) {
public void handleRemoteEvent(RemoteEvent<Integer> event) {
assertCallback("handleRemoteEvent");

// Update `receivedDocument` as appropriate for any limbo targets.
Expand Down Expand Up @@ -465,8 +464,8 @@ public void handleRejectedListen(int targetId, Status error) {
Map<DocumentKey, MutableDocument> documentUpdates =
Collections.singletonMap(limboKey, result);
Set<DocumentKey> limboDocuments = Collections.singleton(limboKey);
RemoteEvent event =
new RemoteEvent(
RemoteEvent<Integer> event =
new RemoteEvent<>(
SnapshotVersion.NONE,
/* targetChanges= */ Collections.emptyMap(),
/* targetMismatches= */ Collections.emptyMap(),
Expand Down Expand Up @@ -673,7 +672,8 @@ private void removeLimboTarget(DocumentKey key) {
* snapshot.
*/
private void emitNewSnapsAndNotifyLocalStore(
ImmutableSortedMap<DocumentKey, Document> changes, @Nullable RemoteEvent remoteEvent) {
ImmutableSortedMap<DocumentKey, Document> changes,
@Nullable RemoteEvent<Integer> remoteEvent) {
List<ViewSnapshot> newSnapshots = new ArrayList<>();
List<LocalViewChanges> documentChangesInAllViews = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ public static TargetIdGenerator forSyncEngine() {
return new TargetIdGenerator(SYNC_ENGINE_ID, 1);
}

/**
* Creates and returns a new TargetIdGenerator for remote target cache watches.
*
* @return A new instance of TargetIdGenerator starting at 1000 (even).
*/
public static TargetIdGenerator forRemoteTargetCache() {
return new TargetIdGenerator(QUERY_CACHE_ID, 1000);
}

/**
* Creates and returns a new TargetIdGenerator for remote sync engine watches.
*
* @return A new instance of TargetIdGenerator starting at 1001 (odd).
*/
public static TargetIdGenerator forRemoteSyncEngine() {
return new TargetIdGenerator(SYNC_ENGINE_ID, 1001);
}

private static final int QUERY_CACHE_ID = 0;
private static final int SYNC_ENGINE_ID = 1;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.firebase.firestore.local

import com.google.firebase.firestore.core.TargetOrPipeline
import com.google.firebase.firestore.model.SnapshotVersion
import com.google.firebase.firestore.util.Preconditions
import com.google.protobuf.ByteString
import java.util.Objects

/** An abstract set of metadata that the store will need to keep track of for each target. */
abstract class BaseTargetData
protected constructor(
target: TargetOrPipeline,
@JvmField val sequenceNumber: Long,
@JvmField val purpose: QueryPurpose,
snapshotVersion: SnapshotVersion,
@JvmField val lastLimboFreeSnapshotVersion: SnapshotVersion,
resumeToken: ByteString,
@JvmField val expectedCount: Int?
) {
@JvmField val target: TargetOrPipeline
@JvmField val snapshotVersion: SnapshotVersion
@JvmField val resumeToken: ByteString

/** Creates a new BaseTargetData with the given values. */
init {
this.target = Preconditions.checkNotNull<TargetOrPipeline>(target)
this.snapshotVersion = Preconditions.checkNotNull<SnapshotVersion>(snapshotVersion)
this.resumeToken = Preconditions.checkNotNull<ByteString>(resumeToken)
}

/** Creates a new target data instance with an updated sequence number. */
abstract fun withSequenceNumber(sequenceNumber: Long): BaseTargetData?

/** Creates a new target data instance with an updated resume token and snapshot version. */
abstract fun withResumeToken(
resumeToken: ByteString,
snapshotVersion: SnapshotVersion
): BaseTargetData?

/** Creates a new target data instance with an updated expected count. */
abstract fun withExpectedCount(expectedCount: Int?): BaseTargetData?

/** Creates a new target data instance with an updated last limbo free snapshot version number. */
abstract fun withLastLimboFreeSnapshotVersion(
lastLimboFreeSnapshotVersion: SnapshotVersion
): BaseTargetData?

override fun equals(o: Any?): Boolean {
if (this === o) {
return true
}
if (o == null || javaClass != o.javaClass) {
return false
}

val that = o as BaseTargetData
return target == that.target &&
sequenceNumber == that.sequenceNumber &&
purpose == that.purpose &&
snapshotVersion == that.snapshotVersion &&
lastLimboFreeSnapshotVersion == that.lastLimboFreeSnapshotVersion &&
resumeToken == that.resumeToken &&
expectedCount == that.expectedCount
}

override fun hashCode(): Int {
var result = target.hashCode()
result = 31 * result + sequenceNumber.toInt()
result = 31 * result + purpose.hashCode()
result = 31 * result + snapshotVersion.hashCode()
result = 31 * result + lastLimboFreeSnapshotVersion.hashCode()
result = 31 * result + resumeToken.hashCode()
result = 31 * result + Objects.hashCode(expectedCount)
return result
}

override fun toString(): String {
return (javaClass.getSimpleName() +
"{" +
"target=" +
target +
", sequenceNumber=" +
sequenceNumber +
", purpose=" +
purpose +
", snapshotVersion=" +
snapshotVersion +
", lastLimboFreeSnapshotVersion=" +
lastLimboFreeSnapshotVersion +
", resumeToken=" +
resumeToken +
", expectedCount=" +
expectedCount +
'}')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,23 +206,23 @@ MutationBatch decodeMutationBatch(com.google.firebase.firestore.proto.WriteBatch

com.google.firebase.firestore.proto.Target encodeTargetData(TargetData targetData) {
hardAssert(
QueryPurpose.LISTEN.equals(targetData.getPurpose()),
QueryPurpose.LISTEN.equals(targetData.purpose),
"Only queries with purpose %s may be stored, got %s",
QueryPurpose.LISTEN,
targetData.getPurpose());
targetData.purpose);

com.google.firebase.firestore.proto.Target.Builder result =
com.google.firebase.firestore.proto.Target.newBuilder();

result
.setTargetId(targetData.getTargetId())
.setLastListenSequenceNumber(targetData.getSequenceNumber())
.setTargetId(targetData.targetId)
.setLastListenSequenceNumber(targetData.sequenceNumber)
.setLastLimboFreeSnapshotVersion(
rpcSerializer.encodeVersion(targetData.getLastLimboFreeSnapshotVersion()))
.setSnapshotVersion(rpcSerializer.encodeVersion(targetData.getSnapshotVersion()))
.setResumeToken(targetData.getResumeToken());
rpcSerializer.encodeVersion(targetData.lastLimboFreeSnapshotVersion))
.setSnapshotVersion(rpcSerializer.encodeVersion(targetData.snapshotVersion))
.setResumeToken(targetData.resumeToken);

TargetOrPipeline target = targetData.getTarget();
TargetOrPipeline target = targetData.target;
if (target.isTarget()) {
if (target.target().isDocumentQuery()) {
result.setDocuments(rpcSerializer.encodeDocumentsTarget(target.target()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,8 @@ public void setSessionsToken(ByteString sessionToken) {
*
* <p>LocalDocuments are re-calculated if there are remaining mutations in the queue.
*/
public ImmutableSortedMap<DocumentKey, Document> applyRemoteEvent(RemoteEvent remoteEvent) {
public ImmutableSortedMap<DocumentKey, Document> applyRemoteEvent(
RemoteEvent<Integer> remoteEvent) {
SnapshotVersion remoteVersion = remoteEvent.getSnapshotVersion();

// TODO: Call queryEngine.handleDocumentChange() appropriately.
Expand Down Expand Up @@ -571,23 +572,23 @@ private DocumentChangeResult populateDocumentChanges(
private static boolean shouldPersistTargetData(
TargetData oldTargetData, TargetData newTargetData, @Nullable TargetChange change) {
// Always persist query data if we don't already have a resume token.
if (oldTargetData.getResumeToken().isEmpty()) return true;
if (oldTargetData.resumeToken.isEmpty()) return true;

// Don't allow resume token changes to be buffered indefinitely. This allows us to be reasonably
// up-to-date after a crash and avoids needing to loop over all active queries on shutdown.
// Especially in the browser we may not get time to do anything interesting while the current
// tab is closing.
long newSeconds = newTargetData.getSnapshotVersion().getTimestamp().getSeconds();
long oldSeconds = oldTargetData.getSnapshotVersion().getTimestamp().getSeconds();
long newSeconds = newTargetData.snapshotVersion.getTimestamp().getSeconds();
long oldSeconds = oldTargetData.snapshotVersion.getTimestamp().getSeconds();
long timeDelta = newSeconds - oldSeconds;
if (timeDelta >= RESUME_TOKEN_MAX_AGE_SECONDS) return true;

// Update the target cache if sufficient time has passed since the last
// LastLimboFreeSnapshotVersion
long newLimboFreeSeconds =
newTargetData.getLastLimboFreeSnapshotVersion().getTimestamp().getSeconds();
newTargetData.lastLimboFreeSnapshotVersion.getTimestamp().getSeconds();
long oldLimboFreeSeconds =
oldTargetData.getLastLimboFreeSnapshotVersion().getTimestamp().getSeconds();
oldTargetData.lastLimboFreeSnapshotVersion.getTimestamp().getSeconds();
long limboFreeTimeDelta = newLimboFreeSeconds - oldLimboFreeSeconds;
if (limboFreeTimeDelta >= RESUME_TOKEN_MAX_AGE_SECONDS) return true;

Expand Down Expand Up @@ -626,7 +627,7 @@ public void notifyLocalViewChanges(List<LocalViewChanges> viewChanges) {
targetId);

// Advance the last limbo free snapshot version
SnapshotVersion lastLimboFreeSnapshotVersion = targetData.getSnapshotVersion();
SnapshotVersion lastLimboFreeSnapshotVersion = targetData.snapshotVersion;
TargetData updatedTargetData =
targetData.withLastLimboFreeSnapshotVersion(lastLimboFreeSnapshotVersion);
queryDataByTarget.put(targetId, updatedTargetData);
Expand Down Expand Up @@ -668,7 +669,7 @@ public TargetData allocateTarget(TargetOrPipeline target) {
if (cached != null) {
// This query has been listened to previously, so reuse the previous targetID.
// TODO: freshen last accessed date?
targetId = cached.getTargetId();
targetId = cached.targetId;
// deserialized target is missing a firestore reference, so we use the one that has it
// to replace just to be safe.
cached = cached.withTarget(target);
Expand Down Expand Up @@ -759,8 +760,8 @@ public ImmutableSortedMap<DocumentKey, Document> applyBundledDocuments(
documentMap.put(documentKey, document);
}

targetCache.removeMatchingKeysForTargetId(umbrellaTargetData.getTargetId());
targetCache.addMatchingKeys(documentKeys, umbrellaTargetData.getTargetId());
targetCache.removeMatchingKeysForTargetId(umbrellaTargetData.targetId);
targetCache.addMatchingKeys(documentKeys, umbrellaTargetData.targetId);

DocumentChangeResult result = populateDocumentChanges(documentMap);
Map<DocumentKey, MutableDocument> changedDocs = result.changedDocuments;
Expand All @@ -777,13 +778,13 @@ public void saveNamedQuery(NamedQuery namedQuery, ImmutableSortedSet<DocumentKey
TargetData existingTargetData =
allocateTarget(
new TargetOrPipeline.TargetWrapper(namedQuery.getBundledQuery().getTarget()));
int targetId = existingTargetData.getTargetId();
int targetId = existingTargetData.targetId;

persistence.runTransaction(
"Saved named query",
() -> {
// Only update the matching documents if it is newer than what the SDK already has
if (namedQuery.getReadTime().compareTo(existingTargetData.getSnapshotVersion()) > 0) {
if (namedQuery.getReadTime().compareTo(existingTargetData.snapshotVersion) > 0) {
// Update existing target data because the query from the bundle is newer.
TargetData newTargetData =
existingTargetData.withResumeToken(ByteString.EMPTY, namedQuery.getReadTime());
Expand Down Expand Up @@ -861,7 +862,7 @@ public void releaseTarget(int targetId) {
// Note: This also updates the query cache
persistence.getReferenceDelegate().removeTarget(targetData);
queryDataByTarget.remove(targetId);
targetIdByTarget.remove(targetData.getTarget());
targetIdByTarget.remove(targetData.target);
});
}

Expand All @@ -878,8 +879,8 @@ public QueryResult executeQuery(QueryOrPipeline query, boolean usePreviousResult
ImmutableSortedSet<DocumentKey> remoteKeys = DocumentKey.emptyKeySet();

if (targetData != null) {
lastLimboFreeSnapshotVersion = targetData.getLastLimboFreeSnapshotVersion();
remoteKeys = this.targetCache.getMatchingKeysForTargetId(targetData.getTargetId());
lastLimboFreeSnapshotVersion = targetData.lastLimboFreeSnapshotVersion;
remoteKeys = this.targetCache.getMatchingKeysForTargetId(targetData.targetId);
}

ImmutableSortedMap<DocumentKey, Document> documents =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ long getNthSequenceNumber(int count) {
return ListenSequence.INVALID;
}
RollingSequenceNumberBuffer buffer = new RollingSequenceNumberBuffer(count);
delegate.forEachTarget((targetData) -> buffer.addElement(targetData.getSequenceNumber()));
delegate.forEachTarget((targetData) -> buffer.addElement(targetData.sequenceNumber));
delegate.forEachOrphanedDocumentSequenceNumber(buffer::addElement);
return buffer.getMaxValue();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void removeMutationReference(DocumentKey key) {
@Override
public void removeTarget(TargetData targetData) {
MemoryTargetCache targetCache = persistence.getTargetCache();
for (DocumentKey key : targetCache.getMatchingKeysForTargetId(targetData.getTargetId())) {
for (DocumentKey key : targetCache.getMatchingKeysForTargetId(targetData.targetId)) {
orphanedDocuments.add(key);
}
targetCache.removeTargetData(targetData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ public void setLastRemoteSnapshotVersion(SnapshotVersion snapshotVersion) {

@Override
public void addTargetData(TargetData targetData) {
targets.put(targetData.getTarget(), targetData);
int targetId = targetData.getTargetId();
targets.put(targetData.target, targetData);
int targetId = targetData.targetId;
if (targetId > highestTargetId) {
highestTargetId = targetId;
}
if (targetData.getSequenceNumber() > highestSequenceNumber) {
highestSequenceNumber = targetData.getSequenceNumber();
if (targetData.sequenceNumber > highestSequenceNumber) {
highestSequenceNumber = targetData.sequenceNumber;
}
}

Expand All @@ -105,8 +105,8 @@ public void updateTargetData(TargetData targetData) {

@Override
public void removeTargetData(TargetData targetData) {
targets.remove(targetData.getTarget());
references.removeReferencesForId(targetData.getTargetId());
targets.remove(targetData.target);
references.removeReferencesForId(targetData.targetId);
}

/**
Expand All @@ -120,8 +120,8 @@ int removeQueries(long upperBound, SparseArray<?> activeTargetIds) {
for (Iterator<Map.Entry<TargetOrPipeline, TargetData>> it = targets.entrySet().iterator();
it.hasNext(); ) {
Map.Entry<TargetOrPipeline, TargetData> entry = it.next();
int targetId = entry.getValue().getTargetId();
long sequenceNumber = entry.getValue().getSequenceNumber();
int targetId = entry.getValue().targetId;
long sequenceNumber = entry.getValue().sequenceNumber;
if (sequenceNumber <= upperBound && activeTargetIds.get(targetId) == null) {
it.remove();
removeMatchingKeysForTargetId(targetId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ private void rewriteCanonicalIds() {
try {
Target targetProto = Target.parseFrom(targetProtoBytes);
TargetData targetData = serializer.decodeTargetData(targetProto);
String updatedCanonicalId = targetData.getTarget().canonicalId();
String updatedCanonicalId = targetData.target.canonicalId();
db.execSQL(
"UPDATE targets SET canonical_id = ? WHERE target_id = ?",
new Object[] {updatedCanonicalId, targetId});
Expand Down
Loading
Loading