use std::sync::Arc;
use dashmap::{DashMap, mapref::entry::Entry};
use ustr::Ustr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TradeStreamUse {
Ticks,
PublicTrades,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct TradeStreamUses {
pub ticks: bool,
pub public_trades: bool,
}
impl TradeStreamUses {
pub(crate) fn is_empty(self) -> bool {
!self.ticks && !self.public_trades
}
fn set(&mut self, stream_use: TradeStreamUse, active: bool) {
match stream_use {
TradeStreamUse::Ticks => self.ticks = active,
TradeStreamUse::PublicTrades => self.public_trades = active,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TradeStreamRegistration {
pub subscribe: bool,
pub uses: TradeStreamUses,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TradeStreamRelease {
pub unsubscribe: bool,
pub uses: TradeStreamUses,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct TradeStreamRegistry {
streams: Arc<DashMap<Ustr, TradeStreamUses>>,
}
impl TradeStreamRegistry {
pub(crate) fn register(
&self,
coin: Ustr,
stream_use: TradeStreamUse,
) -> TradeStreamRegistration {
match self.streams.entry(coin) {
Entry::Occupied(mut occupied) => {
let uses = occupied.get_mut();
uses.set(stream_use, true);
TradeStreamRegistration {
subscribe: false,
uses: *uses,
}
}
Entry::Vacant(vacant) => {
let mut uses = TradeStreamUses::default();
uses.set(stream_use, true);
vacant.insert(uses);
TradeStreamRegistration {
subscribe: true,
uses,
}
}
}
}
pub(crate) fn release(&self, coin: &Ustr, stream_use: TradeStreamUse) -> TradeStreamRelease {
match self.streams.entry(*coin) {
Entry::Occupied(mut occupied) => {
let uses = occupied.get_mut();
uses.set(stream_use, false);
let uses = *uses;
if uses.is_empty() {
occupied.remove();
}
TradeStreamRelease {
unsubscribe: uses.is_empty(),
uses,
}
}
Entry::Vacant(_) => TradeStreamRelease {
unsubscribe: false,
uses: TradeStreamUses::default(),
},
}
}
pub(crate) fn snapshot(&self) -> Vec<(Ustr, TradeStreamUses)> {
self.streams
.iter()
.map(|entry| (*entry.key(), *entry.value()))
.collect()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn logical_uses_share_one_venue_subscription() {
let registry = TradeStreamRegistry::default();
let coin = Ustr::from("BTC");
assert!(registry.register(coin, TradeStreamUse::Ticks).subscribe);
let registration = registry.register(coin, TradeStreamUse::PublicTrades);
assert!(!registration.subscribe);
assert_eq!(
registration.uses,
TradeStreamUses {
ticks: true,
public_trades: true,
}
);
}
#[rstest]
fn duplicate_logical_use_is_idempotent() {
let registry = TradeStreamRegistry::default();
let coin = Ustr::from("BTC");
assert!(
registry
.register(coin, TradeStreamUse::PublicTrades)
.subscribe
);
assert!(
!registry
.register(coin, TradeStreamUse::PublicTrades)
.subscribe
);
assert!(
registry
.release(&coin, TradeStreamUse::PublicTrades)
.unsubscribe
);
}
#[rstest]
fn releasing_one_use_retains_the_other() {
let registry = TradeStreamRegistry::default();
let coin = Ustr::from("BTC");
registry.register(coin, TradeStreamUse::Ticks);
registry.register(coin, TradeStreamUse::PublicTrades);
let release = registry.release(&coin, TradeStreamUse::Ticks);
assert!(!release.unsubscribe);
assert!(release.uses.public_trades);
let release = registry.release(&coin, TradeStreamUse::PublicTrades);
assert!(release.unsubscribe);
assert!(release.uses.is_empty());
}
}