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

Pre-RFC Live queries #386

Open
acjay opened this issue Nov 22, 2017 · 36 comments
Open

Pre-RFC Live queries #386

acjay opened this issue Nov 22, 2017 · 36 comments
Labels
💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)

Comments

@acjay
Copy link

acjay commented Nov 22, 2017

I've heard live queries alluded to on various podcast episodes and in the RFC for subscriptions, but it's unclear to me whether there's a repository of ideas on what this feature would look like.

Purpose

GraphQL supports an event-based real-time mechanism called subscriptions. These are useful for many uses cases, such as when you need a UI to explicitly reflect domain events to the user.

However, these aren't the only type of real-time semantics. Sometimes, a the UX is designed around depicting the real-time state of a domain, rather than discrete domain events. It is possible to serve this need with GraphQL subscriptions. Each event could effectively expose the entire root query schema, or the applicable slice of it. That way, clients could update arbitrary local state for every event. This brings up a couple challenges:

  1. This could result in a great deal of over-pushing, if most of the schema hasn't changed in response to the new event.
  2. There needs to be a way to ensure clients have never miss any information. Subscriptions don't natively support a first payload.

Less naive implementation on subscriptions

I think it may be possible to implement achieve this on top of subscriptions with some machinery.

Change types

For issue (1), there would need to be a way to represent changes to a schema. This could be done by representing every type A in a schema with a box type:

type Change[A] {
  change: A
} 

The box would be necessary for times when A is nullable, to differentiate between a change to null and no change.

The subscription representing a live query update would effectively be one giant "data model changed" event. It would need to provide a field that serves as the root of the live-updatable data. If the data is represented by type Data, this field would have a type of Change[Data]. Change[Data] would have a field of type Change[A] for every field of type A in Data.

This generic Change type doesn't need to literally exist, but it could be synthesized.

Server-side client model

To determine what changes to send, the server would either need to store the current client-side state (in the same way that it needs to store subscriptions queries) or its business logic would need to natively be capable of deriving diffs.

Array changes

This leaves out the question of efficient updates to arrays, which would naively be wrapped whole in a box. But that could still result in over-pushing and it might not play way well abstractions like Relay's connections.

Bootstrap events and consistency

For issue (2), there could be a synthetic event that the server fires immediately upon subscription success, which would have field containing the unaugmented payload of Data, which would allow the clients to bootstrap their local state. If the transport guarantees that either all messages are received or the connection fails (like Web Sockets), consistency would be guaranteed. Other transports would need some other system for ensuring consistency. Either way, applications would likely want to be able to depict interruptions of the connection.

So, how to do it?

The above approach is appealing in that it can conceivably be built on primitives that exist today, albeit with some schema augmentation and some GraphQL library machinery on the client and server sides. On the pro side, this would allow for event-based subscription payloads and live query updates to coexist in one protocol. On the con side, not having a first-class representation could make the pieces needed to achieve live queries feel disjointed.

I wanted to throw this out there as a rough proposal to get some comments and suggestions. If there's any interest, I can try to make it a proper RFC. My company is looking at adopting GraphQL for one of our real-time products, and I'm trying to look ahead to whether we'd be able to replace some bespoke Web Sockets stuff with something that's more of a standard. Maybe there's an opportunity for us to help push such a standard forward.

@acjay
Copy link
Author

acjay commented Nov 24, 2017

I'm discovering more writings related to live queries:

#284 -- A long discussion of how live queries relate to subscriptions.

I’ve put more thought into this, and I’ve concluded that while it would be possible to build live queries on top of event-based subscriptions, perhaps as a proof-of-concept, there's really a need for some dedicated support for live queries for the concept to truly be effective.

Live query semantics

