Skip to content

Commit

Permalink
refactor(api): Maintain cache between screens (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
mertwole committed Aug 18, 2024
1 parent 86d7d91 commit 751d69d
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 227 deletions.
93 changes: 51 additions & 42 deletions app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ use ratatui::{

use crate::{
api::{
blockchain_monitoring::mock::BlockchainMonitoringApiMock,
blockchain_monitoring::{mock::BlockchainMonitoringApiMock, BlockchainMonitoringApiT},
cache_utils::ModePlan,
coin_price::{cache::Cache as CoinPriceApiCache, mock::CoinPriceApiMock, CoinPriceApi},
coin_price::{
cache::Cache as CoinPriceApiCache, mock::CoinPriceApiMock, CoinPriceApi, CoinPriceApiT,
},
common::{Account, Network},
ledger::{cache::Cache as LedgerApiCache, mock::LedgerApiMock, Device, DeviceInfo},
},
screen::{
asset::Model as AssetScreen, deposit::Model as DepositScreen,
device_selection::Model as DeviceSelectionScreen, portfolio::Model as PortfolioScreen,
OutgoingMessage, Screen, ScreenName,
ledger::{
cache::Cache as LedgerApiCache, mock::LedgerApiMock, Device, DeviceInfo, LedgerApiT,
},
},
screen::{OutgoingMessage, Screen, ScreenName},
};

pub struct App {
Expand All @@ -38,6 +38,18 @@ pub(crate) struct StateRegistry {
_phantom: PhantomData<()>,
}

pub(crate) struct ApiRegistry<L, C, M>
where
L: LedgerApiT,
C: CoinPriceApiT,
M: BlockchainMonitoringApiT,
{
pub ledger_api: L,
pub coin_price_api: C,
pub blockchain_monitoring_api: M,
_phantom: PhantomData<()>,
}

impl StateRegistry {
fn new() -> StateRegistry {
StateRegistry {
Expand Down Expand Up @@ -71,15 +83,39 @@ impl App {
async fn main_loop<B: Backend>(&mut self, mut terminal: Terminal<B>) {
let mut state = Some(StateRegistry::new());

let api_registry = {
let ledger_api = LedgerApiMock::new(10, 3);
let mut ledger_api = block_on(LedgerApiCache::new(ledger_api));
ledger_api.set_all_modes(ModePlan::Transparent);

let _coin_price_api = CoinPriceApiMock::new();
let coin_price_api = CoinPriceApi::new("https://data-api.binance.vision");
let mut coin_price_api = block_on(CoinPriceApiCache::new(coin_price_api));
coin_price_api.set_all_modes(ModePlan::TimedOut(Duration::from_secs(5)));

let blockchain_monitoring_api = BlockchainMonitoringApiMock::new(4);

ApiRegistry {
ledger_api,
coin_price_api,
blockchain_monitoring_api,
_phantom: PhantomData,
}
};

let mut api_registry = Some(api_registry);

loop {
let screen = self
.screens
.last()
.expect("At least one screen should be present");
let screen = create_screen(*screen);

let (new_state, msg) = Self::screen_loop(screen, &mut terminal, state.take().unwrap());
let screen = Screen::new(*screen, state.take().unwrap(), api_registry.take().unwrap());

let (new_state, new_api_registry, msg) = Self::screen_loop(screen, &mut terminal);
state = Some(new_state);
api_registry = Some(new_api_registry);

match msg {
OutgoingMessage::Exit => {
Expand All @@ -97,13 +133,10 @@ impl App {
}
}

fn screen_loop<B: Backend>(
mut screen: Box<dyn Screen>,
fn screen_loop<B: Backend, L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
mut screen: Screen<L, C, M>,
terminal: &mut Terminal<B>,
state: StateRegistry,
) -> (StateRegistry, OutgoingMessage) {
screen.construct(state);

) -> (StateRegistry, ApiRegistry<L, C, M>, OutgoingMessage) {
loop {
terminal.draw(|frame| screen.render(frame)).unwrap();

Expand All @@ -114,33 +147,9 @@ impl App {
let msg = screen.tick(event);

if let Some(msg) = msg {
let state = screen.deconstruct();
return (state, msg);
let (state, api_registry) = screen.deconstruct();
return (state, api_registry, msg);
}
}
}
}

fn create_screen(screen: ScreenName) -> Box<dyn Screen> {
let ledger_api = LedgerApiMock::new(10, 3);
let mut ledger_api = block_on(LedgerApiCache::new(ledger_api));
ledger_api.set_all_modes(ModePlan::Transparent);

let _coin_price_api = CoinPriceApiMock::new();
let coin_price_api = CoinPriceApi::new("https://data-api.binance.vision");
let mut coin_price_api = block_on(CoinPriceApiCache::new(coin_price_api));
coin_price_api.set_all_modes(ModePlan::TimedOut(Duration::from_secs(5)));

let blockchain_monitoring_api = BlockchainMonitoringApiMock::new(4);

match screen {
ScreenName::Portfolio => Box::from(PortfolioScreen::new(
ledger_api,
coin_price_api,
blockchain_monitoring_api,
)),
ScreenName::DeviceSelection => Box::from(DeviceSelectionScreen::new(ledger_api)),
ScreenName::Asset => Box::from(AssetScreen::new(coin_price_api, blockchain_monitoring_api)),
ScreenName::Deposit => Box::from(DepositScreen::new()),
}
}
13 changes: 8 additions & 5 deletions app/src/screen/asset/controller.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use ratatui::crossterm::event::{Event, KeyCode};

use crate::{
api::{blockchain_monitoring::BlockchainMonitoringApiT, coin_price::CoinPriceApiT},
api::{
blockchain_monitoring::BlockchainMonitoringApiT, coin_price::CoinPriceApiT,
ledger::LedgerApiT,
},
screen::{EventExt, OutgoingMessage, ScreenName},
};

use super::{Model, TimePeriod};

pub(super) fn process_input<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
pub(super) fn process_input<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
event: &Event,
model: &mut Model<C, M>,
model: &mut Model<L, C, M>,
) -> Option<OutgoingMessage> {
if event.is_key_pressed(KeyCode::Char('q')) {
return Some(OutgoingMessage::Exit);
Expand All @@ -28,9 +31,9 @@ pub(super) fn process_input<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
None
}

fn process_time_interval_selection<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
fn process_time_interval_selection<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
event: &Event,
model: &mut Model<C, M>,
model: &mut Model<L, C, M>,
) {
match () {
() if event.is_key_pressed(KeyCode::Char('d')) => {
Expand Down
62 changes: 27 additions & 35 deletions app/src/screen/asset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,29 @@ use ratatui::{crossterm::event::Event, Frame};
use rust_decimal::Decimal;
use strum::EnumIter;

use super::{OutgoingMessage, Screen};
use super::{OutgoingMessage, ScreenT};
use crate::{
api::{
blockchain_monitoring::{BlockchainMonitoringApiT, TransactionInfo, TransactionUid},
coin_price::{Coin, CoinPriceApiT, TimePeriod as ApiTimePeriod},
common::Network,
ledger::LedgerApiT,
},
app::StateRegistry,
app::{ApiRegistry, StateRegistry},
};

mod controller;
mod view;

const DEFAULT_SELECTED_TIME_PERIOD: TimePeriod = TimePeriod::Day;

pub struct Model<C: CoinPriceApiT, M: BlockchainMonitoringApiT> {
coin_price_api: C,
blockchain_monitoring_api: M,

pub struct Model<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT> {
coin_price_history: Option<Vec<PriceHistoryPoint>>,
transactions: Option<Vec<(TransactionUid, TransactionInfo)>>,
selected_time_period: TimePeriod,

state: Option<StateRegistry>,
state: StateRegistry,
apis: ApiRegistry<L, C, M>,
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
Expand All @@ -40,27 +39,10 @@ enum TimePeriod {

type PriceHistoryPoint = Decimal;

impl<C: CoinPriceApiT, M: BlockchainMonitoringApiT> Model<C, M> {
pub fn new(coin_price_api: C, blockchain_monitoring_api: M) -> Self {
Self {
coin_price_api,
blockchain_monitoring_api,

coin_price_history: Default::default(),
transactions: Default::default(),
selected_time_period: DEFAULT_SELECTED_TIME_PERIOD,

state: None,
}
}

impl<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT> Model<L, C, M> {
fn tick_logic(&mut self) {
let state = self
let (selected_network, selected_account) = self
.state
.as_ref()
.expect("Construct should be called at the start of window lifetime");

let (selected_network, selected_account) = state
.selected_account
.as_ref()
.expect("Selected account should be present in state"); // TODO: Enforce this rule at `app` level?
Expand All @@ -78,15 +60,16 @@ impl<C: CoinPriceApiT, M: BlockchainMonitoringApiT> Model<C, M> {
TimePeriod::All => ApiTimePeriod::All,
};

self.coin_price_history = block_on(self.coin_price_api.get_price_history(
self.coin_price_history = block_on(self.apis.coin_price_api.get_price_history(
coin,
Coin::USDT,
time_period,
));

// TODO: Don't make requests to API each tick.
let tx_list = block_on(
self.blockchain_monitoring_api
self.apis
.blockchain_monitoring_api
.get_transactions(*selected_network, selected_account),
);
let txs = tx_list
Expand All @@ -95,7 +78,8 @@ impl<C: CoinPriceApiT, M: BlockchainMonitoringApiT> Model<C, M> {
(
tx_uid.clone(),
block_on(
self.blockchain_monitoring_api
self.apis
.blockchain_monitoring_api
.get_transaction_info(*selected_network, &tx_uid),
),
)
Expand All @@ -106,9 +90,18 @@ impl<C: CoinPriceApiT, M: BlockchainMonitoringApiT> Model<C, M> {
}
}

impl<C: CoinPriceApiT, M: BlockchainMonitoringApiT> Screen for Model<C, M> {
fn construct(&mut self, state: StateRegistry) {
self.state = Some(state);
impl<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT> ScreenT<L, C, M>
for Model<L, C, M>
{
fn construct(state: StateRegistry, api_registry: ApiRegistry<L, C, M>) -> Self {
Self {
coin_price_history: Default::default(),
transactions: Default::default(),
selected_time_period: DEFAULT_SELECTED_TIME_PERIOD,

state,
apis: api_registry,
}
}

fn render(&self, frame: &mut Frame<'_>) {
Expand All @@ -121,8 +114,7 @@ impl<C: CoinPriceApiT, M: BlockchainMonitoringApiT> Screen for Model<C, M> {
controller::process_input(event.as_ref()?, self)
}

fn deconstruct(self: Box<Self>) -> StateRegistry {
self.state
.expect("Construct should be called at the start of window lifetime")
fn deconstruct(self) -> (StateRegistry, ApiRegistry<L, C, M>) {
(self.state, self.apis)
}
}
19 changes: 8 additions & 11 deletions app/src/screen/asset/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ use crate::{
api::{
blockchain_monitoring::{BlockchainMonitoringApiT, TransactionType},
coin_price::CoinPriceApiT,
ledger::LedgerApiT,
},
screen::common::network_symbol,
};

use super::{Model, TimePeriod};

pub(super) fn render<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<C, M>,
pub(super) fn render<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<L, C, M>,
frame: &mut Frame<'_>,
) {
let area = frame.size();
Expand All @@ -45,8 +46,8 @@ pub(super) fn render<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
render_tx_list(model, frame, inner_txs_list_area);
}

fn render_price_chart<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<C, M>,
fn render_price_chart<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<L, C, M>,
frame: &mut Frame<'_>,
area: Rect,
) {
Expand Down Expand Up @@ -106,8 +107,8 @@ fn render_price_chart<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
frame.render_widget(chart, area);
}

fn render_tx_list<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<C, M>,
fn render_tx_list<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
model: &Model<L, C, M>,
frame: &mut Frame<'_>,
area: Rect,
) {
Expand All @@ -121,12 +122,8 @@ fn render_tx_list<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
return;
}

let state = model
let (selected_account_network, selected_account) = model
.state
.as_ref()
.expect("Construct should be called at the start of window lifetime");

let (selected_account_network, selected_account) = state
.selected_account
.as_ref()
.expect("Selected account should be present in state"); // TODO: Enforce this rule at `app` level?
Expand Down
21 changes: 13 additions & 8 deletions app/src/screen/deposit/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@ use copypasta::{ClipboardContext, ClipboardProvider};
use ratatui::crossterm::event::{Event, KeyCode};

use super::Model;
use crate::screen::{EventExt, OutgoingMessage};

pub(super) fn process_input(event: &Event, model: &mut Model) -> Option<OutgoingMessage> {
use crate::{
api::{
blockchain_monitoring::BlockchainMonitoringApiT, coin_price::CoinPriceApiT,
ledger::LedgerApiT,
},
screen::{EventExt, OutgoingMessage},
};

pub(super) fn process_input<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
event: &Event,
model: &mut Model<L, C, M>,
) -> Option<OutgoingMessage> {
if event.is_key_pressed(KeyCode::Char('q')) {
return Some(OutgoingMessage::Exit);
}
Expand All @@ -18,12 +27,8 @@ pub(super) fn process_input(event: &Event, model: &mut Model) -> Option<Outgoing
if event.is_key_pressed(KeyCode::Char('c')) {
model.last_address_copy = Some(Instant::now());

let state = model
let pubkey = model
.state
.as_ref()
.expect("Construct should be called at the start of window lifetime");

let pubkey = state
.selected_account
.as_ref()
.expect("Selected account should be present in state") // TODO: Enforce this rule at `app` level?
Expand Down
Loading

0 comments on commit 751d69d

Please sign in to comment.