diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index dcc7c8b5cbe..bf4169c5a42 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -13,7 +13,7 @@ use crate::{ types::{ self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion, DeployAppVersionConnection, DnsDomain, GetAppTemplateFromSlugVariables, - GetAppTemplatesQueryVariables, GetCurrentUserWithAppsVars, GetDeployAppAndVersion, + GetAppTemplatesVars, GetCurrentUserWithAppsVars, GetDeployAppAndVersion, GetDeployAppVersionsVars, GetNamespaceAppsVars, GetSignedUrlForPackageUploadVariables, Log, LogStream, PackageVersionConnection, PublishDeployAppVars, PushPackageReleasePayload, SignedUrl, TagPackageReleasePayload, UpsertDomainFromZoneFileVars, @@ -78,14 +78,12 @@ pub async fn fetch_app_templates( sort_by: Option, ) -> Result, anyhow::Error> { client - .run_graphql_strict(types::GetAppTemplatesQuery::build( - GetAppTemplatesQueryVariables { - category_slug, - first, - after, - sort_by, - }, - )) + .run_graphql_strict(types::GetAppTemplates::build(GetAppTemplatesVars { + category_slug, + first, + after, + sort_by, + })) .await .map(|r| r.get_app_templates) } @@ -93,49 +91,58 @@ pub async fn fetch_app_templates( /// Fetch all app templates by paginating through the responses. /// /// Will fetch at most `max` templates. -pub async fn fetch_all_app_templates( +pub fn fetch_all_app_templates( client: &WasmerClient, page_size: i32, - max: usize, sort_by: Option, -) -> Result, anyhow::Error> { - let mut all = Vec::new(); - let mut cursor = None; +) -> impl futures::Stream, anyhow::Error>> + '_ { + let vars = GetAppTemplatesVars { + category_slug: String::new(), + first: page_size, + sort_by, + after: None, + }; - loop { - let con = client - .run_graphql_strict(types::GetAppTemplatesQuery::build( - GetAppTemplatesQueryVariables { - category_slug: String::new(), - first: page_size, - sort_by, - after: cursor.take(), - }, - )) - .await? - .get_app_templates - .context("backend did not return any data")?; + futures::stream::try_unfold( + Some(vars), + move |vars: Option| async move { + let vars = match vars { + Some(vars) => vars, + None => return Ok(None), + }; - if con.edges.is_empty() { - break; - } + let con = client + .run_graphql_strict(types::GetAppTemplates::build(vars.clone())) + .await? + .get_app_templates + .context("backend did not return any data")?; - let new = con.edges.into_iter().flatten().filter_map(|edge| edge.node); - all.extend(new); + let items = con + .edges + .into_iter() + .flatten() + .filter_map(|edge| edge.node) + .collect::>(); - let next_cursor = con - .page_info - .end_cursor - .filter(|_| con.page_info.has_next_page && all.len() < max); + let next_cursor = con + .page_info + .end_cursor + .filter(|_| con.page_info.has_next_page); - if let Some(next) = next_cursor { - cursor = Some(next); - } else { - break; - } - } + let next_vars = next_cursor.map(|after| types::GetAppTemplatesVars { + after: Some(after), + ..vars + }); - Ok(all) + #[allow(clippy::type_complexity)] + let res: Result< + Option<(Vec, Option)>, + anyhow::Error, + > = Ok(Some((items, next_vars))); + + res + }, + ) } /// Get a signed URL to upload packages. diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index 371c24afc0e..e06a03e06b0 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -9,7 +9,7 @@ mod queries { use super::schema; - #[derive(cynic::Scalar, Debug, Clone)] + #[derive(cynic::Scalar, Debug, Clone, PartialEq, Eq)] pub struct DateTime(pub String); impl TryFrom for DateTime { @@ -157,8 +157,8 @@ mod queries { Popular, } - #[derive(cynic::QueryVariables, Debug)] - pub struct GetAppTemplatesQueryVariables { + #[derive(cynic::QueryVariables, Debug, Clone)] + pub struct GetAppTemplatesVars { pub category_slug: String, pub first: i32, pub after: Option, @@ -166,8 +166,8 @@ mod queries { } #[derive(cynic::QueryFragment, Debug)] - #[cynic(graphql_type = "Query", variables = "GetAppTemplatesQueryVariables")] - pub struct GetAppTemplatesQuery { + #[cynic(graphql_type = "Query", variables = "GetAppTemplatesVars")] + pub struct GetAppTemplates { #[arguments( categorySlug: $category_slug, first: $first, @@ -189,7 +189,7 @@ mod queries { pub cursor: String, } - #[derive(serde::Serialize, cynic::QueryFragment, Debug)] + #[derive(serde::Serialize, cynic::QueryFragment, PartialEq, Eq, Debug)] pub struct AppTemplate { #[serde(rename = "demoUrl")] pub demo_url: String, @@ -214,7 +214,7 @@ mod queries { pub use_cases: Jsonstring, } - #[derive(cynic::Scalar, Debug, Clone)] + #[derive(cynic::Scalar, Debug, Clone, PartialEq, Eq)] #[cynic(graphql_type = "JSONString")] pub struct Jsonstring(pub String); diff --git a/lib/cli/src/commands/app/create.rs b/lib/cli/src/commands/app/create.rs index bf3bfe975e7..addf5e4d4a2 100644 --- a/lib/cli/src/commands/app/create.rs +++ b/lib/cli/src/commands/app/create.rs @@ -1,14 +1,5 @@ //! Create a new Edge app. -use crate::{ - commands::AsyncCliCommand, - opts::{ApiOpts, ItemFormatOpts, WasmerEnv}, - utils::{load_package_manifest, prompts::PackageCheckMode}, -}; -use anyhow::Context; -use colored::Colorize; -use dialoguer::{theme::ColorfulTheme, Confirm, Select}; -use is_terminal::IsTerminal; use std::{ collections::HashMap, env, @@ -17,10 +8,21 @@ use std::{ str::FromStr, time::Duration, }; + +use anyhow::Context; +use colored::Colorize; +use dialoguer::{theme::ColorfulTheme, Confirm, Select}; +use futures::stream::TryStreamExt; +use is_terminal::IsTerminal; use wasmer_api::{types::AppTemplate, WasmerClient}; use wasmer_config::{app::AppConfigV1, package::PackageSource}; use super::{deploy::CmdAppDeploy, util::login_user}; +use crate::{ + commands::AsyncCliCommand, + opts::{ApiOpts, ItemFormatOpts, WasmerEnv}, + utils::{load_package_manifest, prompts::PackageCheckMode}, +}; async fn write_app_config(app_config: &AppConfigV1, dir: Option) -> anyhow::Result<()> { let raw_app_config = app_config.clone().to_yaml()?; @@ -282,18 +284,13 @@ impl CmdAppCreate { /// Returns an error if the cache file is older than the max age. fn load_cached_templates( path: &Path, - max_cache_age: std::time::Duration, - ) -> Result, anyhow::Error> { + ) -> Result<(Vec, std::time::Duration), anyhow::Error> { let modified = path.metadata()?.modified()?; let age = modified.elapsed()?; - if age > max_cache_age { - anyhow::bail!("cache has expired"); - } - let data = std::fs::read_to_string(path)?; match serde_json::from_str::>(&data) { - Ok(v) => Ok(v), + Ok(v) => Ok((v, age)), Err(err) => { std::fs::remove_file(path).ok(); Err(err).context("could not deserialize cached file") @@ -321,35 +318,65 @@ impl CmdAppCreate { client: &WasmerClient, cache_dir: &Path, ) -> Result, anyhow::Error> { - const MAX_CACHE_AGE: Duration = Duration::from_secs(60 * 15); + const MAX_CACHE_AGE: Duration = Duration::from_secs(60 * 60); const MAX_COUNT: usize = 100; - const CACHE_FILENAME: &'static str = "app_templates.json"; + const CACHE_FILENAME: &str = "app_templates.json"; let cache_path = cache_dir.join(CACHE_FILENAME); - match Self::load_cached_templates(&cache_path, MAX_CACHE_AGE) { - Ok(v) => { - return Ok(v); + let cached_items = match Self::load_cached_templates(&cache_path) { + Ok((items, age)) => { + if age <= MAX_CACHE_AGE { + return Ok(items); + } + items } Err(e) => { tracing::trace!(error = &*e, "could not load templates from local cache"); + Vec::new() } - } + }; - let templates = wasmer_api::query::fetch_all_app_templates( + // Either no cache present, or cache has exceeded max age. + // Fetch the first page. + // If first item matches, then no need to re-fetch. + let mut stream = Box::pin(wasmer_api::query::fetch_all_app_templates( client, 10, - MAX_COUNT, - Some(wasmer_api::types::AppTemplatesSortBy::Popular), - ) - .await?; + Some(wasmer_api::types::AppTemplatesSortBy::Newest), + )); + + let first_page = match stream.try_next().await? { + Some(items) => items, + None => return Ok(Vec::new()), + }; + + if let (Some(a), Some(b)) = (cached_items.first(), first_page.first()) { + if a == b { + // Cached items are up to date, no need to query more. + return Ok(cached_items); + } + } + + let mut items = first_page; + while let Some(next) = stream.try_next().await? { + items.extend(next); + + if items.len() >= MAX_COUNT { + break; + } + } // Persist to cache. - if let Err(err) = Self::persist_template_cache(&cache_path, &templates) { + if let Err(err) = Self::persist_template_cache(&cache_path, &items) { tracing::trace!(error = &*err, "could not persist template cache"); } - Ok(templates) + // TODO: sort items by popularity! + // Since we can't rely on backend sorting because of the cache + // preservation logic, the backend needs to add a popluarity field. + + Ok(items) } // A utility function used to fetch the URL of the template to use. @@ -395,6 +422,7 @@ impl CmdAppCreate { let dialog = dialoguer::Select::with_theme(&theme) .with_prompt(format!("Select a template ({} available)", items.len())) .items(&items) + .max_length(10) .clear(true) .report(false) .default(0);