Skip to content

Firebase Data Connect: Mutations do not invalidate related query caches #9698

@hatoya

Description

@hatoya

Operating System

macOS Tahoe 26.3.1

Environment (if applicable)

Node.js v24.13.0

Firebase SDK Version

12.1.0

Firebase SDK Product(s)

DataConnect

Project Tooling

Angular 21 (but issue is framework-agnostic)

Detailed Problem Description

After executing a mutation via Firebase Data Connect SDK, related query caches are not invalidated. Subsequent query subscriptions continue to return stale cached data instead of fetching fresh results from the server.

Steps and code to reproduce issue

  1. Subscribe to a query (e.g., ListItems) using subscribe()
  2. Execute a mutation that modifies the underlying data (e.g., CreateItem)
  3. Observe that the query subscription still returns the old cached data
  4. Only a manual page refresh or explicit executeQuery() call fetches updated data

Expected Behavior

After a mutation completes, related query caches should be invalidated (or at minimum, re-fetched), so that active subscriptions receive updated data.

Actual Behavior

Query caches persist indefinitely after mutations. The MutationManager and QueryManager are completely isolated with no cross-communication.

Root Cause Analysis

After reviewing the SDK source code (@firebase/data-connect/dist/index.esm.js), the following issues were identified:

1. PREFER_CACHE behavior is hardcoded with no fetch policy option

In QueryManager.addSubscription(), the SDK always returns cached data if available and only fetches from the server when no cache exists:

// QueryManager.addSubscription()
if (trackedQuery.currentCache !== null) {
    // Returns cached data immediately
    onResultCallback({
        data: cachedData,
        source: SOURCE_CACHE,
        ref: queryRef,
        fetchTime: trackedQuery.currentCache.fetchTime
    });
}
// Only fetches if NO cache exists
if (!trackedQuery.currentCache) {
    const promise = this.executeQuery(queryRef);
}

There is no way to configure fetch policies like cache-and-network or network-only.

2. MutationManager is completely isolated from QueryManager

class MutationManager {
    executeMutation(mutationRef) {
        const result = this._transport.invokeMutation(mutationRef.name, mutationRef.variables);
        // ... only tracks the promise in _inflight array
        // NO cache invalidation logic
        return withRefPromise;
    }
}

Both managers are instantiated independently with no reference to each other:

this._queryManager = new QueryManager(this._transport);
this._mutationManager = new MutationManager(this._transport);

3. No dependency tracking between mutations and queries

TrackedQuery contains no metadata about data dependencies:

const newTrackedQuery = {
    ref,
    subscriptions: [],
    currentCache: initialCache || null,
    lastError: null
    // No dependency info, no invalidation flags
};

4. Date comparison is only used for cache version ordering

compareDates() is only used to compare two cached versions of the same query. It never checks whether a mutation has occurred since the cache was set.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions