Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to remove entries from cache in ApolloStore by Operation/Fragment #77

Open
Tracked by #2331
themlkang42 opened this issue Apr 27, 2021 · 7 comments

Comments

@themlkang42
Copy link

themlkang42 commented Apr 27, 2021

Is your feature request related to a problem? Please describe.
I think it's pretty common to want to invalidate some parts of the cache by removing entries. ApolloStore has some functionally to remove entries in the cache by CacheKey, but it's hard to remove from the cache by other means, mainly by an Operation or Fragment.

Describe the solution you'd like
It would be nice to have some functions in ApolloStore to remove entries in the cache by the Operation or Fragment similar to how the read() and write() methods can be called with an Operation/Fragment.

Maybe something like:

fun <D : Operation.Data, T, V : Operation.Variables> remove(
      operation: Operation<D, T, V>
  ): ApolloStoreOperation<Boolean>
@martinbonnin
Copy link
Contributor

martinbonnin commented Apr 28, 2021

Makes sense.

I can think of 2 possible ways to do that:

  1. Cascade remove the root key and all CacheReference referenced from there
  2. Remove all the keys of a given Operation.Data

The first one is easier as it doesn't require having the data but it might over-delete.

For an exemple:

query GetBook {
  catalog {
    book(id: "1") {
      title
    }
  }
}
  1. will also remove book(id: "2") if it's in the cache:
store.remove(GetBookQuery())
  1. will not remove book(id: "2") but requires to pass the data:
store.remove(GetBookQuery(), GetBookQuery.Data(catalog = GetBookQuery.Catalog(book = GetBookQuery.Book(title = ""))

@themlkang42
Copy link
Author

@martinbonnin Sorry, responding a bit late. Been looking at iOS Apollo client caching too.
My thought is have an interface similar to the existing one:

fun remove(cacheKey: CacheKey, cascade: Boolean): ApolloStoreOperation<Boolean>
fun <D : Operation.Data, T, V : Operation.Variables> remove(operation: Operation<D, T, V>, cascade: Boolean): ApolloStoreOperation<Boolean>
fun remove(fragment: GraphqlFragment, cascade: Boolean): ApolloStoreOperation<Boolean>

So remove the root key and, if cascade=true, remove all references

Another useful function could be to get the CacheKey from an Operation or Fragment, since I had some pretty complicated logic to calculate the CacheKey inside the CacheKeyResolver, and then was using that to call remove()

fun <D : Operation.Data, T, V : Operation.Variables> getCacheKey(operation: Operation<D, T, V>): CacheKey
fun getCacheKey(fragment: GraphqlFragment): CacheKey

@martinbonnin
Copy link
Contributor

martinbonnin commented Jun 2, 2021

Hi 👋 sorry for the delay!

Another useful function could be to get the CacheKey from an Operation or Fragment, since I had some pretty complicated logic to calculate the CacheKey inside the CacheKeyResolver, and then was using that to call remove()

That's the thing. Operations do not have a cache key per-se since the cache key depends of the data returned. If the root field of an operation is constant then we can get it but if it's not, like seems to be your case, then it's not working anymore.

i.e.:

# The following query will always have "catalog" as root key
query GetCatalog {
  catalog {
    book(id: "1") {
      title
    }
  }
}
# The root key depend on a variable here (`book($id)`)
query GetBook($id: String!) {
  book(id: $id) {
    title
  }
}

The root key can even be completely unknown at the time we send the query:

# The id of the featured book is returned by the backend and might change at anytime
query GetFeaturedBook{
  featuredBook {
    id
    title
  }
}

I had some pretty complicated logic to calculate the CacheKey inside the CacheKeyResolver, and then was using that to call remove()

Do you mind sharing your CacheKeyResolver so that we can investigate how to solve this in your case?

@martinbonnin
Copy link
Contributor

@Leevida we could add something like this:

// Note how data is required in the general case
fun <D : Operation.Data> getCacheKey(operation: Operation<D>, data: D): CacheKey
fun <D : Fragment.Data> getCacheKey(fragment: Fragment<D>, data: D): CacheKey

Would that help?

The data parameter up there I guess will be cumbersome as it's not easy to construct such data programmatically. We could potentially make data optional but I'm afraid it's going to be more of a footgun than anything else since providing null in data would potentially return the wrong key.

Something else we could do is

fun <D : Operation.Data> ApolloResponse<D>.keys(): Set<CacheKey>

This way you could "remember" a response and delete it when needed. Any thoughts?

@martinbonnin
Copy link
Contributor

@Leevida any news?

@themlkang42
Copy link
Author

Here's the cache key resolver. It's just that we had some logic to switch between using contentUrn and urn as the cache key so calculating the cache key for a Fragment was slightly more complicated to do outside this function...:

val resolver = object : CacheKeyResolver() {
            override fun fromFieldRecordSet(field: ResponseField, recordSet: Map<String, Any>): CacheKey {
                if (recordSet["__typename"] == "ContentContainer" && recordSet["contentUrn"] != null) {
                    return formatCacheKey(recordSet["contentUrn"] as? String)
                }
                return formatCacheKey(recordSet["urn"] as String?)
            }

            override fun fromFieldArguments(field: ResponseField, variables: Operation.Variables): CacheKey {
                return formatCacheKey(field.resolveArgument("urn", variables) as String?)
            }

            private fun formatCacheKey(urn: String?): CacheKey {
                return if (urn == null || urn.isEmpty()) {
                    CacheKey.NO_KEY
                } else {
                    CacheKey.from(urn)
                }
            }
        }

I thought apollo stores the query inside the cache and uses the query along with its parameters as the cache key (https://www.apollographql.com/blog/apollo-client/caching/demystifying-cache-normalization/#storing-the-objects-in-a-flattened-data-structure). And, if I'm not wrong, the Operation represents the query.
So I would think data: D wouldn't be necessary, especially for the Fragment case since the Fragment already has the data. I'd also think there would be some similarities with how read( operation: Operation<D, T, V> ): ApolloStoreOperation<T> works since it takes in just the Operation and has to find it in the cache somehow, presumably by calculating the cache key.

@martinbonnin
Copy link
Contributor

Thanks for sending this!

I thought apollo stores the query inside the cache and uses the query along with its parameters as the cache key

Not really. The normalized cache stores "Records", which are flattened objects identified by a "CacheKey". For an example, assuming you have a query like this (most likely not the exact same query that you have but it should be enough as an example):

query GetContentContainer {
  contentContainer {
    contentUrn
    title
    lastModified
  }
}

That returns the following json:

"data": {
  "contentContainer': {
    "contentUrn": "https://example.com/content001",
    "title": "Demo Content",
    "last modified": "July, 29th 2021"
  }
)

If we need to remove this query from the cache, we need to delete the object with CacheKey = "https://example.com/content001" which is part of the Json response, not the query or variables.

I'd also think there would be some similarities with how read( operation: Operation<D, T, V> ): ApolloStoreOperation<T>

Fair point. We can get the data from store.read(query) so we could definitely use that to make a all-in-one store.remove(query). I'll try to think of something, most likely in the dev-3.x branch.

@BoD BoD transferred this issue from apollographql/apollo-kotlin Dec 13, 2024
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

No branches or pull requests

2 participants