https://kotlinlang.org logo
#apollo-kotlin
Title
# apollo-kotlin
s

S.

12/10/2023, 10:57 PM
I'm getting a lot of cache misses on a union type
Object 'DeckOrError:*UUID*' not found
. I have a typePolicy on
Deck
with it's id and I can see that those are being stored in the cache, however it's fields are never getting retrieved from the cache. I also set up a fieldPolicy
extend type Deck @fieldPolicy(forField: "cards", keyArgs: "afterId limit sortBy")
but same story - it's being cached but never retrieved and resulting in the above not-found message. It only works for the top-level getUser -> userDecks query, which says
Object 'QUERY_ROOT' has no field named 'getUser'
only the first time, subsequent calls are fetched from the cache. What can be done, or what am I missing to make the cache work properly? it's not even possible to apply extensions on the union type itself
m

mbonnin

12/11/2023, 10:08 AM
It's a bit hard to tell without more details. I'm guessing you're trying to avoid a round trip on stuff like this:
Copy code
query GetDeck {
  deck(id: "42") {
    id
  }
}
? For these kind of things, you'll need to make a field policy for deck:
Copy code
extend type Query @fieldPolicy(forField: "deck", keyArgs: "id")
If that doesn't work and you can upload a small reproducer, it's the best way to investigate those
s

S.

12/11/2023, 8:04 PM
I already have a field policy for this in place but yes I want to avoid such round trips
m

mbonnin

12/11/2023, 8:20 PM
Can you share your
schema.graphqls
and
extra.graphqls
?
s

S.

12/11/2023, 8:29 PM
Copy code
union UserOrError = User | Error
# same for the others 

type Card {
  id: UUID!
}

type CardList {
  hasNext: Boolean!
  list: [Card!]!
}

type Deck {
  cards(afterId: UUID, limit: Int, sortBy: CardSortingInput): CardListOrError!
  id: UUID!
}

type DeckList {
  hasNext: Boolean!
  list: [Deck!]!
}

type Query {
  getDeck(id: UUID!): DeckOrError!
  getUser: UserOrError!
}

type User {
  id: UUID!
  userDecks(): DeckListOrError!
}

extend type User @typePolicy(keyFields: "id")
extend type Card @typePolicy(keyFields: "id")
extend type Deck @typePolicy(keyFields: "id")

extend type Query @fieldPolicy(forField: "getCard", keyArgs: "id")
extend type Query @fieldPolicy(forField: "getDeck", keyArgs: "id")
extend type Deck @fieldPolicy(forField: "cards", keyArgs: "afterId limit sortBy")
thank you color 1
I tried to remove all the irrelevant pieces. idk if the queries are also of interest?
m

mbonnin

12/11/2023, 8:33 PM
More context is always useful
s

S.

12/11/2023, 8:35 PM
this is what i can see in the cache dump, so everything does get stored properly but it still says
Copy code
Object 'DeckOrError:b090cd01-1107-4b19-8d03-de513591e4fb' not found
Object 'DeckOrError:1269fecc-adf1-4e12-84bb-466d3d3dfa97' not found
every time I try to query it again
queries roughly look like this
Copy code
query UserDecks() {
    getUser {
        ... on User {
            userDecks() {
                ... on DeckList {
                    list {
                        id
                    }
                    hasNext
                }
                ... on NotFound {
                    msg
                }
            }
        }
        ... on Error {
            msg
        }
    }
}

query Deck($id: UUID!) {
    getDeck(id: $id) {
        ... on Error {
            msg
        }
        ... on Deck {
            id
            cards {
                ...CardItem
            }
        }
    }
}

query DeckCards($id: UUID!, $afterId: UUID, $limit: Int, $sortBy: CardSortingInput) {
    getDeck(id: $id) {
        ... on Error {
            msg
        }
        ... on Deck {
            id
            cards(afterId: $afterId, limit: $limit, sortBy: $sortBy) {
                ...CardItem
            }
        }
    }
}
m

mbonnin

12/11/2023, 8:42 PM
Copy code
Object 'DeckOrError:b090cd01-1107-4b19-8d03-de513591e4fb' not found
I'm guessing
DeckOrError
is an union as well, right? If that's the case, looks like this is the issue, the default FieldPolicyCacheResolver doesn't work with abstract types (interfaces and unions)
s

