I got a query ```type Query { chat(): Chat } typ...
# apollo-kotlin
s
I got a query
Copy code
type Query {
  chat(): Chat
}
type Chat {
  id: ID!
  messages: [ChatMessage!]!
  ...
}
interface ChatMessage {
  id: ID!
  ...
}
Then on a mutation response, the message is also reported back, to get back the result quickly, so that there’s no delay before the next poll we do (we don’t use subscriptions here) and when it appears on the screen.
Copy code
type Mutation {
  chatSendText(input: ChatMessageTextInput!): ChatSendOutcome!
}
type ChatSendOutcome {
  """
  The message to manually patch in the cache at the end of the messages list for immediate user feedback
  """
  message: ChatMessage
}
I was thinking of using the gql cache as the source of truth for the chat messages, so I’ll just be `.watch()`ing the cache to reflect that in the UI, so I wanted to see if there’s a way to get the ChatMessage returned from this mutation to the right place in the cache too. They do have a unique ID, and I will add on the extra.graphql file:
Copy code
extend type Chat @typePolicy(keyFields: "id")
extend type ChatMessage @typePolicy(keyFields: "id")
but as I understood, the cache will need to put this specifically under the
chat
query, and not globally for me to be able to watch for it. Does that mean that if I want this to happen, I will need to simply write this response to the cache under the
chat
query myself? Pretty much as I was doing here before?
The context for this is what we were discussing back here https://kotlinlang.slack.com/archives/C01A6KM1SBZ/p1692705300266539?thread_ts=1692699876.256569&cid=C01A6KM1SBZ But turns out we didn’t want to do pagination on the backend, so we are kinda hacking it now. Also no subscription. Will be able to get the chat messages in an ever increasing fashion (first the 15 first, then 30, then 45 and so on, instead of pages. Which we’ll be polling every X seconds), so I wanted to try and use the gql cache as the place where all these responses are put into, so that it will also serve as the way to filter out all the duplicates by the ID which must be unique.
m
Unfortunately there is no "simple" solution to this problem as of now. If the mutation modifies a list without returning the list, you have to update the list manually.
s
Well, I sure am glad I can copy over my old code again then from here 😅
m
Compared to your linked solution, you can also use higher level primitives than
loadRecords
/`mergeRecords` . Something like so (wildly untested so don't take it as gospel but should give the general idea)
Copy code
val mutationResponse = // execute your mutation
val data = apolloClient.apolloStore.readOperation(ChatQuery(), customScalarAdapters)

val modifiedData = data.copy(data.chat.copy(messages = data.chat.messages + mutationResponse.data.chatSendText.message)
apolloClient.apolloStore.writeOperation(ChatQuery(), modifiedData, customScalarAdapters, CacheHeaders.NONE, true)
s
Oh that would be lovely if it just works like this too. I will absolutely give it a shot when I reach the point that I am testing this. Right now I am still in the “planning things out” phase as we finally have time to work on this thing 😄 I will let you know how it goes, thanks a lot 🤗
👍 1
Hey, this does seem to work actually!!
Copy code
private suspend fun populateCacheWithNewMessage(message: ChatMessagesQuery.Data.Chat.Message) {
  val data = apolloClient.apolloStore.readOperation(ChatMessagesQuery(null), apolloClient.customScalarAdapters)
  val modifiedData = data.copy(data.chat.copy(messages = data.chat.messages + message))
  apolloClient.apolloStore.writeOperation(ChatMessagesQuery(null), modifiedData, apolloClient.customScalarAdapters, CacheHeaders.NONE, true)
}
With the only “tricky” part being that I had to do the mapping from the type generated for the mutation to the one generated for the query response, so I am calling it like
Copy code
populateCacheWithNewMessage(
  ChatMessagesQuery.Data.Chat.ChatMessageTextMessage(
    __typename = "ChatMessageText",
    id = chatMessage.id,
    sender = when (chatMessage.sender) {
      ChatMessage.Sender.MEMBER -> ChatMessageSender.MEMBER
      ChatMessage.Sender.HEDVIG -> ChatMessageSender.HEDVIG
    },
    sentAt = chatMessage.sentAt,
    text = (chatMessage as ChatMessage.ChatMessageText).text,
  ),
)
The only part which I do not love is that I have to write the
__typename
myself, which I for now got by looking at what it was already from the okhttp logs, but this would break if that changed in any way, right? Should I be worried about that? Is there some way to get the proper
__typename
in a type-safe way instead? But so cool that I can just do this. Now only to figure out how to make sure that my “message is pending” and “message from cache” do not show on the UI at the same time. Oh boy I am pretty sure writing a chat is potentially an bottomless pit of things to take care of if I am not careful to gloss over some details 😄
m
👍 Regarding mapping from mutation type to query type, maybe fragments could help? Regarding
__typename
, I agree this is not ideal. We could make it a default value for the constructor but that doesn't work for interfaces so it becomes somewhat inconsistent. You should be able to use
schema.packagename.type.ChatMessageText.type
to get the value in a more typesafe way.
s
I thought Fragments would help indeed, but the data I get back from doing the readOperation on the query is of the specific type, and not of the fragment’s interface. So when I want to + the two lists together I think I am forced to have them be of the specific type. For the
type
, yeah I think I can do
__typename = octopus.type.ChatMessageText.type.name,
here and it looks correct, thanks 😊
👍 1