I'd suggest the following semantics:

  1. A live query yields a result, rather than returning it. This is in the sense that can it be thought to produce a stream of discrete responses, like an iterable or generator function, rather than returning a single response, like an ordinary function.
  2. The initial result that a live query yields is the same as what the same ordinary query would return.
  3. Subsequent responses, the updates, must contain at least the data in the result that has changed.
  4. It is possible that response is yielded that has no changes or changes that reflect the same data.
  5. It is not guaranteed that all changes are reflected. In other words, the query can be considered to be sampled.
  6. For updates that exclude unchanged data, there is a need to differentiate between data changing to null and data that has not been updated. (Would key presence be sufficient to indicate that something changed?)

This is loose enough to allow for many implementations to meet this standard, from the standpoints of a GraphQL client library API and client-server communication. In the worst case, the library could simply poll an entire ordinary query and unconditionally invoke a callback with the response. This would provide suboptimal latency, would likely overpush, would trigger client updates too often, and could miss some rapid changes. But, it could satisfy the above points only using standard HTTP approaches, proving that there’s nothing terribly exotic about unoptimized live queries.

However, a better client library solution would be to represent the result of a query as some sort of observable data structure. Likewise, a better client-server communication paradigm would be to use a server push technology to stream minimal updates. Such optimizations would likely rely on more advanced technology, like reactive systems and stateful server push mechanisms.

Representing updates

From what I can tell, the biggest novelty from a GraphQL perspective would be a protocol for representing incremental updates to a response. It would be highly desirable to model both the initial result and the updates using one type schema. As the spec for executing selection sets mandates that every selected field appear in the result, I believe this would be possible by loosening this restriction for updates. Absence in the response map would indicate that no change occurred.

The trickiest part would be concisely representing changes in GraphQL’s only unbounded type, the list. One option would be to use something like Javascript’s Array splice API as a concise way of describing edits to a list.

Coexistence with subscriptions

To achieve desirable user experience, sometimes it’s best to sync the UI with the server-side data model, which is the use case live queries serve. Other times, it’s best to depict to the user what happened, rather than showing a live view of the data. In these cases event subscriptions are more appropriate. But often, there is a blend of these needs, and it would be best for a client to have both the update and the reason. So then it would be ideal for a client to able to subscribe to live queries and events in one operation, over a single connection.

It seems like the existing subscription mechanism could be expanded to accommodate live queries. It could be as simple as adding a liveQuery field to the subscription type, which would expose as much of the query schema as supports live queries. I’m not sure whether it’s spec-compliant, but it appears Sangria already uses special semantics for subscriptions, in which only events that happened are included in responses.

To tie the two types of subscriptions together, it would be nice to mandate that a source event that results in an response event and a change to the selection of the live query should package that live query data in the same response. That way, clients could count on depicting the latest at the time of the event.

@alloy
Copy link

alloy commented Nov 24, 2017

As an FYI, I think that when you hear about ‘live queries’ it refers to a polling based approach which Relay calls ‘live queries’.

You can read more about it in this discussion and the linked to API.

@acjay
Copy link
Author

acjay commented Nov 25, 2017

@alloy Ah, thanks! Yeah, so Relay is literally implementing the naive live query approach. Biggest problem is that it doesn't seem to leave room for partial updates.

So, I actually think the scope of this is much smaller than I would have guessed. It's really more like, we should enhance current subscriptions model with the concept of partial updates and a schema directive that specifies that a given root field in the subscription root type has live query (initial response + [possibly partial] updates) semantics. Everything else would be left to application-level SLAs that would depend on library support and the requirements of individual products.

@stubailo
Copy link
Contributor

Apollo Client has polling built in, but we don't call it live queries. I think when people refer to live queries they are usually talking about the server pushing updates to a query, possibly combined with some kind of directive. Usually it's something that's quite hard to do with just normal queries and subscriptions, at least in a generic way.

For example, you can get this today with packages built by @DxCx:

  1. Observables in resolvers: https://github.com/DxCx/graphql-rxjs
  2. Integration with Meteor: https://github.com/DxCx/meteor-graphql-rxjs

These aren't quite prepared for public presentation but I think Hagai would be interested in collaborating with more people about it!

@acjay
Copy link
Author

acjay commented Nov 28, 2017

