Giter Club home page Giter Club logo

graphql-live-query's Introduction

GraphQL Live Query

Real-Time with any schema or transport.

Why Live Queries? - Read the introduction Post - Learn how Live Query Tracking works



Packages in this Repository

Package Description Stats
@n1ru4l/in-memory-live-query-store Live query implementation. npm version npm downloads
@n1ru4l/graphql-live-query Utilities for live query implementations. npm version npm downloads
@n1ru4l/graphql-live-query-patch-json-patch Reduce live query payload size with JSON patches npm version npm downloads
@n1ru4l/graphql-live-query-patch-jsondiffpatch Reduce live query payload size with @n1ru4l/json-patch-plus npm version npm downloads
@n1ru4l/socket-io-graphql-server GraphQL over Socket.io - Server Middleware npm version npm downloads
@n1ru4l/socket-io-graphql-client GraphQL over Socket.io - Client npm version npm downloads
todo-example-app Todo App with state sync across clients. -

Motivation

There is no mature live query implementation that is not tied to any specific database or SaaS product. This implementation should serve as an example for showcasing how live queries can be added to any GraphQL.js schema with (almost) any GraphQL transport.

GraphQL already has a solution for real-time: Subscriptions. Those are the right tool for responding to events. E.g. triggering a sound or showing a toast message because someone poked you on Facebook. Subscriptions are also often used for updating existing query results on a client that consumes data from the same GraphQL API. Depending on the complexity of that data, cache update code for applying the subscription result can eventually become pretty bloated (!!! especially, for adding and removing list items). Often for developers it is more straight-forward to simply refetch the query once a subscription event is received instead of doing cache voodoo magic.

In contrast to manual cache updates using subscriptions, live queries should feel magical and update the UI with the latest data from the server without having to write any cache update wizardry code on the client.

Concept

A live query is a query operation that is annotated with a @live directive.

query users @live {
  users(first: 10) {
    id
    login
  }
}

A live query is sent to the server (via a transport that supports delivering partial execution results) and registered. The client receives a immediate execution result and furthermore receives additional (partial) execution results once the live query operation was invalidated and therefore the client data became stale.

The client can inform the server that it is no longer interested in the query (unsubscribe the live query operation).

On the server we have a live query invalidation mechanism that is used for determining which queries have become stale, and thus need to be rescheduled for execution.

Instead of sending the whole execution result, the server can diff the previous and the current execution result and only send a delta instruction to the client instead of the whole operation, resulting in less network overhead!

In a future implementation the server might only need to re-execute partial subtrees of a query operation instead of the whole operation.

How does the server know the underlying data of a query operation has changed?

The reference InMemoryLiveQueryStore implementation uses an ad hoc resource tracker, where an operation subscribes to the specific topics computed out of the query and resolved resources during the latest query execution.

A resource (in terms of the reference implementation) is described by a root query field schema coordinate (such as Query.viewer or Query.users), a root query field with id argument (Query.user(id:"1")) or a resource identifier (such as User:1). The latter is by default composed out of the resource typename and the non nullable id field of the given GraphQL type.

For the following type:

type User {
  id: ID!
  name: String!
}

Legitimate resource identifiers could be User:1, User:2, User:dfsg12. Where the string after the first colon describes the id of the resource.

In case a resource has become stale it can be invalidated using the InMemoryLiveQueryStore.invalidate method, which results in all operations that select a given resource to be scheduled for re-execution.

Practical example:

// somewhere inside a userChangeLogin mutation resolver
user.login = "n1ru4l";
user.save();
liveQueryStore.invalidate([
  // Invalidate all operations whose latest execution result contains the given user
  `User:${user.id}`,
  // Invalidate query operations that select the Query,user field with the id argument
  `Query.user(id:"${user.id}")`,
  // invalidate a list of all users (redundant with previous invalidations)
  `Query.users`,
]);

Those invalidation calls could be done manually in the mutation resolvers or on more global reactive level e.g. as a listener on a database write log. The possibilities are infinite. ๐Ÿค”

