clue.model.StreamingMessage.scala Maven / Gradle / Ivy
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause
package clue.model
import cats.Eq
import cats.syntax.all.*
import io.circe.Json
import io.circe.JsonObject
// FIXME This should go in json? Be named ApolloMessage?
/**
* GraphQL web socket protocol streaming messages. Messages are cleanly divided in those coming
* `FromClient` and those coming `FromServer`. See also
* https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md. Also see:
* https://medium.com/@rob.blackbourn/writing-a-graphql-websocket-subscriber-in-javascript-4451abb9cd60
*/
object StreamingMessage {
/**
* Messages implement `Identifier` to distinguish (potentially) long-running operations like
* subscriptions where results come in over time.
*/
sealed trait Identifier {
val id: String
}
/**
* Messages that include a payload implement `Payload`.
*/
sealed trait Payload[P] {
val payload: P
}
/**
* Client-produced streaming messages.
*/
sealed trait FromClient extends Product with Serializable
object FromClient {
/**
* Starts communication with the server. The client may expect a `ConnectionAck` or
* `ConnectionError` in response.
*
* @param payload
* any connection parameters that the client wishes to send
*/
final case class ConnectionInit(payload: Map[String, Json] = Map.empty)
extends FromClient
with Payload[Map[String, Json]]
object ConnectionInit {
implicit val EqConnectionInit: Eq[ConnectionInit] =
Eq.by(_.payload)
}
/**
* Starts a GraphQL operation. The operation contains an id so that it can be explicitly stopped
* by the client and so that data associated with the operation coming from the server may
* identified.
*
* @param id
* identifier of the operation to start
* @param payload
* the GraphQL request itself
*/
final case class Start(id: String, payload: GraphQLRequest[JsonObject])
extends FromClient
with Identifier
with Payload[GraphQLRequest[JsonObject]]
object Start {
implicit val EqStart: Eq[Start] =
Eq.by(a => (a.id, a.payload))
}
/**
* Stops a running GraphQL operation (for example, a subscription).
*
* @param id
* identifier of the operation that was previously started
*/
final case class Stop(id: String) extends FromClient with Identifier
object Stop {
implicit val EqStop: Eq[Stop] =
Eq.by(_.id)
}
/**
* Informs the server that the client wishes to terminate the connection.
*/
case object ConnectionTerminate extends FromClient
implicit val EqFromClient: Eq[FromClient] =
Eq.instance {
case (a: ConnectionInit, b: ConnectionInit) => a === b
case (a: Start, b: Start) => a === b
case (a: Stop, b: Stop) => a === b
case (ConnectionTerminate, ConnectionTerminate) => true
case _ => false
}
}
/**
* Server-produced streaming messages.
*/
sealed trait FromServer extends Product with Serializable
object FromServer {
/**
* A server acknowledgement and acceptance of a `ConnectionInit` request.
*/
case object ConnectionAck extends FromServer
/**
* A server rejection of a `ConnectionInit` request.
*
* @param payload
* error information
*/
final case class ConnectionError(payload: Json) extends FromServer with Payload[Json]
object ConnectionError {
implicit val EqConnectionError: Eq[ConnectionError] =
Eq.by(_.payload)
}
/**
* Server initiated message that keeps the client connection alive.
*/
case object ConnectionKeepAlive extends FromServer
/**
* GraphQL execution result from the server. The result is associated with an operation that was
* previously started by a `Start` message with the associated `id`.
*
* @param id
* operation id
* @param payload
* GraphQL result
*/
final case class Data(id: String, payload: GraphQLResponse[Json])
extends FromServer
with Identifier
with Payload[GraphQLResponse[Json]]
object Data {
implicit val EqData: Eq[Data] =
Eq.by(a => (a.id, a.payload))
}
/**
* Server-provided error information for a failed GraphQL operation, previously started with a
* `Start` message with the associated `id`.
*
* @param id
* operation id
* @param payload
* error information
*/
final case class Error(id: String, payload: GraphQLErrors)
extends FromServer
with Identifier
with Payload[GraphQLErrors]
object Error {
implicit val EqError: Eq[Error] =
Eq.by(a => (a.id, a.payload))
}
/**
* Message sent to the client indicating that no more data will be forthcoming for the
* associated GraphQL operation.
*
* @param id
* operation id
*/
final case class Complete(id: String) extends FromServer with Identifier
object Complete {
implicit val EqComplete: Eq[Complete] =
Eq.by(_.id)
}
implicit val EqFromServer: Eq[FromServer] =
Eq.instance {
case (ConnectionAck, ConnectionAck) => true
case (a: ConnectionError, b: ConnectionError) => a === b
case (ConnectionKeepAlive, ConnectionKeepAlive) => true
case (a: Data, b: Data) => a === b
case (a: Error, b: Error) => a === b
case (a: Complete, b: Complete) => a === b
case _ => false
}
}
}