Yeah, my idea here was to present something that would encompass polling and efficient reactive push updates. I'll check those out and see if there's a good place to collaborate!

@taion
Copy link

taion commented Dec 11, 2017

See also the JSON patch spec: http://jsonpatch.com/

In practice it's not actually that hard to build out something live-query-like on top of subscriptions. We've done it – it's not so bad.

The semantics get tricky if you have something like – suppose you have a connection that's searchable. You probably don't want to allow live queries when you have a search query specified, since they're not really efficiently implementable (arbitrary sort/filter in general – if a new element is inserted, can the server efficiently compute the insertion index?).

The other issue implementation-wise is – how do you even follow the spec? In principle live queries should be fully recursive, right? Step down the query, provide updates on everything down the path... but what happens if portion of the query isn't (or shouldn't be) "live"?

@acjay
Copy link
Author

acjay commented Dec 11, 2017

See also the JSON patch spec: http://jsonpatch.com/

Interesting. So looking at that spec, one implementation of this concept would simply be a standard event subscription with a schema that would look something like:

scalar GraphQLQuery 
scalar JSON

type Subscription {
  liveQuery(query: GraphQLQuery): LiveQuery
}

type LiveQuery {
  op: String!
  from: String
  path: String
  value: JSON
}

A couple thoughts:

  • The initial return could be represented as an add operation at the root.
  • There isn't a way to statically check the selection for the query.
  • It doesn't really identify what fields are available for live queries.
  • JSON Patch might be a heavyweight representation for query diffs, in a lot of cases.

[...] suppose you have a connection that's searchable.

[...] In principle live queries should be fully recursive, right?

The idea would be that it's up to developer judgment what makes sense to live query.

As mentioned in my proposed semantics, a library could always trivially fall back to polling the standard resolvers to implement the bare minimum requirements. Libraries could simply choose to offer better guarantees by offering a way for some data to be pushed by application code.

The way I see it, you would only put types in your liveQuery field in the subscription type that you'd want to be queried this way. It would be a filtered down version of the query root type.

I guess it would be tricky to share types and resolvers between normal queries and live queries, if you want the latter to be more restrictive. For recursion and live updated searches, my first thought is I don't see why to outlaw these things, but I'd probably leave it to people who have the need to figure out how to do it efficiently for their use case.

@acjay
Copy link
Author

acjay commented Dec 11, 2017

Actually, maybe a better way to apply this idea would be:

scalar JSON

type Subscription {
  liveQuery: LiveQuery
}

type LiveQuery {
  query: Query # or an alternate root query type
  update: Patch # or an alternate type with different update semantics
}

type Patch {
  op: String!
  from: String
  path: String
  value: JSON
}

This would make the live query shape itself checkable and returned as an immediate initial subscription push. All subsequent updates would come back in patch format in the update field. It would take some server-side magic to implement to right semantics for LiveQuery. Query parsing would have to store the query AST for generating the initial response event and for filtering pushes.

@taion
Copy link

taion commented Dec 12, 2017

@robzhu linked https://www.youtube.com/watch?v=BSw05rJaCpA in #284 (comment), which is super helpful. Adding it here in case anyone missed it on #284.

Someone should write up the video content into a blog post! It's really great.

@taion
Copy link

taion commented Dec 22, 2017

@paralin

Following up on our discussion in #284, I think there's really two questions here with live queries:

  1. How do we know when to send a live query update?
  2. What do we send on the wire to the client?

I entirely agree that "the full query response" is an inefficient answer for (2), but it is nevertheless an answer. I think, however, that @rodmk's talk mostly touches on (1).

And just to note earlier, with subscriptions, the equivalent of (2) is really obvious. Here we do in fact need to spec it out.

@paralin
Copy link

paralin commented Dec 22, 2017

For live query semantics, I would note:

  • Live queries don't have to finish receiving the entire result before yielding one to the UI. This would be called a "deferred query" and in rgraphql I make everything deferred on default.

This causes some drawbacks though... I've run into issues with deciding when to sync the partial result state to the UI. I'm thinking it may be a good idea to build some "response gates" that allow pieces of the result to surface to the UI as they are completed - by depth, perhaps?

