Add: subscription + query with warp

This commit is contained in:
Efremov Alexey 2021-09-10 13:08:15 +03:00
parent 36793998a3
commit a9ba061325
6 changed files with 459 additions and 987 deletions

1211
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,15 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-web = "3" env_logger = "0.9"
env_logger = "0.8" futures = "0.3.1"
log = "0.4.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
warp = "0.3"
async-stream = "0.3"
spl-graphql-server = { path = "../program" } spl-graphql-server = { path = "../program" }
juniper = "0.15"
juniper_graphql_ws = "0.3.0"
juniper_warp = { version = "0.7.0", features = ["subscriptions"] }

View File

@ -1,29 +1,20 @@
use std::io;
use std::sync::Arc;
use std::sync::RwLock;
use std::thread; use std::thread;
use spl_graphql_server::schema::{create_schema, Ctx}; use spl_graphql_server::schema::{create_schema, Ctx};
use spl_graphql_server::server::AppServer; use spl_graphql_server::server::AppServer;
#[actix_web::main] #[tokio::main]
async fn main() -> io::Result<()> { async fn main() {
std::env::set_var("RUST_LOG", "actix_web=info"); std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init(); env_logger::init();
let context = std::sync::Arc::new(RwLock::new(Ctx::new())); let context = Ctx::new();
let mut ctx = Ctx::clone(&context);
let ctx = Arc::clone(&context); let server = AppServer::new(create_schema, context);
thread::spawn(move || { thread::spawn(move || {
match ctx.try_write() { ctx.preload();
Ok(mut c) => {
c.preload();
}
Err(_) => {}
};
}); });
let ctx = Arc::clone(&context);
let schema = std::sync::Arc::new(create_schema());
let server = AppServer::new(schema, ctx);
server.run().await server.run().await
} }

View File

@ -6,13 +6,18 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-web = "3.3.2" warp = "0.3"
actix-cors = "0.4.0"
env_logger = "0.8" env_logger = "0.8"
serde = "1.0.103" serde = "1.0.103"
futures = "0.3"
serde_json = "1.0.44" serde_json = "1.0.44"
serde_derive = "1.0.103" serde_derive = "1.0.103"
juniper = "0.15" juniper = "0.15"
juniper_graphql_ws = "0.3.0"
juniper_warp = { version = "0.7.0", features = ["subscriptions"] }
async-stream = "0.3"
solana-client = "1.7.8" solana-client = "1.7.8"
solana-program = "1.7.8" solana-program = "1.7.8"
spl-token-vault = { path = "../../token-vault/program", features = [ "no-entrypoint" ] } spl-token-vault = { path = "../../token-vault/program", features = [ "no-entrypoint" ] }

View File

