///|
/// A `connection_init` frame carrying the connection's parameters.
fn init_frame(params : Json) -> @moonasgi.WsMessage {
let m : Json = { "type": "connection_init", "payload": params }
@ws.Text(m.stringify())
}
///|
/// A `subscribe` frame `{ id, type, payload: { query } }` as a text message.
fn subscribe_frame(id : String, query : String) -> @moonasgi.WsMessage {
let payload : Json = { "query": Json(query) }
let m : Json = { "id": Json(id), "type": "subscribe", "payload": payload }
@ws.Text(m.stringify())
}
///|
/// The text bodies of the `SendText` frames a handler emits, in order.
fn sent_texts(sends : Array[@moonasgi.WsSend]) -> Array[String] {
let out : Array[String] = []
for s in sends {
match s {
@ws.SendText(t) => out.push(t)
_ => ()
}
}
out
}
///|
/// The close code in a reply, or -1 when it did not close.
fn close_code(sends : Array[@moonasgi.WsSend]) -> Int32 {
for s in sends {
if s is @ws.Close(code~, ..) {
return code
}
}
-1
}
///|
/// Subscriptions over the `graphql-transport-ws` protocol. `GqlWs` is one
/// connection's protocol state and `recv` maps a client frame to the frames sent
/// back — the synchronous seam under the async socket. A `connection_init` is
/// answered with `connection_ack` and its payload reaches every resolver's context
/// under `connectionParams`; a `subscribe` streams one `next` per source event and
/// closes with `complete`, which frees its id for the next operation.
///
/// The protocol's two timers are state here, not threads: `tick` takes a
/// millisecond clock reading from the server and hands back the 4408 close for a
/// `connection_init` that never arrived, or a keep-alive `ping`. `graphql_ws_handler`
/// wraps a connection whose timers nothing drives.
///
/// moon run src/examples/23-graphql-ws
#coverage.skip
fn main {
let s = @moongql.Schema::new()
let q = s.object("Query")
q.field("ok", NonNull(Scalar("String")))
s.set_subscription("Subscription")
let sub = s.object("Subscription")
sub.field("ticks", NonNull(Scalar("Int")))
let subs = @moongql.Subscribers::new()
subs.field("Subscription", "ticks", _info => [Json(1), Json(2)])
let r = @moongql.Resolvers::new()
r.field("Query", "ok", info => {
match info.ctx {
Object(m) =>
match m.get("connectionParams") {
Some(p) => p
None => Json("no params")
}
_ => Json("no context")
}
})
let ws = @moongql.GqlWs::new(s, r, subs)
let params : Json = { "token": "heke1228" }
for t in sent_texts(ws.recv(init_frame(params))) {
println("ack: " + t)
}
for t in sent_texts(ws.recv(subscribe_frame("1", "subscription { ticks }"))) {
println("frame: " + t)
}
// The id was released with the `complete` above, so the client may reuse it.
println(
"id reusable: \{close_code(ws.recv(subscribe_frame("1", "subscription { ticks }"))) == -1}",
)
for t in sent_texts(ws.recv(subscribe_frame("2", "{ ok }"))) {
println("params: " + t)
}
ws.tick(0L) |> ignore
for t in sent_texts(ws.tick(12000L)) {
println("keep-alive: " + t)
}
let silent = @moongql.GqlWs::new(s, r, subs, init_timeout=3000)
silent.tick(0L) |> ignore
println("no init after 3s: \{close_code(silent.tick(3000L))}")
}