@taion Easy answers:

  1. Use a merkle hash tree between the server and client to keep track of what the client knows without keeping the actual data in memory. Cache this merkle hash into something like Redis. If the connection to the edge server serving the live query dies, when reconnecting you can recover the state of the query and re-negotiate the result with a merkle tree negotiation.
  2. Use my encoding scheme: https://github.com/rgraphql/magellan/blob/master/DESIGN.md#result-encoding-algorithm or something similar-

I maintain that it's unnecessary and in fact a detriment to attempt to build something like this on top of a classic GraphQL engine. The performance losses and development work-arounds necessary defeat the purpose. You may as well start from scratch with something efficient and correct.

@acjay
Copy link
Author

acjay commented Dec 22, 2017

@taion posed the key questions. Here's what I would propose:

  1. Leave it up to the server. In some cases, the entire data layer could be implemented in something like observables or streams, in which case you could batch up all the change for an update in each turn of an event loop, then basically mask that data with the stored query document to determine what fields need to be pushed. Sangria already implements this. Or, if the data layer isn't reactive, GraphQL live query layer could poll. But in any case, the answer is not much different than for events. From the subscription RFC:

    In "GraphQL Subscriptions", clients send the server a GraphQL query and query variables. The server maps these inputs to a set events, and executes the query when the events trigger. This model avoids overpushing/underpushing but requires a GraphQL backend. GraphQL Subscriptions provides an abstraction over individual events and exposes an API where the client subscribes to a query.

    In other words, this already exists!

  2. We've effectively got a whole bunch of JSON paths that either have a new value, or not. There are many totally valid answers, striking different balances of performance (under various assumptions) and simplicity. I would recommend that the choice be left undefined in the spec, other than defining a way to specify an encoding scheme by identifier per field. I'd suggest we figure out a sensible way to specify this. Maybe this could just be done with directives, but I'll try to come up something as an example that can be discussed.

Lastly, where (1) and (2) connect is that it's possible that some fields will have arguments per subscriber and need to calculate diffs. I'd suggest that servers can account for this by having the previous value of a field be made available to their resolver.

@paralin I've got some thoughts on the points you raised, but I'll follow up with another comment shortly, since this one's long enough...

@acjay
Copy link
Author

acjay commented Dec 22, 2017

@paralin I think you may be thinking of a fairly different concept. I want to be clear here, what I've got in mind is a system that would allow people to architect systems with "live view" semantics on top of GraphQL's paradigm of client-specified API traversal. While it should be performant, the goal isn't to squeeze ever drop of performance in day one (which is really an implementation concern, anyway), but rather to be flexible enough to admit optimization.

That said, you've brought up two concerns with considering:

  • Streaming & prioritization I think this can be left up tot he server implementation, as is the case with transport concerns in vanilla query operations. I think directives could probably be used to allow a client to instruct a suitable server in how to send responses, which should leave the door open for that type of optimization.

  • Reconnection This is an interesting way in which live query subscriptions might semantically differ from event subscriptions. In an event subscription, the full burden is on the client to manage their local state. An event-based system may not even assume the client maintains a fully up-to-date model (notifications are often designed not to carry a full UI update).

    By contrast, the whole point of a live query system is to maintain up-to-date state. The trivial solution is to just reissue the live query and get the full base response again. We can probably do better, though. Riffing off of the Merkle tree idea, I think a pattern like cursor-based pagination could be used. If an update could contain provide some key representing its current state, the key could be passed to the root live query subscription field for resumption, and the server can send an appropriate update, dependent on its capabilities. Those capabilities could be resumption states arbitrarily far back in the past or a simple boolean decision on whether the client missed anything during the connection lapse.

    As is my answer to everything -- leave it up to the implementations. But the most important takeaway for me is that the tenant that "the initial result that a live query yields is the same as what the same ordinary query would return" is maybe not desirable, and instead maybe the whole stream should be updates, some of which may happen to be full responses.