@ -1,30 +1,55 @@
use solana_program::pubkey::Pubkey; use solana_program::pubkey::Pubkey;
use juniper::{FieldResult, FieldError, EmptySubscription, EmptyMutation, RootNode }; use juniper::{FieldResult, FieldError, EmptyMutation, RootNode, graphql_subscription };
use { use {
crate::state::SharedState crate::state::SharedState
}; };
use juniper::{GraphQLEnum, GraphQLObject}; use juniper::{GraphQLEnum, GraphQLObject};
use std::str::FromStr; use std::str::FromStr;
use futures::Stream;
use std::sync::{RwLock, Arc};
use std::pin::Pin;
pub struct Ctx(SharedState);
pub struct Ctx {
state: Arc<RwLock<SharedState>>
}
impl Ctx { impl Ctx {
pub fn new() -> Ctx { pub fn new() -> Ctx {
let state = SharedState::new(); let state = SharedState::new();
Ctx(state) Ctx {
state: Arc::new(RwLock::new(state))
}
} }
pub fn preload(&mut self) { pub fn clone(ctx: &Ctx) -> Ctx {
self.0.preload() let state = Arc::clone(&ctx.state);
} Ctx {
pub fn find_vault(&self, key: &str) -> Option<&spl_token_vault::state::Vault> { state: state
match Pubkey::from_str(key) {
Ok(id) => self.0.vaults.get(&id),
Err(_) => Option::None
} }
} }
pub fn preload<'a>(&'a mut self) {
if let Ok(mut state) = self.state.try_write() {
state.preload()
}
}
pub fn find_vault(&self, key: &str) -> Option<Vault> {
let res = self.state
.try_read()
.map(|st| {
match Pubkey::from_str(key) {
Ok(id) => st.vaults.get(&id).map(|v| Vault::from(v)),
Err(_) => None,
}
});
res.unwrap_or(None)
}
pub fn vaults(&self) -> Vec<Vault> { pub fn vaults(&self) -> Vec<Vault> {
self.0.vaults.values().map(|v| Vault::from(v)).collect() match self.state.try_read() {
Ok(state) => state.vaults.values().map(|v| Vault::from(v)).collect(),
Err(_) => Vec::new(),
}
} }
} }
@ -129,7 +154,7 @@ pub struct QueryRoot;
impl QueryRoot { impl QueryRoot {
/// get vault by id /// get vault by id
fn vault(context: &Ctx, id: String) -> FieldResult<Vault> { fn vault(context: &Ctx, id: String) -> FieldResult<Vault> {
let result = context.find_vault(&id).map(|v| Vault::from(v)); let result = context.find_vault(&id);
if let Some(v) = result { if let Some(v) = result {
Ok(v) Ok(v)
} else { } else {
@ -143,8 +168,21 @@ impl QueryRoot {
} }
} }
pub type Schema = RootNode<'static, QueryRoot, EmptyMutation<Ctx>, EmptySubscription<Ctx>>; type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
pub struct Subscription;
#[graphql_subscription(context = Ctx)]
impl Subscription {
async fn hello_world() -> StringStream {
let stream = futures::stream::iter(vec![
Ok(String::from("Hello")),
Ok(String::from("World!"))
]);
Box::pin(stream)
}
}
pub type Schema = RootNode<'static, QueryRoot, EmptyMutation<Ctx>, Subscription>;
pub fn create_schema() -> Schema { pub fn create_schema() -> Schema {
Schema::new(QueryRoot {}, EmptyMutation::new(), EmptySubscription::new()) Schema::new(QueryRoot {}, EmptyMutation::new(), Subscription {})
} }

View File

@ -1,79 +1,69 @@
use std::{ collections::HashMap, convert::Infallible };
use juniper_warp::subscriptions::serve_graphql_ws;
use actix_cors::Cors; use juniper_graphql_ws::ConnectionConfig;
use actix_web::{middleware, web, App, HttpResponse, HttpServer}; use juniper_warp::{playground_filter};
use juniper::http::graphiql::graphiql_source; use futures::FutureExt;
use juniper::http::GraphQLRequest; use warp::Filter;
use juniper::InputValue;
use std::sync::Arc; use std::sync::Arc;
use std::sync::RwLock;
use std::io;
use crate::schema::{Schema, Ctx}; use crate::schema::{Schema, Ctx};
pub struct AppServer { pub struct AppServer {
schema: Arc<Schema>, create_schema: Box<fn() -> Schema>,
context: Arc<RwLock<Ctx>> context: Ctx
}
async fn graphiql() -> HttpResponse {
let html = graphiql_source("http://127.0.0.1:8080/graphql", None);
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html)
}
async fn graphql(
st: web::Data<Arc<Schema>>,
ctx: web::Data<Arc<RwLock<Ctx>>>,
data: web::Json<GraphQLRequest>,
) -> Result<HttpResponse, actix_web::Error> {
let user = web::block(move || {
match ctx.get_ref().try_read() {
Ok(context) => {
let res = data.execute_sync(&st, &context);
let json = serde_json::to_string(&res)?;
return Ok::<_, serde_json::error::Error>(json);
}
Err(e) => {
let json_str = format!("{{\"error\":\"{}\"}}", e.to_string());
let json = serde_json::to_string(&json_str)?;
return Ok::<_, serde_json::error::Error>(json);
}
}
}).await?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(user))
} }
impl AppServer { impl AppServer {
pub fn new(schema: Arc<Schema>, context: Arc<RwLock<Ctx>>) -> Self { pub fn new(create_schema: fn() -> Schema, context: Ctx) -> Self {
AppServer { AppServer {
schema: schema, create_schema: Box::new(create_schema),
context: context context: context
} }
} }
pub async fn run(self) -> io::Result<()> { pub async fn run(self) {
let schema = self.schema; let qm_schema = (*self.create_schema)();
let context = self.context; let base_context = Arc::new(self.context);
// Start http server
HttpServer::new(move || { let context = Arc::clone(&base_context);
App::new() let qm_state = warp::any().map(move || {
.data(schema.clone()) return Ctx::clone(&context);
.data(context.clone()) });
.wrap(middleware::Logger::default()) let qm_graphql_filter = juniper_warp::make_graphql_filter(qm_schema, qm_state.boxed());
.wrap( let root_node = Arc::new((*self.create_schema)());
Cors::new() let log = warp::log("warp_subscriptions");
.allowed_methods(vec!["POST", "GET"]) let context = Arc::clone(&base_context);
.supports_credentials() let routes = (warp::path("subscriptions")
.max_age(3600) .and(warp::ws())
.finish(), .map(move |ws: warp::ws::Ws| {
) let root_node = Arc::clone(&root_node);
.service(web::resource("/graphql").route(web::post().to(graphql))) let context = Arc::clone(&context);
.service(web::resource("/graphiql").route(web::get().to(graphiql)))
let ctx = Ctx::clone(&context);
ws.on_upgrade(move |websocket| async move {
let connection_config = move |_: HashMap<String, InputValue>| async move {
Ok(ConnectionConfig::new(ctx)) as Result<_, Infallible>
};
serve_graphql_ws(websocket, root_node, connection_config)
.map(|r| {
if let Err(e) = r {
println!("Websocket error: {}", e);
}
})
.await
})
}))
.map(|reply| {
// TODO#584: remove this workaround
warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
}) })
.bind("127.0.0.1:8080")? .or(warp::post()
.run() .and(warp::path("graphql"))
.await .and(qm_graphql_filter))
} .or(warp::get()
.and(warp::path("playground"))
.and(playground_filter("/graphql", Some("/subscriptions"))))
.with(log);
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
}
} }