figure out how to properly track paginated GraphQL connections
n1ru4l opened this issue · 2 comments
Tracking updates for potentially huge paginated GraphQL connections is hard and can not be done with the implementation inside this repository.
The current way of doing this is a subscription (with super complicated pub-sub handlers), with manual cache update handlers on the frontend.
For a project, I came up with the following subscription solution for a connection of notes.
Ideally, we would want to avoid a lot of this boilerplate and make connection updates live without much hassle. I thought of having a specific directive especially for connection such as liveConnection
, but did not work out the details yet. The idea is that it behaves a bit differently than the live
directive and in case more items are fetched the servercan then based on that directive check which items on the client would be affected.
type Query {
notes(first: Int, after: String, filter: NotesFilter): NoteConnection!
}
type NoteConnection {
edges: [NoteEdge!]!
pageInfo: PageInfo!
}
type NoteEdge {
cursor: String!
node: Note!
}
type Note implements Node {
id: ID!
documentId: ID!
title: String!
content: String!
contentPreview: String!
createdAt: Int!
viewerCanEdit: Boolean!
viewerCanShare: Boolean!
access: String!
isEntryPoint: Boolean!
updatedAt: Int!
}
type NotesUpdates {
"""
A node that was added to the connection.
"""
addedNode: NotesConnectionEdgeInsertionUpdate
"""
A note that was updated.
"""
updatedNote: Note
"""
A note that was removed.
"""
removedNoteId: ID
}
type NotesConnectionEdgeInsertionUpdate {
"""
The cursor of the item before which the node should be inserted.
"""
previousCursor: String
"""
The edge that should be inserted.
"""
edge: NoteEdge
}
type Subscription {
notesUpdates(
filter: NotesFilter
endCursor: String!
hasNextPage: Boolean!
): NotesUpdates!
}
The implementation on the frontend then could look similar to this (Full code can be found here):
const subscription =
requestSubscription <
tokenInfoSideBar_NotesUpdatesSubscription >
(environment,
{
subscription: TokenInfoSideBar_NotesUpdatesSubscription,
variables: {
filter: props.showAll ? "All" : "Entrypoint",
endCursor: data.notes.pageInfo.endCursor,
hasNextPage: data.notes.pageInfo.hasNextPage,
},
updater: (store, payload) => {
console.log(JSON.stringify(payload, null, 2));
if (payload.notesUpdates.removedNoteId) {
const connection = store.get(data.notes.__id);
if (connection) {
ConnectionHandler.deleteNode(
connection,
payload.notesUpdates.removedNoteId
);
}
}
if (payload.notesUpdates.addedNode) {
const connection = store.get(data.notes.__id);
if (connection) {
const edge = store
.getRootField("notesUpdates")
?.getLinkedRecord("addedNode")
?.getLinkedRecord("edge");
// we need to copy the fields at the other Subscription.notesUpdates.addedNode.edge field
// will be mutated when the next subscription result is arriving
const record = store.create(
// prettier-ignore
`${data.notes.__id}-${edge.getValue("cursor")}-${++newEdgeIdCounter.current}`,
"NoteEdge"
);
record.copyFieldsFrom(edge);
if (payload.notesUpdates.addedNode.previousCursor) {
ConnectionHandler.insertEdgeBefore(
connection,
record,
payload.notesUpdates.addedNode.previousCursor
);
} else if (
// in case we don't have a previous cursor and there is no nextPage the edge must be added the last list item.
connection?.getLinkedRecord("pageInfo")?.getValue("hasNextPage") ===
false
) {
ConnectionHandler.insertEdgeAfter(connection, record);
}
}
}
},
});
const TokenInfoSideBar_NotesUpdatesSubscription = graphql`
subscription tokenInfoSideBar_NotesUpdatesSubscription(
$filter: NotesFilter!
$endCursor: String!
$hasNextPage: Boolean!
) {
notesUpdates(
filter: $filter
endCursor: $endCursor
hasNextPage: $hasNextPage
) {
removedNoteId
updatedNote {
id
title
isEntryPoint
}
addedNode {
previousCursor
edge {
cursor
node {
id
documentId
title
}
}
}
}
}
`;
Some thoughts on how I wanna continue shaping graphql-live-query: https://dev.to/n1ru4l/graphql-live-queries-backed-by-the-relay-specification-3mlo