For scaling horizontally the independent InMemoryLiveQueryStore instances can be wired together via a PubSub system such as Redis.

How are the updates sent/applied to the client?

The transport layer can be any transport that allows sending partial execution results to the client.

Most GraphQL clients (including GraphiQL, Apollo, Relay and Urql) have support for Observable or async iterable data structures which are perfect for describing both Subscription and Live Queries. Ideally a GraphQL Live Query implementation uses a AsyncIterable or Observable for pushing the latest query data to the client framework that consumes the data.

List of compatible transports/servers

List of known and tested compatible transports/servers. The order is alphabetical. Please feel free to add additional compatible transports to the list!

Package Transport Version Downloads
@n1ru4l/socket-io-graphql-server GraphQL over Socket.io (WebSocket/HTTP Long Polling) npm version npm downloads
graphql-helix GraphQL over HTTP (IncrementalDelivery/SSE) npm version npm downloads
graphql-ws GraphQL over WebSocket (WebSocket) npm version npm downloads

graphql-live-query's People

Contributors

ardatan avatar carlosdp avatar ccollie avatar danielrearden avatar dburles avatar dependabot[bot] avatar github-actions[bot] avatar n1ru4l avatar renovate-bot avatar renovate[bot] avatar saihaj avatar trentwest7190 avatar venryx avatar yayvery avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-live-query's Issues

[InMemoryLiveQueryStore] Invalidate fields with arguments

interface Node {
  id: ID!
}

type Note implements Node {
  id: ID! # graphql conform id
  documentId: ID! # human readable id
  title: String!
  content: String!
  access: NoteAccess
}

enum NoteAccess {
  public
  private
}

type Query {
  node(id: ID!): Node
  note(documentId: ID!): Note
}
query noteDetail($documentId: ID!) @live {
  note(documentId: $documentId) {
    id
    title
    content
  }
}

Variables

{
 "documentId": "my-first-note"
}
Query.note(documentId:"my-first-note")
liveQueryStore.invalidate(`Query.note(documentId:"my-first-note")`)

Let's say the access was updated from private to public. The Query.note field returns null if the viewer is not allowed to see it. But later the access is changed, so the note should be pushed to the client. There is currently no efficient way for invalidating only a field with a specific argument. The solution rn would be to invalidate Query.note which will cause any query (even those that are unaffected) to be scheduled for re-execution. With the argument narrowing more efficient invalidations would be possible.

`iterator.return` running when it's not supposed to.

Hi, I'm currently having a very strange problem. I'm having differing execution results for the same server implementation of the live query with different clients. Specifically, a difference between a production and development version of my client code. On the development version, everything works just fine 100% of the time. However, on my production code, there is a pattern of live query results not being sent back to the client. After throwing in a lot of debugging statements, the only difference between the execution of the two requests that I can find, is that on my production version of the client, this function is being run and it's releasing the identifiers used to alert of a change, even though it should still be watching those identifiers.

iterator.return = () => {
this._resourceTracker.release(record, previousIdentifier);
return (
originalReturn?.() ?? Promise.resolve({ done: true, value: undefined })
);
};

I'm really stumped on this one, since both clients should be sending identical requests, and I could not find anything in this codebase which would change upon setting the environment to production rather than development.

solve relay store not deleting removed nodes

Stale nodes that are no longer displayed a first removed after the query component (that ONCE referenced them) unmounts. Ideally, we want those removed after they are no longer referenced in the query.

This is an performance issue for long-running applications.

Ideas for making LiveQueryStore more powerful

The approach of calling liveQueryStore.triggerUpdate("Query.users") might not scale that well. It could return different results for different users. It does not address the unique field argument combinations.

Resource-Based Updates

If a user with id fgahex changes we ideally only want to update queries that select the given user.

Object types that have an id field could be automatically collected during query execution (and updated during re-execution).

Query

query user @live {
  me {
    __typename
    id
    login
  }
}

Execution result:

{
  "data": {
    "__typename": "User",
    "id": "fgahex",
    "login": "testuser"
  }
}