You're right that a live query system could be built around the basic query concept purely by playing with the transport details the spec intentionally leaves unspecified. However, I believe that the same can be said for event subscriptions, and also that failing to capture push concerns in the spec will fragment the community.

I'm beginning to feel confident that I can put together either an RFC for Live Query Subscriptions or a PR to the current subscriptions RFC. I think that at this point, the principles are more or less clear and some examples would be a good next step.

@paralin
Copy link

paralin commented Dec 23, 2017

@acjay I too am talking about making a live view on top of existing apis.

You're misunderstanding the intent behind subscriptions. The intent is to subscribe to a set of events of a single type. Each event emitted has the same type. There are no semantics for updating a result, it is purely events.

This is called "event based state" - one event type emitted for each change to the state. What this lacks in order to make it a "live query system":

  • Developers do all the work of defining event types, when they are emitted, and how they are interpreted. A good example of an event is COMMENT_POSTED.
  • There is no way to specify a flexible query that will engage the typical graphql query mechanisms. The arguments to the subscription are of a single type and are passed to the server code verbatim.
  • There is no field based change detection, because the data layer is not aware of the structure of the data.

The subscriptions system is not and was not intended to be a live queries system. It solves a different set of problems. Live queries are a different thing altogether.

Live queries are not a question or language or spec. The existing language and spec works just fine for them. I did not have to modify the underlying Go and JavaScript graphql parsers at all. The spec has room for server declared directives, so the server can specify @live as a valid directive, for example.

The difficult parts of live queries are dealing with change detection, computation sharding, the inefficiencies of keeping a connection open with all connected clients, and result encoding. Otherwise, it is possible to prototype a live query system in 10 minutes flat, because computing a diff and sending it over the wire is an easy task - it's making this scale, and allowing the query to be changed without restarting the entire operation, that makes something like this take some planning.

It is for these reasons that I think the best way forward for implementing these things is to make real practical proof of concept implementations that do not stray from the existing spec, aside from custom directives. We can keep the base code in place for the parser, there's no need to modify it. Let's find ways to make this scale, prove that they work, and then consider making some sort of Request For Comment from the Facebook team to initiate a standard.

@acjay
Copy link
Author

acjay commented Jan 9, 2018

@paralin Sorry for the long delay -- holiday break.

I think you misunderstand what I'm saying. I totally get that the intention of subscriptions was an event-push model. My point is that the mechanism--even if inadvertently--is actually suitable for live queries in a fairly generic way, as well.

Yes, you are right that a subscription has one field. But that field has the full power of the GraphQL type system behind it, and it doesn't have to conceptually represent a domain event. There's no reason it can't be the entry point to an entire schema of live data, such that every event effectively triggers a push of data of the requested shape.

Similar patterns are often used in mutations to allow the mutation request to respond with mutation outcome data (intuitively) or arbitrary data, in any combination, as the schema designer and client see fit.

A more concrete idea is forthcoming...

@paralin
Copy link

paralin commented Jan 9, 2018

But that field has the full power of the GraphQL type system behind it, and it doesn't have to conceptually represent a domain event. There's no reason it can't be the entry point to an entire schema of live data, such that every event effectively triggers a push of data of the requested shape.

This is unfortunately not true. You can of course make a generic message type and send it over the subscription but this doesn't get you anywhere.

@acjay
Copy link
Author

acjay commented Jan 9, 2018

I just reread the subscription spec, but I'm not seeing how this is disallowed. Could you explain?

@paralin
Copy link

paralin commented Jan 10, 2018

@acjay I'm guessing you want to do something like this?

Schema:

type Query {
 ...
}
type Subscription {
  subscribeToQuery: Query
}

Rerunning all of the resolvers every time something changes is going to kill your server. I don't see the point. It's possible I'm completely out of sync with what you are imagining, in which case, I look forward to seeing your designs and prototypes. Otherwise, I can't see how something like this could be useful for more than just a toy implementation.

@acjay
Copy link
Author

