Can a paged query's response take both items from the cache a new items from the network when forming a response?

I’m attempting to use paging in a query such that I request first 5 items (starting with item 0) and then 10 items (also starting from item 0.)

I originally expected that Apollo would first fetch the first 5 items over the network but for the second query (requesting the first 10) it would manage to pull the first 5 items from cache and fulfill the remaining with a network request for items 5-9

What I expected:
query 1 - snowRegionPagedConnection(5) → Network { Regions[0-4] }
query 2 - snowRegionPagedConnection(10) → Cache{ Regions[0-4] } ] + Network { Regions[5-9] }

Actual:
query 1 - snowRegionPagedConnection(5) → Network { Regions[0-4] }
query 2 - snowRegionPagedConnection(10) → Network { Regions[0-9] }

Can a paged query work this way at all, or are the page result bound in the cache to the query paramters?

After snowRegionPagedConnection(5) the cache has indexed entries corresponding to each item:

  "snowRegionsConnection({"after":null,"filter":null,"limit":5}).edges.0" : {
    "cursor" : ChEKDw0wQmVydGhvdWQgUGFzcwoDEKMC
    "node" : CacheKey(SnowRegion:15x36wk86ykm)
  }

After snowRegionPagedConnection(10) the cache has new entries for those same items:

  "snowRegionsConnection({"after":null,"filter":null,"limit":10}).edges.0" : {
    "cursor" : ChEKDw0wQmVydGhvdWQgUGFzcwoDEKMC
    "node" : CacheKey(SnowRegion:15x36wk86ykm)
  }

Same items, but keyed to different query parameters.

Additional info about my setup if it matters:

query snowRegionPagedConnection($filter: SnowRegionFilter, $limit: Int, $after: String) {
  snowRegionsConnection(filter: $filter, limit: $limit, after: $after) {
    edges {
      cursor
      node {
        ...snowRegionModel
      }
    }
    pageInfo {
      hasPreviousPage
      hasNextPage
      startCursor
      endCursor
    }
  }
}
fragment snowRegionModel on SnowRegion{
    id
}

extend type SnowRegion @typePolicy(keyFields: "id")

There’s nothing like this at the moment and in general I would say this is not something we are aiming at because it complicates the pipeline quite a lot. For an example, a response has cache and/or network information attached. Composing from both sources would make that information more complicated to expose.

What we are working on is better scrolling support, i.e. the ability to query successive, non-overlapping pages and store them under a single cache key for later retrieval. You can see some example of that in that test.

Can you share a bit more about your use case? Any chance you can know at the call site that [0-4] are already cached and therefore query [5-9] instead of [0-9]?

Thanks Martin!

I can perform those extra steps to manage assembling the cached + network response items in the repo instead. I wanted to avoid extra hand-management of the items collection if Apollo already provided the functionality.

I’ll pull the repo and run the tests you point out to explore how you accomplishing it. Cloning the repo on Windows produces some path too long errors from git - I’ll pull it on Ubuntu and take a look.

Re: my use case. My example query above is just dealing with paged items and accumulating them over several paged queries. In reality, I want a query to provided additional data to render my overview region screen + all accumulated snowReionModel data as a single query where a follow-on paged queries would provide the same overview data + cached regions + newly fetched regions. The goal is just to keep this paged query and response as simple as possible and very close to what the ui will use to render the entire screen with the least amount of stitching and transforming.

Pardon my pseudo code:

Page size 5

Query1
snowRegionPagedConnection(5)

{
 id: "abc123"
 name: "Snow area collection XXX"
 location: X
 geometry: X
 regions[5]: {region0, ... region4 }
}

Query2
snowRegionPagedConnection(10)

{
 id: "abc123"
 name: "Snow area collection XXX"
 location: X
 geometry: X
 regions[10]: {region0, ... region9 } <- 0-4 came from cache, 5-9 came from the network
}
//initial fetch to show screen
regionOverviewState.update { snowRegionPagedConnection(5)  } 
//user scrolled to end of list
regionOverviewState.update { snowRegionPagedConnection(10)  }

I took a shot at creating this functionality in my repo. For my case, forward accumulative loading is all that’s required. Each time a new page is needed, all entries for all pages including the new page are requested, allowing Apollo to determine if a page of entries reside in cache or if a network fetch is required.

I think this solves my not wanting to stitch together and collect all of the loaded paged entries elsewhere. What’s returned from this function is the entire ui state for that section of ui.

Also, if a previously cached page was evicted for some reason, I don’t need to explicitly request the data from the network.

    /**
     * Load the next page of regions. Given the current number of loaded regions, this function will calculate
     * the next page to load. During this operation, the new and all previously loaded pages are re-requested. If
     * a page of results was previously fetched, the cache will provide that page of results. If the request includes
     * a new page, those results are requested over the network.
     * Example:
     * --
     * Request 1 fetch page up to index 0: snowRegionPagedConnection(0) -> Network page 0
     * --
     * Request 2 fetch page up to index 1: snowRegionPagedConnection(0) -> Cache page 0
     *                                     snowRegionPagedConnection(1) -> Network page 1
     * --
     * Request 3 fetch page up to index 2: snowRegionPagedConnection(0) -> Cache page 0
     *                                     snowRegionPagedConnection(1) -> Cache page 1
     *                                     snowRegionPagedConnection(2) -> Network page 2
     *
     * @param currentlyLoadedItemCount the number of regions currently loaded into the state
     * @return all of the snow region previews that will be rendered
     */
    override suspend fun snowRegionPagedConnection(currentlyLoadedItemCount: Int): List<SnowRegionPreviewModel>? =
        try {
            val currentlyLoadedPageCount = currentlyLoadedItemCount / REGION_ITEM_PER_PAGE
            var query = snowRegionPagedQuery

            (0..currentlyLoadedPageCount + 1 ).mapNotNull {
                debug{ Timber.i("GuideBookNav: Loading more regions page index: $it") }
                graphQLClient.snowRegionPagedConnection(query)?.let { response ->
                    response.edges.mapNotNull { it.node?.snowRegionPreviewModel }.let {
                        query = snowRegionPagedQuery.copy(after = response.edges.lastOrNull()?.cursor.asOptional())
                        it
                    }
                }
            }.flatten().apply {
                debug{ Timber.i("GuideBookNav: Total loaded region count ${count()}") }
            }
        } catch (e: Exception) {
            e.rethrowIfCancelled()
            null
        }

    private val snowRegionPagedQuery = SnowRegionPagedConnectionQuery(
        after = Optional.absent(),
        limit = REGION_ITEM_PER_PAGE.asOptional(),
    )

Reply

1 Like