Skip to content

Caching for FDC#9439

Open
maneesht wants to merge 78 commits intomainfrom
mtewani/caching-first-pass
Open

Caching for FDC#9439
maneesht wants to merge 78 commits intomainfrom
mtewani/caching-first-pass

Conversation

@maneesht
Copy link
Contributor

@maneesht maneesht commented Jan 6, 2026

No description provided.

@maneesht maneesht requested review from a team and aashishpatil-g as code owners January 6, 2026 23:39
@maneesht maneesht changed the title WIP: Caching Caching for FDC Feb 10, 2026
}

// @public (undocumented)
export interface DataConnectResponse<T> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the response object meant to be exported or is it for internal SDK use?

}
constructor(
private rootStub: EntityNode,
private maxAge: number = 30,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove the default here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. maxAge = 30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxAge should be zero by default.

Copy link
Contributor

@stephenarosaj stephenarosaj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments resolved - LGTM (waiting to approve until other comments resolve)

Copy link
Contributor

@stephenarosaj stephenarosaj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all comments resolved - LGTM

}
}
if (scalarArray.length > 0 && objArray.length > 0) {
throw new DataConnectError(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am logging and storing the original JSON for this case with either entity (similar to empty erray case) - https://github.com/firebase/flutterfire/pull/17988/changes#diff-b09e0ad5accf5925eb2fa32291b9d398531917c1b3f711c4cc3a181eedb6acc5R104

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lets us reproduce the original query when hydrating.

async getResultTree(queryId: string): Promise<ResultTree | undefined> {
return this.resultTrees.get(queryId);
}
async createGlobalId(): Promise<string> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

@aashishpatil-g
Copy link

LGTM pending the comments.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive caching layer for Data Connect, which is a significant and well-executed feature. The architecture is robust, handling entity normalization, cache invalidation on user changes, and providing different fetch policies. The code is well-structured with good separation of concerns and is accompanied by extensive tests. I've identified one high-severity issue with the implementation of the CACHE_ONLY fetch policy, where it incorrectly falls back to a network request. A code suggestion to fix this is provided. Overall, this is an excellent contribution.

Comment on lines +194 to +216
if (options?.fetchPolicy === QueryFetchPolicy.SERVER_ONLY) {
// definitely execute query
// queryResult = await this.executeQuery(queryRef, options);
shouldExecute = true;
} else {
if (cachingEnabled) {
if (
!(await this.cache?.containsResultTree(key)) ||
(await this.cache?.getResultTree(key))!.isStale()
) {
shouldExecute = true;
} else {
queryResult = await this.getFromResultTreeCache(key, queryRef);
}
} else {
// read from subscriber cache.
const fromSubscriberCache = await this.getFromSubscriberCache(key);
if (!fromSubscriberCache) {
shouldExecute = true;
}
queryResult = fromSubscriberCache as QueryResult<Data, Variables>;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of fetch policies doesn't correctly handle CACHE_ONLY. If an item is not in the cache or is stale, it sets shouldExecute = true and proceeds to make a network request. This violates the contract of a CACHE_ONLY policy, which should only ever read from the cache and never trigger a network fetch.

I suggest explicitly handling the CACHE_ONLY policy to prevent network requests and to throw an error if the requested data is not found in the cache. This will make the behavior of the fetch policies predictable and correct.

    if (options?.fetchPolicy === QueryFetchPolicy.SERVER_ONLY) {
      shouldExecute = true;
    } else if (options?.fetchPolicy === QueryFetchPolicy.CACHE_ONLY) {
      if (cachingEnabled) {
        if (await this.cache!.containsResultTree(key)) {
          queryResult = await this.getFromResultTreeCache(key, queryRef);
        }
      } else {
        const fromSubscriberCache = await this.getFromSubscriberCache(key);
        if (fromSubscriberCache) {
          queryResult = fromSubscriberCache as QueryResult<Data, Variables>;
        }
      }
      if (!queryResult) {
        throw new DataConnectError(
          Code.OTHER, // Consider a more specific error code like 'cache-miss'.
          'Query result not found in cache for CACHE_ONLY policy.'
        );
      }
    } else {
      // Default behavior for PREFER_CACHE or undefined
      if (cachingEnabled) {
        if (
          !(await this.cache?.containsResultTree(key)) ||
          (await this.cache?.getResultTree(key))!.isStale()
        ) {
          shouldExecute = true;
        } else {
          queryResult = await this.getFromResultTreeCache(key, queryRef);
        }
      } else {
        // read from subscriber cache.
        const fromSubscriberCache = await this.getFromSubscriberCache(key);
        if (!fromSubscriberCache) {
          shouldExecute = true;
        }
        queryResult = fromSubscriberCache as QueryResult<Data, Variables>;
      }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.