acjay commented Jan 10, 2018

That's not what I would literally do, but I would argue that that schema should be possible. So, yes: types used as queries should be reusable in subscriptions.

Realistically, the root fields present in the subscription type would be limited to those that the server is architected to produce efficiently. The resolvers for types shared between queries and subscriptions would have to be written to take advantage of data pushed in the event payload or reactive data wherever possible, instead of pulling from services, as a normal query would. I'd expect techniques like caching and memoization to be used--scoped to the processing cycle of an event--to prevent duplicate work from being done when many clients need the same data. This is not unlike the way data loaders are used to make resolution more efficient in ordinary queries.

@sorenbs
Copy link

sorenbs commented Feb 23, 2018

@acjay - We implemented a prototype of live queries on top of subscriptions for Graphcool back when we first implemented subscriptions. We found that exposing diffs or changes in the schema was overly complicated. Our prototype ended up simply exposing the field live: Query on the Subscription type. Whenever the query result would change we send the full payload as an event. Three implementation details are critical to make this work at scale:

  1. queries must be cheap - this is the case for Graphcool and many modern other modern systems.
  2. query coalescing - never send more than one update a second to a single subscriber (or whatever the best tradeoff is for your application)
  3. query deduplication - when multiple subscriptions exist for the same query, only do the work once.

2 is a fairly limiting constraint. If the query depends on the active session, it can be difficult or impossible to deduplicate between sessions, forcing you to evaluate the query for each subscriber.

We never shipped live queries, but I am excited to see that interest is picking up around this topic.

See also the notes from the latest working group meeting: https://github.com/graphql/graphql-wg/blob/master/notes/2018-02-01.md#discuss-subscriptions-for-live-queries

@paralin
Copy link

paralin commented Feb 23, 2018

@sorenbs The issues you describe were already solved in the rgraphql proof of concept:

  1. Queries are cheaper than what you describe when you stream results rather than send the entire thing every time it changes.The net bandwidth is lower and the CPU usage is lower.
  2. Query batching with this encoding algorithm can be used to send a packet at whatever threshold is needed. In the proof of concept it is a packet size and timeout constraint.
  3. Deduplication is possible if you understand the contexts of the queries. Entire sub-trees of queries can be cached and saved. Typically this is quite doable by marking a sub-tree of the graph as "public" and "singleton" with identical arguments.

@acjay
Copy link
Author

acjay commented Feb 24, 2018

@sorenbs That was me who brought up the topic at graphql-wg :)

So, for 2 and 3, I think those would be "left up to the implementation".

I'm actually at a point right now where I think I want to pursue something that requires no actual changes to the GraphQL spec at all. The schema would look something like:

scalar GraphQLRequest
scalar JSON

type subscription {
  liveQuery(query: GraphQLRequest!, resumptionCursor: ID) {
    data: LiveQueryData 
  } 
}

union LiveQueryData = LiveQueryInitialResponse | LiveQueryUpdate

type LiveQueryInitialResponse {
  # A standard GraphQL query response
  payload: JSON!
  resumptionCursor: ID!
}

type LiveQueryUpdate {
  payload: JSON!
  # An event that would normally be a root subscription field
  event: Event!
  resumptionCursor: ID!
}

GraphQLRequest is a string containing a request that is valid with respect to whatever slice of the query schema we see fit to whitelist for live queries. By representing the responses as JSON, it frees our hand to implement whatever type of diff representation we want.

We probably won't actually attempt this for a while yet, but thought I'd report this idea back, since I started this ticket.

@D1plo1d
Copy link

D1plo1d commented Mar 29, 2018

@acjay I would be interested to see this implemented in a GraphqlJS library and could help build that prototype. Am I understanding you correctly that you are creating a subscription that returns a LiveQueryInitialRespone immediately and a LiveQueryUpdate as needed after that?

Also https://tools.ietf.org/html/rfc6902 format is already an existing standard so I would lean towards your 2nd December 11 suggestion for the Patch type. Also I expect having a update and a query field will make client side implementation easier (eg. nextState = data.query || applyRFC6902Patch(previousState, data.patch))