S.

12/11/2023, 8:42 PM
yep exactly
m

mbonnin

12/11/2023, 8:43 PM
What I would recommend is removing the typename from the CacheKey but for this you require global ids
(also, is this a Hearthstone API by any chance ? blob upside down I used to do a bunch of hearthstone back in the day)
❤️ 1
s

S.

12/11/2023, 8:46 PM
passionate hearthstone player 😄 (battlegrounds though) but no, it refers to something like flash cards
💙 1
🤝 1
I do use UUIDs everywhere for ids, or what is meant with global ids?
m

mbonnin

12/11/2023, 8:51 PM
UUIDs are good. As long as a Deck and a User id cannot collide, you're good to go
You can remove the
__typename
from the cache key:
Copy code
object FieldPolicyCacheResolver : CacheResolver {
  override fun resolveField(
      field: CompiledField,
      variables: Executable.Variables,
      parent: Map<String, @JvmSuppressWildcards Any?>,
      parentId: String,
  ): Any? {
    val keyArgsValues = field.argumentValues(variables) {it.isKey }.values.map { it.toString() }

    if (keyArgsValues.isNotEmpty()) {
      // Remove the typename here
      return CacheKey(keyArgsValues)
    }

    return DefaultCacheResolver.resolveField(field, variables, parent, parentId)
  }
}
And similarly for the
TypePolicyCacheKeyGenerator
:
Copy code
object TypePolicyCacheKeyGenerator : CacheKeyGenerator {
  override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {
    val keyFields = context.field.type.rawType().keyFields()

    return if (keyFields.isNotEmpty()) {
      CacheKey(keyFields.map { obj[it].toString() }.joinToString(""))
    } else {
      null
    }
  }
}
s

S.

12/11/2023, 8:55 PM
I see, will give it a try, ty
m

mbonnin

12/11/2023, 8:55 PM
Sure thing
Also you should probably remove that part:
Copy code
extend type Deck @fieldPolicy(forField: "cards", keyArgs: "afterId limit sortBy")
From a Deck, there's no way to avoid the roundtrip for cards if you did not load the deck before
s

S.

12/11/2023, 8:59 PM
this part is meant for paging, so the Deck should already be loaded at this point. but I will investigate this again after configuring the proper cache keys now
👍 1
General cache question: if I have a query fetching Deck.title and another one that fetches Deck.progress, obviously it requires two network calls but do subsequent queries on Deck automatically update the cache and add the newly fetched values? or do I have to do this manually
m

mbonnin

12/12/2023, 2:05 PM
add the newly fetched values?
In the cache yes, in your responses, "it depends the query"
This works if the
Deck
has an id of course
s

S.

