Skip to content

Commit

Permalink
Cache retrieved flakes on disk (#101)
Browse files Browse the repository at this point in the history
- Update dioxus-std PR
- nix_rs: Avoid serde untagged on exposed types
  • Loading branch information
srid authored Nov 8, 2023
1 parent 4b39d85 commit 90ffe9c
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 56 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 46 additions & 15 deletions crates/nix_rs/src/flake/outputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,21 @@ use std::{

/// Represents the "outputs" of a flake
///
/// This structure is currently produced by `nix flake show`
/// This structure is currently produced by `nix flake show`, thus to parse it we must toggle serde untagged.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FlakeOutputs {
Val(Val),
Attrset(BTreeMap<String, FlakeOutputs>),
}

impl FlakeOutputs {
/// Run `nix flake show` on the given flake url
#[tracing::instrument(name = "flake-show")]
pub async fn from_nix(
nix_cmd: &crate::command::NixCmd,
flake_url: &super::url::FlakeUrl,
) -> Result<Self, crate::command::NixCmdError> {
let v = nix_cmd
.run_with_args_expecting_json(&[
"flake",
"show",
"--legacy", // for showing nixpkgs legacyPackages
"--allow-import-from-derivation",
"--json",
&flake_url.to_string(),
])
.await?;
Ok(v)
let v = FlakeOutputsUntagged::from_nix(nix_cmd, flake_url).await?;
Ok(v.into_flake_outputs())
}

/// Get the non-attrset value
Expand Down Expand Up @@ -122,3 +110,46 @@ impl Display for Type {
f.write_str(&format!("{:?}", self))
}
}

/// This type is identical to [FlakeOutputs] except for the serde untagged attribute, which enables parsing the JSON output of `nix flake show`.
///
/// This separation exists to workaround https://github.com/DioxusLabs/dioxus-std/issues/20
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
enum FlakeOutputsUntagged {
UVal(Val),
UAttrset(BTreeMap<String, FlakeOutputsUntagged>),
}

impl FlakeOutputsUntagged {
/// Run `nix flake show` on the given flake url
#[tracing::instrument(name = "flake-show")]
async fn from_nix(
nix_cmd: &crate::command::NixCmd,
flake_url: &super::url::FlakeUrl,
) -> Result<Self, crate::command::NixCmdError> {
let v = nix_cmd
.run_with_args_expecting_json(&[
"flake",
"show",
"--legacy", // for showing nixpkgs legacyPackages
"--allow-import-from-derivation",
"--json",
&flake_url.to_string(),
])
.await?;
Ok(v)
}

/// Convert to [FlakeOutputs]
fn into_flake_outputs(self) -> FlakeOutputs {
match self {
Self::UVal(v) => FlakeOutputs::Val(v),
Self::UAttrset(v) => FlakeOutputs::Attrset(
v.into_iter()
.map(|(k, v)| (k, v.into_flake_outputs()))
.collect(),
),
}
}
}
6 changes: 3 additions & 3 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,16 @@ fn Dashboard(cx: Scope) -> Element {
p { "TODO: search input" }
h2 { class: "text-2xl", "Or, try one of these:" }
div { class: "flex flex-col",
for flake in state.recent_flakes.read().clone() {
for flake_url in state.flake_cache.read().recent_flakes() {
a {
onclick: move |_| {
let state = AppState::use_state(cx);
let nav = use_navigator(cx);
state.set_flake_url(flake.clone());
state.set_flake_url(flake_url.clone());
nav.replace(Route::Flake {});
},
class: "cursor-pointer text-primary-600 underline hover:no-underline",
"{flake.clone()}"
"{flake_url.clone()}"
}
}
}
Expand Down
10 changes: 6 additions & 4 deletions src/app/state/datum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ impl<T> Datum<T> {
/// Refresh the datum [Signal] using the given function
///
/// If a previous refresh is still running, it will be cancelled.
pub async fn refresh_with<F>(signal: Signal<Datum<T>>, f: F)
pub async fn refresh_with<F>(signal: Signal<Datum<T>>, f: F) -> Option<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
T: Send + Clone + 'static,
{
// Cancel existing fetcher if any.
signal.with_mut(move |x| {
Expand All @@ -65,11 +65,12 @@ impl<T> Datum<T> {
// Wait for result and update the signal state.
match join_handle.await {
Ok(val) => {
signal.with_mut(move |x| {
signal.with_mut(|x| {
tracing::debug!("🍒 Setting {} datum value", std::any::type_name::<T>());
x.value = Some(val);
x.value = Some(val.clone());
*x.task.write() = None;
});
Some(val)
}
Err(err) => {
if !err.is_cancelled() {
Expand All @@ -80,6 +81,7 @@ impl<T> Datum<T> {
}
// x.task will be set to None by the caller who cancelled us, so
// we need not do anything here.
None
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions src/app/state/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! A database of [Flake] intended to be cached in dioxus [Signal] and persisted to disk.
//!
//! This is purposefully dumb right now, but we might revisit this in future based on actual performance.
use dioxus::prelude::Scope;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, time::SystemTime};

use dioxus_signals::Signal;
use dioxus_std::storage::new_storage;
use dioxus_std::storage::LocalStorage;

use crate::app::state::FlakeUrl;
use nix_rs::flake::Flake;

/// A database of [Flake] intended to be cached in dioxus [Signal] and persisted to disk.
///
/// Contains the "last fetched" time and the [Flake] itself.
#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FlakeCache(HashMap<FlakeUrl, Option<(SystemTime, Flake)>>);

impl FlakeCache {
/// Create a new [Signal] for [FlakeCache] from [LocalStorage].
pub fn new_signal(cx: Scope) -> Signal<FlakeCache> {
new_storage::<LocalStorage, _>(cx, "flake_cache".to_string(), || {
tracing::warn!("📦 No flake cache found");
let init = FlakeUrl::suggestions()
.into_iter()
.map(|url| (url, None))
.collect();
FlakeCache(init)
})
}

/// Look up a [Flake] by [FlakeUrl] in the cache.
pub fn get(&self, k: &FlakeUrl) -> Option<Flake> {
let (t, flake) = self.0.get(k).and_then(|v| v.as_ref().cloned())?;
tracing::info!("Cache hit for {} (updated: {:?})", k, t);
Some(flake)
}

/// Update the cache with a new [Flake].
pub fn update(&mut self, k: FlakeUrl, flake: Flake) {
tracing::info!("Caching flake [{}]", &k);
self.0.insert(k, Some((SystemTime::now(), flake)));
}

/// Recently updated flakes, along with any unavailable flakes in cache.
pub fn recent_flakes(&self) -> Vec<FlakeUrl> {
let mut pairs: Vec<_> = self
.0
.iter()
.filter_map(|(k, v)| v.as_ref().map(|(t, _)| (k, t)))
.collect();

// Sort by the timestamp in descending order.
pairs.sort_unstable_by(|a, b| b.1.cmp(a.1));

pairs.into_iter().map(|(k, _)| k.clone()).collect()
}
}
59 changes: 26 additions & 33 deletions src/app/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
pub mod action;
mod datum;
mod db;
mod error;

use dioxus::prelude::{use_context, use_context_provider, use_future, Scope};
use dioxus_signals::Signal;
use dioxus_std::storage::{new_storage, LocalStorage};
use nix_health::NixHealth;
use nix_rs::{
flake::{url::FlakeUrl, Flake},
Expand All @@ -30,20 +30,22 @@ pub struct AppState {
pub flake_url: Signal<Option<FlakeUrl>>,
/// [Flake] for [AppState::flake_url]
pub flake: Signal<Datum<Result<Flake, SystemError>>>,
/// List of recently selected [AppState::flake_url]s
pub recent_flakes: Signal<Vec<FlakeUrl>>,
/// Cached [Flake] values indexed by [FlakeUrl]
///
/// Most recently updated flakes appear first.
pub flake_cache: Signal<db::FlakeCache>,

/// [Action] represents the next modification to perform on [AppState] signals
pub action: Signal<(usize, Action)>,
}

impl AppState {
fn new(cx: Scope) -> Self {
tracing::debug!("🔨 Creating new AppState");
let recent_flakes =
new_storage::<LocalStorage, _>(cx, "recent_flakes".to_string(), FlakeUrl::suggestions);
tracing::info!("🔨 Creating new AppState");
// TODO: Should we use new_synced_storage, instead? To allow multiple app windows?
let flake_cache = db::FlakeCache::new_signal(cx);
AppState {
recent_flakes,
flake_cache,
..AppState::default()
}
}
Expand Down Expand Up @@ -97,35 +99,37 @@ impl AppState {
let update_flake = |refresh: bool| async move {
let flake_url = self.flake_url.read().clone();
if let Some(flake_url) = flake_url {
tracing::info!("Updating flake [{}] refresh={} ...", flake_url, refresh);
Datum::refresh_with(self.flake, async move {
Flake::from_nix(&nix_rs::command::NixCmd::default(), flake_url.clone())
let flake_url_2 = flake_url.clone();
tracing::info!("Updating flake [{}] refresh={} ...", &flake_url, refresh);
let res = Datum::refresh_with(self.flake, async move {
Flake::from_nix(&nix_rs::command::NixCmd::default(), flake_url_2)
.await
.map_err(|e| Into::<SystemError>::into(e.to_string()))
})
.await
.await;
if let Some(Ok(flake)) = res {
self.flake_cache.with_mut(|cache| {
cache.update(flake_url, flake);
});
}
}
};
let flake_url = self.flake_url.read().clone();
let refresh_action =
Action::signal_for(cx, self.action, |act| act == Action::RefreshFlake);
let idx = *refresh_action.read();
// ... when URL changes.
use_future(cx, (&flake_url,), |_| update_flake(false));
// ... when refresh button is clicked.
use_future(cx, (&idx,), |(idx,)| update_flake(idx.is_some()));
}

// Update recent_flakes
{
let flake_url = self.flake_url.read().clone();
use_future(cx, (&flake_url,), |(flake_url,)| async move {
if let Some(flake_url) = flake_url {
self.recent_flakes.with_mut(|items| {
vec_push_as_latest(items, flake_url).truncate(8);
});
if let Some(cached_flake) = self.flake_cache.read().get(&flake_url) {
Datum::refresh_with(self.flake, async { Ok(cached_flake) }).await;
} else {
self.act(Action::RefreshFlake);
}
}
});
// ... when refresh button is clicked.
use_future(cx, (&idx,), |(idx,)| update_flake(idx.is_some()));
}

// Build `state.health_checks` when nix_info changes
Expand Down Expand Up @@ -165,14 +169,3 @@ impl AppState {
}
}
}

/// Push an item to the front of a vector
///
/// If the item already exits, move it to the front.
fn vec_push_as_latest<T: PartialEq>(vec: &mut Vec<T>, item: T) -> &mut Vec<T> {
if let Some(idx) = vec.iter().position(|x| *x == item) {
vec.remove(idx);
}
vec.insert(0, item);
vec
}

0 comments on commit 90ffe9c

Please sign in to comment.