Will result in the following entry for the selected resources of that query: User.fgahex.
An update for all the live queries that select the given user could be scheduled via liveQueryStore.triggerUpdate("User.fgahex").

Things to figure out:

What is the best way of collecting resources?

This would require sth. like a middleware for each resolver that gathers the ids and attaches them to the result (as extensions?) that the liveQueryStore can pick up (maybe this?).

Another solution would be to push this to the user so he has to register it manually, by calling some function that is passed along the context object?

The information could also be extracted from the query response. However, that would have the implications that it needs at least the id extracted for each record (or even id + __typename for GraphQL servers that do not expose unique ids across object types). Since most client-side frameworks encourage using unique ids for identifying Objects anyways that shouldn't be that big of a limitation. An additional drawback would be that the whole result tree must be traversed.

Pros:

  • The server automatically keeps track of the resources a query selects.
  • Does work for resources that got replaced with objects of another id (by calling triggerUpdate for the previous resource).

Cons:

  • Big queries result in a lot of strings that are stored additionally in memory
  • Does only work for object types that have an id property.
  • Does probably not play that well with a list type.
  • Does not solve the field argument issue.

GraphQL extensions field for overriding the generated resource identifiers

I thought about allowing to provide a buildResourceIdentifier function to the extensions field of a field resolver. That would easily allow adding custom resource identifiers/ events in user-land for invalidating a query.

const GraphQLQueryType = new GraphQLObjectType<Root>({
  name: "Query",
  fields: {
    todos: {
      type: new GraphQLNonNull(
        new GraphQLList(new GraphQLNonNull(GraphQLTodoType))
      ),
      extensions: {
        liveQuery: {
          // return string or string array?
          buildResourceIdentifier: (_root, _args, context) => `UserTodos:${context.viewer.id}`
        }
      },
      resolve: (root, args, context) => Array.from(root.todos.values()),
    },
  },
});

Make InMemoryLiveQueryStore.execute compatible with overloaded arguments

aka

export function execute(
  args: ExecutionArgs,
): PromiseOrValue<
  ExecutionResult | AsyncIterableIterator<AsyncExecutionResult>
>;
export function execute(
  schema: GraphQLSchema,
  document: DocumentNode,
  rootValue?: any,
  contextValue?: any,
  variableValues?: Maybe<{ [key: string]: any }>,
  operationName?: Maybe<string>,
  fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
  typeResolver?: Maybe<GraphQLTypeResolver<any, any>>,
): PromiseOrValue<
  ExecutionResult | AsyncIterableIterator<AsyncExecutionResult>
>;

Figure out how to optimize list changes

When publishing a new value through the live query observable list updates cause the whole list to re-render. Also, the old records seem not to be evicted properly from the relay store.

`createLiveQueryPatchGenerator` shouldn't yield empty patches

Hey @n1ru4l thanks for all your hard work on these libraries!

I've come across an issue where I'm seeing a load of empty patches, e.g.: {"revision":92}, which the client isn't too happy about, https://github.com/n1ru4l/graphql-live-query/blob/main/packages/graphql-live-query-patch/src/createApplyLiveQueryPatch.ts#L42-L44.

The generatePatch function returns undefined in the case where previousValue is the same as currentValue. I believe there should be a check in place to not yield valueToPublish in those cases.

Annotate fields and/or fragments with @live

Can't believe it has been nearly 5 years since this video https://www.youtube.com/watch?t=1090&v=ViXL0YQnioU where Lee Byron presents the idea of the @live directive.

{
  feed {
    stories {
      author { name }
      message
      likeCount @live
    }
  }
}

Really cool to follow your progress on live queries, but I've been wondering why you went with the approach of annotating the entire query vs being able to annotate the individual fields or fragments?

Are you planning, at some point, to support @live per field or fragment, or what is the rationale of annotating the whole query?

Keep up the great work!

TicTacToe Multiplayer Example App

Seeing some posts about subscriptions on Reddit about game development, I wanna explore options for building multiplayer games with live queries ๐Ÿ˜‰