12/12/2023, 2:15 PM
I see. because it doesn't do this in my case. I certainly still have to adjust some things
back on this. I still have the issue that the cache is getting updated for
QUERY_ROOT.getUser
if I'm fetching new fields but not for any other objects
Copy code
CACHE MISS: Object '6b0bc4f1-4856-4eee-ac91-05f06721f0f4' has no field named 'public'
CACHE MISS: Object '6b0bc4f1-4856-4eee-ac91-05f06721f0f4' has no field named 'public'
CACHE MISS: Object '6b0bc4f1-4856-4eee-ac91-05f06721f0f4' has no field named 'public'
while
CACHE MISS: Object 'getUser' has no field named 'userDecks()'
does only appear once and is then retrieved from the cache successfully
this is what the cache dump looks like but it proceeds to fetch from network
I seem to have found the culprit.
Copy code
extend type Query @fieldPolicy(forField: "getDeck", keyArgs: "id")
however, not sure why
one persisting problem is that mutations do not update the cached objects
Copy code
query Deck($id: UUID!, $limit: Int) {
    getDeck(id: $id) {
        ... on Error {
            msg
        }
        ... on Deck {
            id
            title
            public
            cards(limit: $limit) {
                ...CardItem
            }

mutation updateDeck($deckId: UUID!, $title: String, $public: Boolean) {
    updateDeck(id: $deckId, title: $title, public: $public) {
        ... on Error {
            msg
        }
        ... on Deck {
            id
            title
            public
        }
    }
}
m

mbonnin

02/21/2024, 10:16 AM
What does the above mutation writes to the cache? If you're doing only this mutation, can you dump the cache after it.
s

S.

02/21/2024, 10:23 AM
image.png
this is before the mutation:
also the queries never update the keys with the plain uuid. 'd20..' is coming from
getUser.userDecks
m

mbonnin

02/21/2024, 10:29 AM
this is in the debugger, right?
s

S.

02/21/2024, 10:30 AM
yes, I just ran cache.dump() in the evaluate expression field
👍 1
m

mbonnin

02/21/2024, 10:31 AM
I'm trying to wrap my head around why there is a
"getDeck(id: ...)"
key
Means it's using the path there for some reason instead of an id
Meaning
cacheKeyForObject()
returned
null
Guessing
Mutation.getDeck
is an union, right?
s

S.

02/21/2024, 10:34 AM
in the
TypePolicyCacheKeyGenerator
? I have it like above in the thread
m

mbonnin

02/21/2024, 10:35 AM
Yea exactly
s

S.

02/21/2024, 10:35 AM
getDeck(id: UUID!): DeckOrFetchingError!
Copy code
union DeckOrFetchingError = Deck | NotFound | Unauthorized
those are all union types, yes
updateDeck(id: UUID!, public: Boolean, title: String): DeckOrFetchingError!
m

mbonnin

02/21/2024, 10:36 AM
So looks like this branch is not taken 😞
We'd need something like
Copy code
extend type DeckOrFetchingError @typePolicy(keyFields: "id")
Which is weird because unions do not have fields...
Short term fix is to programmatically set a
CacheKeyGenerator
to handle that case. I need to finish something but will send sample code a bit later
Longer term is to find a better solution to that problem but that's a hard one
s

S.

02/21/2024, 10:39 AM
the union is not an object so setting a policy doesn't compile
m

mbonnin

02/21/2024, 10:39 AM
yea
s

S.

02/21/2024, 10:40 AM
I see. I really like the error handling with union types but it sure makes some problems. Anyway I really appreciate your help
m

mbonnin

02/21/2024, 10:42 AM
Sure thing! This is hard stuff TBH and I hoped the declarative cache would make things easier but cases like this are hard
Maybe we need to enforce some characteristics of the schema to opt in declarative caching. This would allow making some assumption and make the whole thing more robust
In the short term, You might be better off switching to programmatic cache ids completely:
Copy code
object MyCacheKeyGenerator: CacheKeyGenerator {
  override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {
    if (obj["__typename"] == "Deck") {
      return CacheKey(obj["__typename"].toString(), obj["id"].toString())
    }
    // Other types here
    
    return null
  }
}
s

S.

02/21/2024, 10:50 AM
I see. I'm using v4 btw if that makes any difference
m

mbonnin

02/21/2024, 10:51 AM
Should be the same for this
Something else you might bump into with caching and union result types is if you get an error, it'll be saved to the cache
I think we have an issue about this, let me check
I've also created https://github.com/apollographql/apollo-kotlin/issues/5635 for this specific union issue
👍🏻 1
s

S.

02/21/2024, 12:30 PM
little update: everything gets appended to a single key now but still not updated by mutations. I don't see any changes in the cache dump at all after running a mutation.
m

mbonnin

02/21/2024, 12:42 PM
So after the mutation, you still have cache keys that start with ``getDeck(id: ...)``?
s

S.

02/21/2024, 12:43 PM
no, only
Deck:UUID
m

mbonnin

02/21/2024, 12:43 PM
👍 so this is better
And the watcher isn't updated?
s

S.

02/21/2024, 12:44 PM
yep, see the screenshots. it's all in the same object now
👍 1
m

mbonnin

02/21/2024, 12:44 PM
What changed in the Deck? (is it a name, tags, something else?)
From your mutation, looks like title could change?
s

S.

02/21/2024, 12:49 PM
well, it seems to work now. maybe I got into a weird state. I will apply this to other objects as well and see if any problems persist
m

mbonnin

02/21/2024, 12:50 PM
Sounds good. Let me know how that works!
2 Views