@D1plo1d
Copy link

D1plo1d commented Mar 29, 2018

One more note: if possible I would prefer to keep the benefits of GraphQL typing for the initial query. Even though the patch needs to be json (or a minimum of a union of every type of field at any level of nesting in the query) we could at some point use the type information from the query to determine server-side and client-side if the patch JSON is valid and effectively implement typed patches in the library.

So based on this and my previous comment I would suggest the following schema (Scroll to bottom for example useage)

Example Schema

scalar GraphQLRequest
scalar JSON

# As an example we are live querying all posts
type Post {
  id: ID!
  title: String!
  postedAt: String!
}

type subscription {
  AllPosts(resumptionCursor: ID): PostLiveQuery!
}

type PostLiveQuery {
    query: [Post!]
    # Patches may be batched
    patch: [RFC4627Patch!]
    resumptionCursor: ID!
    # TODO: Should resumptionCursor be optional for servers that are unable to
    # resume a live query?
  } 
}

union RFC4627Patch =  
  || RFC4627Add
  || RFC4627Remove
  || RFC4627Replace
  || RFC4627Move
  || RFC4627Copy
  || RFC4627Test

# Note the op fields bellow are redundant with __typename but are included for
# RFC4627 compatibility.

RFC4627Add {
  op: String! # Always returns "add"
  path: String!
  value: JSON!
}

RFC4627Remove {
  op: String! # Always returns "remove"
  path: String!
}

RFC4627Remove {
  op: String! # Always returns "replace"
  path: String!
  value: JSON!
}

RFC4627Move {
  op: String! # Always returns "move"
  from: String!
  path: String!
}

RFC4627Copy {
  op: String! # Always returns "copy"
  from: String!
  path: String!
}

RFC4627Test {
  op: String! # Always returns "test"
  path: String!
  value: JSON!
}

Example Subscription

In this example:

  • an event with the query set would be returned immediately with the id and title of each post.
  • on creation of a post a patch of type RFC4627Add would be sent
  • on deletion of a post a patch of type RFC4627Remove would be sent
  • on change to the title of any post a patch of type RFC4627Replace would be sent
  • on change to the position in the list a patch of type RFC4627Move would be sent
  • RFC4627Test and RFC4627Copy are available for use at the GraphQL server implementer's discretion
subscribe {
  allPosts {
    query {
      id
      title
    }
    patch {
      ... on RFC4627Add { op, path, value }
      ... on RFC4627Remove { op, path }
      ... on RFC4627Replace { op, path, value }
      ... on RFC4627Move { op, from, path }
      ... on RFC4627Copy { op, from, path }
      ... on RFC4627Test { op, path, value }
    }
  }
}

Edit: fixed some mistakes

@D1plo1d
Copy link

D1plo1d commented Apr 3, 2018

I've published an npm package based on this thread for subscription-based live queries: https://github.com/D1plo1d/graphql-live-subscriptions

@acjay
Copy link
Author

acjay commented Apr 7, 2018

This is really awesome @D1plo1d! I think that would work for my use case, and it seems conceptually simpler than what I had described. Especially so in your library, where all the patch subtypes are flattened into one type.

For the resumption question, yeah, optional. In the case I'm considering, it would be easy to implement, but I can definitely imagine that not being the case for all applications.

@nkordulla
Copy link

@D1plo1d I like your schema, I just modified it a little bit, because this makes more sense for me.

Example Schema

type Post {
  id: ID!
  title: String!
  postedAt: String!
}

type subscription {
  allPosts: PostLiveQuery!
}

type PostLiveQuery {
  result: PostLiveQueryResult!
}

union PostLiveQueryResult = [Post]! | [RFC4627Patch]!

# for RFC4627Patch see the comments above

Explanation: You send only the query or the patches not both.

And I dont think we need a resumptionCursor, because the Server knows the "initialquery" of this subscription, and calculates the patches in relation to this initial query.

@D1plo1d
Copy link