Allow returning AsyncIterators from resolvers ๐Ÿค”

I had this idea that is actually possible to allow returning AsyncIterators from resolvers without waiting for it to land in graphql-js. The only downside would be that typings in user code could be wrong (Promise<T> | T expected instead of AsyncIterator<T>) ๐Ÿ˜…. However, that could be easily solved with patch-package.

The idea is to wrap each field resolver (as in #94). The wrapper will then check whether the returned value is a plain value, a Promise, or an AsyncIterator. In the case of plain value and Promise it will just forward it. In case of an AsyncIterator it will (1) get the first value and resolve a Promise with it and (2) register that the live query returns an AsyncIterator for a given query path and then forwards patches to the client once new data is returned from the AsyncIterator.

figure out how to properly track paginated GraphQL connections

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
          }
        }
      }
    }
  }
`;

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Rate-Limited

These updates are currently rate-limited. Click on a checkbox below to force their creation now.

  • fix(deps): update dependency @graphql-tools/utils to v8.13.1
  • chore(deps): update dependency todomvc-app-css to v2.4.3
  • chore(deps): update babel monorepo (@babel/core, @babel/preset-env, @babel/preset-typescript)
  • chore(deps): update dependency @testing-library/jest-dom to v5.17.0
  • chore(deps): update dependency @types/node to v18.19.31
  • chore(deps): update dependency classnames to v2.5.1 (classnames, @types/classnames)
  • chore(deps): update dependency prettier to v2.8.8
  • chore(deps): update dependency @types/node to v20
  • chore(deps): update dependency globby to v14
  • chore(deps): update dependency husky to v9
  • chore(deps): update dependency lint-staged to v15
  • chore(deps): update dependency patch-package to v8
  • chore(deps): update dependency prettier to v3
  • chore(deps): update dependency vite to v5
  • chore(deps): update dependency ts-node to v10.9.2
  • fix(deps): update dependency @repeaterjs/repeater to v3.0.5
  • fix(deps): update dependency fast-json-patch to v3.1.1
  • fix(deps): update dependency graphql-ws to v5.16.0
  • fix(deps): update dependency ioredis to v5.4.1
  • fix(deps): update dependency puppeteer to v18.2.1
  • fix(deps): update dependency ws to v8.16.0 (ws, @types/ws)
  • fix(deps): update socket.io packages to v4.7.5 (socket.io, socket.io-client)
  • fix(deps): update dependency puppeteer to v22
  • ๐Ÿ” Create all rate-limited PRs at once ๐Ÿ”

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/ci-todo-example.yml
.github/workflows/ci.yml
.github/workflows/pr.yml
.github/workflows/release.yml
npm
package.json
  • @babel/core 7.21.8
  • @babel/preset-env 7.21.5
  • @babel/preset-typescript 7.21.5
  • @changesets/cli 2.24.4
  • @changesets/changelog-github 0.4.6
  • @types/jest 27.5.2
  • babel-jest 27.5.1
  • bob-the-bundler 2.0.0
  • chalk 4.1.2
  • globby 12.2.0
  • husky 8.0.1
  • jest 27.5.1
  • lint-staged 12.5.0
  • patch-package 6.5.1
  • prettier 2.7.1
  • semver 7.5.1
  • ts-jest 27.1.5
  • tsc-watch 4.6.2
  • typescript 4.7.4
  • ts-node ~10.9.0
packages/example-redis/package.json
  • @graphql-yoga/node 2.13.13
  • ioredis 5.3.2
  • @types/node 18.16.14
  • ts-node-dev 2.0.0
  • typescript 4.7.4
packages/graphql-live-query-patch-json-patch/package.json
  • fast-json-patch ^3.1.0
packages/graphql-live-query-patch-jsondiffpatch/package.json
packages/graphql-live-query-patch/package.json
  • @repeaterjs/repeater ^3.0.4
packages/graphql-live-query/package.json
packages/in-memory-live-query-store/package.json
  • @graphql-tools/utils ^8.5.2
  • @repeaterjs/repeater ^3.0.4
packages/json-patch-plus/package.json
packages/socket-io-graphql-client/package.json
  • @n1ru4l/push-pull-async-iterable-iterator ^3.2.0
  • typescript 4.7.4
  • socket.io-client ^3.0.1 || ^4.0.0
packages/socket-io-graphql-server/package.json
  • typescript 4.7.4
  • socket.io 4.5.2
  • socket.io ^3.0.1 || ^4.0.0
packages/todo-example/client-apollo/package.json
  • @graphql-codegen/cli 2.16.5
  • @graphql-codegen/gql-tag-operations-preset 1.7.4
  • @testing-library/jest-dom 5.16.5
  • @testing-library/react 12.1.5
  • @testing-library/user-event 13.5.0
  • @types/classnames 2.3.0
  • @types/react-dom 18.2.4
  • @vitejs/plugin-react-refresh 1.3.6
  • classnames 2.3.2
  • react 17.0.2
  • react-dom 17.0.2
  • socket.io-client 4.5.2
  • todomvc-app-css 2.4.2
  • vite 3.2.4
  • @apollo/client 3.7.14
  • @n1ru4l/push-pull-async-iterable-iterator 3.2.0
  • @repeaterjs/repeater 3.0.4
  • graphql-ws 5.11.2
packages/todo-example/client-relay/package.json
  • @n1ru4l/push-pull-async-iterable-iterator 3.2.0
  • @repeaterjs/repeater 3.0.4
  • @testing-library/jest-dom 5.16.5
  • @testing-library/react 12.1.5
  • @testing-library/user-event 13.5.0
  • @types/classnames 2.3.0
  • @vitejs/plugin-react-refresh 1.3.6
  • react-relay 12.0.0
  • react 17.0.2
  • react-dom 17.0.2
  • @types/react-dom 18.2.4
  • @types/react-relay 11.0.3
  • @types/relay-runtime 12.0.2
  • babel-plugin-relay 12.0.0
  • relay-compiler 12.0.0
  • relay-compiler-language-typescript 15.0.1
  • relay-config 12.0.1
  • relay-runtime 12.0.0
  • socket.io-client 4.5.2
  • todomvc-app-css 2.4.2
  • classnames 2.3.2
  • vite 3.2.4
  • vite-plugin-babel-macros 1.0.6
  • graphql-ws 5.11.2
packages/todo-example/client-urql/package.json
  • @repeaterjs/repeater 3.0.4
  • @graphql-codegen/cli 2.16.5
  • @graphql-codegen/gql-tag-operations-preset 1.7.4
  • @n1ru4l/push-pull-async-iterable-iterator 3.2.0
  • @testing-library/jest-dom 5.16.5
  • @testing-library/react 12.1.5
  • @testing-library/user-event 13.5.0
  • @types/classnames 2.3.0
  • @types/react-dom 18.2.4
  • @vitejs/plugin-react-refresh 1.3.6
  • classnames 2.3.2
  • react 17.0.2
  • react-dom 17.0.2
  • socket.io-client 4.5.2
  • todomvc-app-css 2.4.2
  • vite 3.2.4
  • urql 2.2.3
packages/todo-example/end2end-tests/package.json
  • puppeteer 18.0.5
  • fastify 3.29.2
  • fastify-static 4.7.0
packages/todo-example/server-helix/package.json
  • express 4.18.1
  • graphql-helix 1.13.0
  • socket.io 4.5.2
  • @types/express 4.17.14
  • @types/node 18.16.14
  • ts-node-dev 2.0.0
  • typescript 4.7.4
packages/todo-example/server-socket-io/package.json
  • socket.io 4.5.2
  • @types/node 18.16.14
  • ts-node-dev 2.0.0
  • typescript 4.7.4
packages/todo-example/server-ws/package.json
  • graphql-ws 5.11.2
  • ws 8.9.0
  • @types/node 18.16.14
  • @types/ws 8.5.3
  • ts-node-dev 2.0.0
  • typescript 4.7.4
packages/todo-example/server-yoga/package.json
  • @graphql-yoga/node 2.13.13
  • @types/node 18.16.14
  • ts-node-dev 2.0.0
  • typescript 4.7.4

  • Check this box to trigger a request for Renovate to run again on this repository

How to use with Redis?

The readme says:

For scaling horizontally the independent InMemoryLiveQueryStore instances could be wired together via a PubSub system such as Redis.

Could you give an example for how to do this?

Document prisma example usage with resource invalidation middleware

// Register Middleware for automatic model invalidation
prisma.$use(async (params, next) => {
  const resultPromise = next(params);

  if (params.action === "update") {
    resultPromise.then((res) => {
      if (res?.id) {
        liveQueryStore.invalidate(`${params.model}:${res.id}`);
      }
    });
  }

  return resultPromise;
});

add logo

I had this first draft in mind. Wanna incorporate the GraphQL logo as well

image

if argument on live directive

It would be nice if you could define whether a query should be live or not via a Boolean query argument. The main benefit I see is that the operation could then be used for SSR and streaming the results afterward.

unless could also be a valid option.

query users($isClient: Boolean = false) @live(if: $isClient) {
  users(first: 10) {
    id
    login
  }
}
const Foo = () => {
  const isSSR = useIsSSR();
  const [data] = useQuery(UsersQuery, { isClient: !isSSR })
  ...
}

add todo example app with apollo

In order to showcase and proof that @n1ru4l/socket-io-graphql-client works with apollo-client, adding the example app using apollo would be awesome :)

example needed for adding GraphQLLiveDirective to schema using graphql-tools and using subscription in @n1ru4l/socket-io-graphql-server

Screenshot 2021-03-29 125337

am using graphql-tools to build my schema with makeExcutableSchema but when GraphQLLiveDirective is added to the schema using the schema directives I get this error.

GraphQLLiveDirective Popup

this is a portion of my code

schema.ts

import { GraphQLSchema, specifiedDirectives } from 'graphql';
import { makeExecutableSchema, IResolvers } from 'apollo-server-express';
import { GraphQLLiveDirective } from '@n1ru4l/graphql-live-query';
import { typeDefs as scalarTypeDefs, resolvers as scalarResolvers } from 'graphql-scalars';
import { logger, SchemaData } from 'utils';
import * as data from './build';

const {
  typeDefs, Mutation, Query, Subscription, Directives, Resolvers,
  // @ts-ignore
} = Object.values(data as SchemaData)
  .reduce<SchemaData>((result: SchemaData, x: SchemaData): SchemaData => ({
    Mutation: { ...result.Mutation, ...x.Mutation },
    Query: { ...result.Query, ...x.Query },
    Subscription: { ...result.Subscription, ...x.Subscription },
    // @ts-ignore
    typeDefs: [...result.typeDefs, x.typeDefs],
    Directives: { ...result.Directives, ...x.Directives },
    Resolvers: { ...result.Resolvers, ...x.Resolvers },
  }),
{
  typeDefs: [...scalarTypeDefs],
  Mutation: {},
  Query: {},
  Subscription: {},
  Directives: {},
  Resolvers: { ...scalarResolvers },
} as SchemaData);

const schemaDirectives = {
  ...Directives,
};

const resolvers: IResolvers = {
  Mutation, Query, Subscription, ...Resolvers,
};

const schema: GraphQLSchema = makeExecutableSchema({
  logger: {
    log: e => {
      if (typeof e === 'string') {
        logger.info(e);
      }
      logger.error(e);
    },
  },
  allowUndefinedInResolve: false,
  resolverValidationOptions: {
    requireResolversForNonScalar: true,
    requireResolversForArgs: true,
    requireResolversForResolveType: true,
    // requireResolversForAllFields: false,
  },
  inheritResolversFromInterfaces: true,
  resolvers,
  typeDefs,
  schemaDirectives: [GraphQLLiveDirective]
});

export { schema };

and index.ts

import 'module-alias/register';
import { registerSocketIOGraphQLServer,  } from '@n1ru4l/socket-io-graphql-server';
import { Server as IOServer, Socket } from 'socket.io';
import express from 'express';
import http from 'http';
import { schema } from 'schema';
import { ApolloServer } from 'apollo-server-express';
import { checkJwt, pubClient, subClient, logger, mongoose } from 'utils';
import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store';
import { createAdapter } from 'socket.io-redis';

const app = express();
const httpServer = http.createServer(app);

const socketServer = new IOServer(httpServer, {
  cors: {
    origin: ['http://localhost:3000'],
    methods: ['GET', 'POST'],
    allowedHeaders: ['client-name', 'Authorization'],
    credentials: true,
  },
  // path: '/socketio-graphql'
});

socketServer.adapter(createAdapter({ pubClient, subClient }));

const liveQueryStore = new InMemoryLiveQueryStore();

registerSocketIOGraphQLServer({
  socketServer,
  /* getParameter is invoked for each incoming operation and provides all values required for execution. */
  getParameter: async ({
    /* Socket.io instance of the client that executes the operation */
    socket,
  }) => ({
    execute: liveQueryStore.execute,

    /* The parameters used for the operation execution. */
    graphQLExecutionParameter: {
      /* GraphQL schema used for exection (required) */
      schema,
      /* root value for execution (optional) */
      rootValue: {},
      /* context value for execution (optional) */
      contextValue: await (async () => {
        const user = await checkJwt(socket.handshake?.auth?.token);
        return {
          user,
          socket,
          liveQueryStore,
          mongoose,
          logger
        }
      })(),
    },
  }),
});

socketServer.on('connect', (socket: Socket) => console.log({ id: socket.id, token: socket.handshake.auth}));

const apolloServer = new ApolloServer({
  schema,
  context: async ({ req }) => ({
    user: req?.user,
    mongoose,
    logger,
    req,
  }),
  introspection: true,
});

apolloServer.applyMiddleware({ app })

httpServer.listen(4000, () => console.log('listening'));

secondly @n1ru4l/socket-io-graphql-server does not export pubsub. so is there an example guy cause the todo app does not have an example of subscription. but in the readme it says
A layer for serving a GraphQL schema via a socket.io server. Supports Queries, Mutations, Subscriptions and Live Queries.
so how is the subscription suppose to work without pubsub over socket.io.

am I missing something. please help

Issue with chalk dependency within jsondiffpatch [graphql-live-query-patch-jsondiffpatch]

benjamine/jsondiffpatch#249

The issue is that the @n1ru4l/graphql-live-query-patch-jsondiffpatch package doesn't function in-browser due to importing everything from the jsondiffpatch package within this file, which makes the browser try to import the chalk dependency, which throws an error in the browser because it's trying to access process which doesn't exist outside of node.

Action Required: Fix Renovate Configuration

There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.

Location: renovate.json
Error type: Invalid JSON (parsing failed)
Message: Syntax error: expecting end of expression or separator near "] "post

Debouncing Support ?

Is there a possibility of adding a debounce strategy (with an upper limit) ? E.g. debounce changes for 50ms with a hard limit of 500ms. The diff would be between the value at the start of the debounce period (since thats the version matching the client) and the value at the end of the period.

As a use case I'm building an api to a queueing engine, and certain queries can get chatty at high concurrency. Most of the values I'm monitoring are gauges/counters and other entities for which from a dashboard perspective intermediate values (within a certain granularity) are not important.

Improve package publishing

Suggestions:

  1. The exports field shouldn't stripped from the published package.json and no index.mjs file is included. @n1ru4l I found that you raised this issue earlier, (though not sure why it was closed) kamilkisiela/bob#16
  2. Don't bundle. Publish as commonjs unbundled with both commonjs and ESM entrypoints. This can also allow consumers to deep import files rather than via their named exports. This should be encouraged in the documentation. The entrypoints can remain mostly for static analysis.
  3. As an alternative, perhaps consider perhaps only publishing ESM. https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.