n1ru4l/graphql-live-query

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