D1plo1d commented May 23, 2018

@acjay thanks! Resumptions are the biggest unknown for me atm. I haven't given them much thought tbh but please experiment away and let me know how it goes! If you need to change anything to the library to get them working I'd really appreciate the Pull Requests.

@D1plo1d
Copy link

D1plo1d commented May 23, 2018

@nkordulla great idea. I like that returning a union would allow both the Post and the RFC4627Patch to be wrapped in NonNullable - I like that type safety.

Any reason not to wrap the patch part of the union in [RFC4627Patch!]! instead of [RFC4627Patch]!?

I'm presently optimising the patch generation because re-serializing queries every change proved to be prohibitively slow for my usage (we're running our node servers on Raspberry Pi's though to be fair).

If you want to take a stab at refactoring the schema to be Union based on the current master branch that would be great (just shoot me a PR) or I could let you know when I'm finished with my optimisation work to save us a nasty merge.

re: resumptionCursor: Not that any of this is implemented yet but I think the idea here is that if your client momentarily looses it's connection to the server it could send it's resumptionCursor and start receiving patches again from where it left off without the need for another initial query response. As I understand it the resumptionCursor would prevent race conditions in the case where first the client disconnects then a change is made and then the client reconnects (and wishes to optimise the amount of data over the wire by skipping a re-send of the initial query payload). It's unnecessary AFAIK in any scenario where you re-send the initial query on re-connect like subscribeToLiveData does presently.

@D1plo1d
Copy link

D1plo1d commented May 23, 2018

Also a note on usage with graphql-live-subscriptions v0.1.0: I've found in practice that I prefer to nest all of my live data under one "live query root". It simplifies my client-side code because I can create one live subscription for all of my live data. Eg:

Schema

type Post {
  id: ID!
  title: String!
  postedAt: String!
}

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

type LiveQueryRoot {
  jedis: [Jedi]
  posts: [Post]
}

type LiveSubscription {
  query: LiveQueryRoot
  patch: [RFC4627Patch]
}

type subscription {
  live: LiveSubscription!
}

Client Side

subscribe {
  live {
    query: {
      jedis: { name }
      posts: { title }
    }
    patch: // .. usual patch query here
  }
}  

@leebyron leebyron added the 💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md) label Oct 2, 2018
@D1plo1d
Copy link

D1plo1d commented Oct 17, 2018

A quick update, I've released graphql-live-subscriptions 1.0.0 as I've been developing against this version for a couple months now without any issues. Oh, and the earlier performance issues were also fixed.

You can check it out at https://github.com/D1plo1d/graphql-live-subscriptions

My apologies for the less-then-great README. If you have any questions feel free to ask me directly.

@sorenhoyer
Copy link

Any progress?

@Bessonov
Copy link

Bessonov commented Aug 1, 2019

@sorenhoyer there is a lot of progress:
https://www.npmjs.com/package/graphql-live-subscriptions
https://github.com/D1plo1d/graphql-live-subscriptions

@paralin
Copy link

paralin commented Aug 1, 2019

@Bessonov @D1plo1d interesting work on graphql-live-subscriptions, congrats.

I'm actually still using rgraphql / magellan's binpacked protocol in a few projects, and have rewritten it around static code generation in Go for better performance, compiler type safety, and removal of the "reflect" package. The prototype of this approach is here: https://github.com/rgraphql/nion with the TypeScript client here: https://github.com/rgraphql/soyuz/tree/nion

@fbjork
Copy link

fbjork commented Jan 17, 2023

We announced support for Live Queries at Grafbase yesterday. Check it out: https://grafbase.com/blog/simplify-building-realtime-applications-with-graphql-live-queries

Let's get this spec approved:)

@D1plo1d
Copy link

D1plo1d commented Jan 17, 2023

Congrats @fbjork! I like the JSON patch over Server-Sent Events format. I've been playing with WebTransport lately and I suspect that this format could also be applied to WebTransport Unidiretional Streams equally well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)
Projects
None yet
Development

No branches or pull requests