feat: websocket gateway

This commit is contained in:
Lucas Colombo 2024-05-27 07:36:53 -03:00
parent d2c1d95892
commit 7a8966b243
Signed by: lucas
GPG Key ID: EF34786CFEFFAE35
15 changed files with 467 additions and 296 deletions

View File

@ -11,7 +11,7 @@
".task": true, ".task": true,
".cargo": true, ".cargo": true,
// ".github": true, // ".github": true,
"rustfmt.toml": true, // "rustfmt.toml": true,
// "**/**/Cargo.toml": true, // "**/**/Cargo.toml": true,
// 📦 // 📦

273
Cargo.lock generated
View File

@ -137,46 +137,6 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
"futures-core",
]
[[package]]
name = "async-io"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
dependencies = [
"async-lock",
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-lite",
"log",
"parking",
"polling",
"rustix 0.37.27",
"slab",
"socket2 0.4.10",
"waker-fn",
]
[[package]]
name = "async-lock"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
dependencies = [
"event-listener",
]
[[package]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.5" version = "0.3.5"
@ -501,15 +461,6 @@ dependencies = [
"tokio-util", "tokio-util",
] ]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@ -689,15 +640,6 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.0.2" version = "2.0.2"
@ -803,7 +745,6 @@ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
"futures-util", "futures-util",
"num_cpus",
] ]
[[package]] [[package]]
@ -823,21 +764,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand 1.9.0",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.30" version = "0.3.30"
@ -861,18 +787,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-time"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6404853a6824881fe5f7d662d147dc4e84ecd2259ba0378f272a71dab600758a"
dependencies = [
"async-channel",
"async-io",
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.30" version = "0.3.30"
@ -937,16 +851,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "gloo-timers" name = "glob-match"
version = "0.2.6" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "h2" name = "h2"
@ -1111,7 +1019,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.6", "socket2",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@ -1200,26 +1108,6 @@ dependencies = [
"syn 2.0.60", "syn 2.0.60",
] ]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@ -1276,12 +1164,6 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.13" version = "0.4.13"
@ -1306,13 +1188,14 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "lool" name = "lool"
version = "0.2.1" version = "0.3.2"
source = "sparse+http://lugit.local/api/packages/lucodear/cargo/" source = "sparse+http://lugit.local/api/packages/lucodear/cargo/"
checksum = "79d0803f1329280a3e66cf9a0aad94d5a012514eb2072e6be9ea5a4b645ac2d7" checksum = "749ebc401b1a8a0bcc99daf6172f1f8e2ede7d6658150937922033208bbd28e4"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"chrono", "chrono",
"eyre", "eyre",
"glob-match",
"log", "log",
"num-traits", "num-traits",
"tokio", "tokio",
@ -1582,12 +1465,6 @@ dependencies = [
"syn 2.0.60", "syn 2.0.60",
] ]
[[package]]
name = "parking"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -1701,22 +1578,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "polling"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
dependencies = [
"autocfg",
"bitflags 1.3.2",
"cfg-if",
"concurrent-queue",
"libc",
"log",
"pin-project-lite",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -1914,7 +1775,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"ryu", "ryu",
"sha1_smol", "sha1_smol",
"socket2 0.5.6", "socket2",
"tokio", "tokio",
"tokio-util", "tokio-util",
"url", "url",
@ -2053,20 +1914,6 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.37.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys 0.3.8",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.34" version = "0.38.34"
@ -2076,7 +1923,7 @@ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.13", "linux-raw-sys",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -2093,11 +1940,13 @@ dependencies = [
"lool", "lool",
"prost", "prost",
"redis", "redis",
"rxrust",
"sea-orm", "sea-orm",
"sea-orm-migration", "sea-orm-migration",
"serde",
"serde_json",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"tokio-util",
"tonic", "tonic",
"tonic-build", "tonic-build",
] ]
@ -2108,21 +1957,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
[[package]]
name = "rxrust"
version = "1.0.0-beta.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9a529052da83e8897bb1f40459a0837b3c048eafabfae3bf1e7c050146b8141"
dependencies = [
"futures",
"futures-time",
"gloo-timers",
"once_cell",
"pin-project-lite",
"smallvec",
"wasm-bindgen-futures",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.17" version = "1.0.17"
@ -2334,18 +2168,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.198" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.198" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2354,9 +2188,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.116" version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -2431,16 +2265,6 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.6" version = "0.5.6"
@ -2789,8 +2613,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand 2.0.2", "fastrand",
"rustix 0.38.34", "rustix",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -2882,7 +2706,7 @@ dependencies = [
"mio", "mio",
"num_cpus", "num_cpus",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.6", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -2933,16 +2757,15 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.10" version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tracing",
] ]
[[package]] [[package]]
@ -3205,12 +3028,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "waker-fn"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -3257,18 +3074,6 @@ dependencies = [
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.92" version = "0.2.92"
@ -3298,16 +3103,6 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "web-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "1.5.1" version = "1.5.1"
@ -3318,28 +3113,6 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"

View File

@ -26,6 +26,8 @@ async-trait = "0.1.79"
tokio-tungstenite = { version = "0.21.0" } tokio-tungstenite = { version = "0.21.0" }
tonic = "0.11.0" tonic = "0.11.0"
prost = "0.12.3" # protocol buffers prost = "0.12.3" # protocol buffers
futures = "0.3.30"
tokio-util = "0.7.11"
# database # database
sea-orm = { version = "0.12.15", features = [ sea-orm = { version = "0.12.15", features = [
@ -38,11 +40,13 @@ sea-orm-migration = { version = "0.12.15", features = [
"sqlx-sqlite", "sqlx-sqlite",
] } ] }
redis = { version = "0.25.3", features = ["tokio-comp"] } redis = { version = "0.25.3", features = ["tokio-comp"] }
futures = "0.3.30"
rxrust = "1.0.0-beta.7" # other
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117"
[dependencies.lool] [dependencies.lool]
version = "^0.2.0" # crates: disable-check version = "^0.3.2" # crates: disable-check
registry = "lugit" registry = "lugit"
features = [ features = [
"cli.stylize", "cli.stylize",

View File

@ -1,16 +1,19 @@
use { use {
eyre::{set_hook, DefaultHandler, Result}, eyre::{set_hook, DefaultHandler, Result},
futures::{future, StreamExt},
rustler_core::{ rustler_core::{
bus::{self, SubscriberTrait}, bus::{self, SubscriberTrait},
rustlers::{Quote, Ticker}, rustlers::{Quote, Ticker},
}, },
rxrust::observable::{ObservableExt, ObservableItem}, tokio::sync::mpsc,
}; };
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
set_hook(Box::new(DefaultHandler::default_with))?; set_hook(Box::new(DefaultHandler::default_with))?;
let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
let mut sx = bus::redis::subscriber::<Quote, _>(&"redis://127.0.0.1/").await?; let mut sx = bus::redis::subscriber::<Quote, _>(&"redis://127.0.0.1/").await?;
let ticker = Ticker { let ticker = Ticker {
@ -19,12 +22,24 @@ async fn main() -> Result<()> {
quote_asset: None, quote_asset: None,
}; };
let _obs = sx.stream().await?.filter(move |quote| quote.belongs_to(&ticker)).subscribe(|v| { let mut stream =
println!("Received quote: {}", v); sx.stream().await?.filter(move |quote| future::ready(quote.belongs_to(&ticker)));
tokio::spawn(async move {
// cancel the streaming after 10 seconds
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
println!("Cancelling stream");
cancel_tx.send(()).await.unwrap();
}); });
// wait for 10 seconds while let Some(quote) = stream.next().await {
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; println!("Received quote: {}", quote);
if cancel_rx.try_recv().is_ok() {
break;
}
}
println!("Stream cancelled");
Ok(()) Ok(())
} }

89
examples/socket.rs Normal file
View File

@ -0,0 +1,89 @@
use {
async_trait::async_trait,
eyre::Result,
futures::{SinkExt, StreamExt},
lool::logger::{error, info, ConsoleLogger, Level},
rustler_core::{
bus::{self, SubscriberTrait},
rustlers::Quote,
socket::{self, event, Error, EventDispatcher, Outgoing, Request, Response},
},
std::sync::Arc,
tokio::{join, sync::Mutex},
};
#[derive(Clone)]
struct Dispatcher {}
#[async_trait]
impl EventDispatcher for Dispatcher {
async fn dispatch(
&self,
event: String,
data: event::Data,
outgoing: Arc<Mutex<Outgoing>>,
) -> Result<()> {
info!("Event: {}", event);
info!("Data: {:?}", data);
let mut sx = bus::redis::subscriber::<Quote, _>(&"redis://127.0.0.1/").await?;
let mut quote_feed = sx.stream().await?;
tokio::spawn(async move {
while let Some(quote) = quote_feed.next().await {
let response = serde_json::to_string(&quote).unwrap();
let mut o = outgoing.lock().await;
if let Err(e) = o.send(response.into()).await {
// if error is AlreadyClosed or ConnectionClosed, then break the loop
match e {
Error::AlreadyClosed | Error::ConnectionClosed => {
break;
}
_ => {
error!("Error sending message: {:?}", e);
}
}
}
}
info!("Hasta la vista, baby!");
});
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<()> {
ConsoleLogger::builder()
.with_level(Level::Trace)
.with_name("rustler")
.ignore("tungstenite::protocol")
.ignore("tungstenite::protocol::frame*")
.ignore("tokio_tungstenite::compat*")
.ignore("tokio_tungstenite")
.install()?;
let dispatcher = Dispatcher {};
let mut ws_server = socket::Server::new("127.0.0.1", "9002", dispatcher).await?;
let handshaker = |_res: &Request, response: Response| {
Ok(response)
// or fail the handshake, e.g. because of authentication failure
//
// let (mut parts, _) = response.into_parts();
// parts.status = StatusCode::UNAUTHORIZED;
// let res = ErrorResponse::from_parts(parts, None);
// Err(res)
//
// or
// let res = Response::builder().status(401).body(None).unwrap();
// Err(res)
};
join!(ws_server.start(handshaker));
Ok(())
}

View File

@ -1,6 +1,8 @@
use std::{convert::Infallible, fmt::Debug}; use std::{fmt::Debug, pin::Pin};
use {eyre::Result, rxrust::ops::box_it::CloneableBoxOpThreads, tonic::async_trait}; use futures::Stream;
use {eyre::Result, tonic::async_trait};
pub mod redis; pub mod redis;
@ -39,5 +41,5 @@ pub trait SubscriberTrait<RM: BusMessage> {
/// 🐎 » **stream** /// 🐎 » **stream**
/// ///
/// returns an `Observable` stream of messages from the redis bus /// returns an `Observable` stream of messages from the redis bus
async fn stream(&mut self) -> Result<CloneableBoxOpThreads<RM, Infallible>>; async fn stream(&mut self) -> Result<Pin<Box<dyn Stream<Item = RM> + Send + 'static>>>;
} }

View File

@ -1,6 +1,7 @@
use {super::BusMessage, eyre::Result, redis::Client}; use {super::BusMessage, eyre::Result, redis::Client};
pub mod publish; pub mod publish;
pub mod stream;
pub mod subscribe; pub mod subscribe;
pub(crate) const KEY_PREFIX: &str = "rustler"; pub(crate) const KEY_PREFIX: &str = "rustler";

View File

@ -0,0 +1,65 @@
use {
eyre::Result,
futures::Stream,
lool::fail,
std::{
pin::Pin,
task::{Context, Poll},
},
tokio::sync::broadcast::{self, Receiver, Sender},
};
use crate::bus::BusMessage;
pub struct SourceStream<RM: BusMessage> {
sender: Option<Sender<RM>>,
}
impl<RM: BusMessage> Default for SourceStream<RM> {
fn default() -> Self {
Self::new()
}
}
impl<RM: BusMessage> SourceStream<RM> {
// Create a new SourceStream with a broadcast channel
pub fn new() -> Self {
let (sender, _) = broadcast::channel(100); // Adjust the buffer size as needed
SourceStream {
sender: Some(sender),
}
}
pub fn sender(&self) -> Option<Sender<RM>> {
self.sender.clone()
}
// Subscribe to the stream
pub fn subscribe(&self) -> Result<Pin<Box<dyn Stream<Item = RM> + Send + 'static>>> {
if let Some(sender) = &self.sender {
let receiver = sender.subscribe();
Ok(Box::pin(BroadcastStream { receiver }))
} else {
fail!("SourceStream has been consumed")
}
}
}
// Wrapper around Receiver to implement Stream
struct BroadcastStream<RM: BusMessage> {
receiver: Receiver<RM>,
}
impl<RM: BusMessage> Stream for BroadcastStream<RM> {
type Item = RM;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
// Spawn an async task to receive the message
tokio::task::block_in_place(|| match futures::executor::block_on(this.receiver.recv()) {
Ok(msg) => Poll::Ready(Some(msg)),
Err(broadcast::error::RecvError::Closed) => Poll::Ready(None),
Err(broadcast::error::RecvError::Lagged(_)) => Poll::Pending,
})
}
}

View File

@ -1,14 +1,10 @@
use { use {
super::{key, PrefixedPubSub, RedisClient, KEY_PREFIX}, super::{key, stream::SourceStream, PrefixedPubSub, RedisClient, KEY_PREFIX},
crate::bus::{BusMessage, SubscriberTrait}, crate::bus::{BusMessage, SubscriberTrait},
eyre::Result, eyre::Result,
futures::StreamExt, futures::{Stream, StreamExt},
lool::{fail, s}, lool::{fail, s},
rxrust::{ std::pin::Pin,
observable::BoxIt, observer::Observer, ops::box_it::CloneableBoxOpThreads,
subject::SubjectThreads, subscription::Subscription,
},
std::convert::Infallible,
tonic::async_trait, tonic::async_trait,
}; };
@ -24,9 +20,9 @@ pub struct RedisSubscriber<RM: BusMessage> {
// this way we can just clone the connection when needing instead of storing the // this way we can just clone the connection when needing instead of storing the
// redis client and creating a new connection // redis client and creating a new connection
client: redis::Client, client: redis::Client,
subject: Option<SubjectThreads<RM, Infallible>>,
key_prefix: String, key_prefix: String,
pattern: String, pattern: String,
pub source_stream: Option<SourceStream<RM>>,
} }
impl<RM: BusMessage> PrefixedPubSub for RedisSubscriber<RM> { impl<RM: BusMessage> PrefixedPubSub for RedisSubscriber<RM> {
@ -50,7 +46,7 @@ impl<RM: BusMessage> RedisSubscriber<RM> {
pattern: s!("*"), pattern: s!("*"),
client: redis.get_client()?, client: redis.get_client()?,
key_prefix: s!(KEY_PREFIX), key_prefix: s!(KEY_PREFIX),
subject: None, source_stream: None,
}) })
} }
@ -67,21 +63,16 @@ impl<RM: BusMessage> RedisSubscriber<RM> {
/// subscribe to the redis feed /// subscribe to the redis feed
async fn start_streaming(&mut self) -> Result<()> { async fn start_streaming(&mut self) -> Result<()> {
if self.subject.is_none() { if self.source_stream.is_none() {
self.subject = Some(SubjectThreads::default()); self.source_stream = Some(SourceStream::new());
}
if self.subject.is_some() && self.subject.as_ref().unwrap().is_closed() {
drop(self.subject.take());
self.subject = Some(SubjectThreads::default());
} }
let pattern = self.pattern.clone(); let pattern = self.pattern.clone();
let mut conn = self.client.get_async_pubsub().await?; let mut conn = self.client.get_async_pubsub().await?;
let prefix = self.get_prefix(); let prefix = self.get_prefix();
if let Some(subject) = self.subject.as_mut() { if let Some(stream) = self.source_stream.as_mut() {
let mut stream = subject.clone(); let sender = stream.sender().unwrap();
tokio::spawn(async move { tokio::spawn(async move {
conn.psubscribe(key(prefix, pattern)).await?; conn.psubscribe(key(prefix, pattern)).await?;
@ -92,13 +83,10 @@ impl<RM: BusMessage> RedisSubscriber<RM> {
// TODO: handle possible panic when parsing message // TODO: handle possible panic when parsing message
// using catch_unwind // using catch_unwind
let message = RM::from_message(payload); let message = RM::from_message(payload);
stream.next(message); let _ = sender.send(message);
} }
} }
// if the connection with redis is closed, complete the stream
stream.complete();
Result::<()>::Ok(()) Result::<()>::Ok(())
}); });
} }
@ -109,16 +97,13 @@ impl<RM: BusMessage> RedisSubscriber<RM> {
#[async_trait] #[async_trait]
impl<RM: BusMessage> SubscriberTrait<RM> for RedisSubscriber<RM> { impl<RM: BusMessage> SubscriberTrait<RM> for RedisSubscriber<RM> {
/// 🐎 » **stream** async fn stream(&mut self) -> Result<Pin<Box<dyn Stream<Item = RM> + Send + 'static>>> {
/// if self.source_stream.is_none() {
/// returns an `Observable` stream of messages from the redis bus
async fn stream(&mut self) -> Result<CloneableBoxOpThreads<RM, Infallible>> {
if self.subject.is_none() {
self.start_streaming().await?; self.start_streaming().await?;
} }
match self.subject.as_ref() { match self.source_stream.as_ref() {
Some(subject) => Ok(subject.clone().box_it()), Some(stream) => stream.subscribe(),
None => fail!("Could not start streaming messages from redis bus"), None => fail!("Could not start streaming messages from redis bus"),
} }
} }

View File

@ -11,6 +11,7 @@ use {
chrono::{DateTime, Local}, chrono::{DateTime, Local},
eyre::Result, eyre::Result,
lool::s, lool::s,
serde::Serialize,
std::{ std::{
collections::HashMap, collections::HashMap,
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter},
@ -29,7 +30,7 @@ pub enum RustlerStatus {
} }
/// 🐎 » an enum representing the different types of market hours /// 🐎 » an enum representing the different types of market hours
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub enum MarketHourType { pub enum MarketHourType {
Pre = 0, Pre = 0,
Regular = 1, Regular = 1,
@ -57,7 +58,7 @@ impl From<u8> for MarketHourType {
/// 🐎 » a struct storing a ticker's quote at a given time, and the change in price since the last /// 🐎 » a struct storing a ticker's quote at a given time, and the change in price since the last
/// quote /// quote
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct Quote { pub struct Quote {
pub id: String, pub id: String,
pub market: String, pub market: String,

19
lib/socket/event.rs Normal file
View File

@ -0,0 +1,19 @@
use {
serde::{Deserialize, Serialize},
serde_json::{Map, Value},
};
pub type Data = Map<String, Value>;
#[derive(Serialize, Deserialize)]
pub struct WsEvent {
pub event: String,
pub data: Data,
}
#[derive(Serialize, Deserialize)]
pub struct ErrorResponse {
#[serde(rename = "errorCode")]
pub error_code: u16,
pub msg: String,
}

View File

@ -1 +1,5 @@
// TODO: websocket gateway with tokio-tungstenite pub mod event;
mod server;
pub mod stats;
pub use server::*;

171
lib/socket/server.rs Normal file
View File

@ -0,0 +1,171 @@
use {
super::{event, stats::ServerStats},
async_trait::async_trait,
eyre::Result,
futures::{stream::SplitSink, StreamExt},
lool::logger::{error, info},
std::sync::Arc,
tokio::sync::Mutex,
tokio_tungstenite::{
accept_hdr_async,
tungstenite::{handshake::server::Callback, Message},
},
};
pub use {
tokio::net::{TcpListener, TcpStream},
tokio_tungstenite::{
tungstenite::{
handshake::server::{Request, Response},
Error,
},
WebSocketStream,
},
};
pub type Outgoing = SplitSink<WebSocketStream<TcpStream>, Message>;
#[async_trait]
pub trait EventDispatcher: Send {
async fn dispatch(
&self,
event: String,
data: event::Data,
outgoing: Arc<Mutex<Outgoing>>,
) -> Result<()>;
}
/// 🐎 » **`socket::Server`**
/// --
///
/// A websocket gateway server that listens for incoming connections and dispatches events to the
/// appropriate event handlers by using the provided `EventDispatcher`.
///
/// ### Example
/// See `examples/socket.rs` for a complete example.
pub struct Server<ED>
where
ED: EventDispatcher + Clone + Send + Sync + 'static,
{
stats: Arc<ServerStats>,
listener: TcpListener,
host: String,
port: String,
event_dispatcher: ED,
}
impl<ED> Server<ED>
where
ED: EventDispatcher + Clone + Send + Sync + 'static,
{
pub async fn new(host: &str, port: &str, event_dispatcher: ED) -> std::io::Result<Self> {
let listener = TcpListener::bind(format!("{}:{}", host, port)).await?;
Ok(Self {
listener,
event_dispatcher,
host: host.to_string(),
port: port.to_string(),
stats: Arc::new(ServerStats::new()),
})
}
/// **🐎 » `start_no_cb`**: start the server
pub async fn start_no_cb(&mut self) {
let noop_cb = |_: &Request, response: Response| Ok(response);
self.start(noop_cb).await;
}
/// **🐎 » `start`**
///
/// Starts the server with a handshake callback. Usefull for customizing the
/// handshake process, e.g. checking headers, etc.
///
/// **Tip:** if you don't need to customize the handshake process, use
/// `start_no_cb` instead.
pub async fn start<HCb>(&mut self, cb: HCb)
where
HCb: Callback + Unpin + Clone,
{
info!("Started Rustler WS Server on {}:{}", self.host, self.port);
let stats = &self.stats.clone();
while let Ok((stream, peer)) = self.listener.accept().await {
let dispatcher = self.event_dispatcher.clone();
let cb = cb.clone();
info!("Incoming connection from: {}", peer);
// call the handshake callback
let ws_stream = accept_hdr_async(stream, cb).await;
if let Ok(ws_stream) = ws_stream {
stats.inc_current_clients();
let stats = stats.clone();
tokio::spawn(async move {
match Server::handle_connection(ws_stream, dispatcher).await {
Ok(_) => info!("Connection closed"),
Err(e) => error!("Error handling connection: {:?}", e),
};
// decrement client count
stats.clone().dec_current_clients();
info!("{:?}", stats);
});
}
info!("{:?}", stats);
}
}
/// subscribe to incoming messages
async fn handle_connection(
stream: WebSocketStream<TcpStream>,
event_dispatcher: ED,
) -> Result<()> {
let (outgoing, mut incoming) = stream.split();
let synced_outgoing = Arc::new(Mutex::new(outgoing));
while let Some(msg) = incoming.next().await {
Server::handle_message(msg?, &event_dispatcher, synced_outgoing.clone()).await?;
}
Ok(())
}
/// handle an incoming message
async fn handle_message(
msg: Message,
event_dispatcher: &ED,
outgoing: Arc<Mutex<Outgoing>>,
) -> Result<HandlingResult> {
if msg.is_text() || msg.is_binary() {
if let Ok(event) = serde_json::from_str::<event::WsEvent>(&msg.to_string()) {
let outgoing = Arc::clone(&outgoing);
let result = event_dispatcher.dispatch(event.event, event.data, outgoing).await;
match result {
Ok(_) => {}
Err(e) => {
error!("Error dispatching event: {:?}", e);
}
};
}
}
if msg.is_close() {
return Ok(HandlingResult::Closed);
}
// TODO: should we handle ping/pong messages?
Ok(HandlingResult::Handled)
}
}
#[derive(PartialEq)]
enum HandlingResult {
Handled,
Closed,
}

41
lib/socket/stats.rs Normal file
View File

@ -0,0 +1,41 @@
use {
core::fmt,
std::{
fmt::{Debug, Formatter},
sync::{atomic::AtomicU64, Arc},
},
};
/// `ServerStats` is a struct that holds the server statistics such as total clients and current
/// clients; only used for debugging purposes.
#[derive(Default)]
pub struct ServerStats {
total_clients: Arc<AtomicU64>,
current_clients: Arc<AtomicU64>,
}
impl ServerStats {
pub fn new() -> Self {
Self::default()
}
/// increments the total clients and current clients count.
pub fn inc_current_clients(&self) {
self.current_clients.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.total_clients.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
/// decrements the current clients count.
pub fn dec_current_clients(&self) {
self.current_clients.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
}
impl Debug for ServerStats {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ServerStats")
.field("total_clients", &self.total_clients)
.field("current_clients", &self.current_clients)
.finish()
}
}

View File

@ -1,7 +1,8 @@
max_width = 100 max_width = 100
array_width = 80 array_width = 80
chain_width = 100 chain_width = 100
comment_width = 80 comment_width = 70
imports_indent = "Block" imports_indent = "Block"
imports_granularity = "One" imports_granularity = "One"
empty_item_single_line = true empty_item_single_line = true
single_line_if_else_max_width = 70