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

figure out how to properly track paginated GraphQL connections #402

Open
n1ru4l opened this issue Jan 27, 2021 · 2 comments
Open

figure out how to properly track paginated GraphQL connections #402

n1ru4l opened this issue Jan 27, 2021 · 2 comments
Labels
discussion enhancement New feature or request help wanted Extra attention is needed

Comments

@n1ru4l
Copy link
Owner

n1ru4l commented Jan 27, 2021

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
          }
        }
      }
    }
  }
`;
@n1ru4l n1ru4l added enhancement New feature or request help wanted Extra attention is needed labels Jan 27, 2021
@n1ru4l
Copy link
Owner Author

n1ru4l commented Mar 22, 2021

Some thoughts on how I wanna continue shaping graphql-live-query: https://dev.to/n1ru4l/graphql-live-queries-backed-by-the-relay-specification-3mlo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant