Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
519 changes: 253 additions & 266 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ wasm-bindgen = { version = "0.2", optional = true }
web-sys = { version = "0.3", features = ["Storage", "Window", "Performance"], optional = true }
js-sys = { version = "0.3", optional = true }
once_cell = { version = "1.19", optional = true }
bincode = "1"
fluent = "0.16"
fluent-bundle = "0.15"
unic-langid = { version = "0.9", features = ["macros"] }
Expand Down
2 changes: 1 addition & 1 deletion src/api_client/achievement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ use serde::Deserialize;
pub struct MintResponse {
pub transaction_id: String,
pub status: String,
}
}
2 changes: 1 addition & 1 deletion src/api_client/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ pub struct LoginRequest<'a> {
#[derive(Debug, Deserialize)]
pub struct LoginResponse {
pub access_token: String,
}
}
2 changes: 1 addition & 1 deletion src/api_client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ impl Default for ApiClientConfig {
base_delay: Duration::from_millis(500),
}
}
}
}
2 changes: 1 addition & 1 deletion src/api_client/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ pub enum ApiClientError {

#[error("serialization error: {0}")]
Serde(#[from] serde_json::Error),
}
}
2 changes: 1 addition & 1 deletion src/api_client/leaderboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ pub struct LeaderboardEntry {
pub username: String,
pub score: u64,
pub rank: u32,
}
}
49 changes: 24 additions & 25 deletions src/api_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use auth::{LoginRequest, LoginResponse};
use config::ApiClientConfig;
use error::ApiClientError;
use leaderboard::LeaderboardEntry;
use offline_queue::{is_offline_error, OfflineQueue, QueuedAction};
use offline_queue::{OfflineQueue, QueuedAction, is_offline_error};
use retry::with_retry;
use session::Session;
use std::sync::Arc;
Expand Down Expand Up @@ -83,12 +83,12 @@ impl ApiClient {
})
.await;

if let Err(ApiClientError::Network(ref e)) = result {
if is_offline_error(e) {
let payload = serde_json::to_string(session)?;
self.queue.push(QueuedAction::SubmitSession(payload)).await;
return Err(ApiClientError::Offline);
}
if let Err(ApiClientError::Network(ref e)) = result
&& is_offline_error(e)
{
let payload = serde_json::to_string(session)?;
self.queue.push(QueuedAction::SubmitSession(payload)).await;
return Err(ApiClientError::Offline);
}
result
}
Expand All @@ -110,7 +110,10 @@ impl ApiClient {
.await
}

pub async fn mint_achievement(&self, achievement_id: &str) -> Result<MintResponse, ApiClientError> {
pub async fn mint_achievement(
&self,
achievement_id: &str,
) -> Result<MintResponse, ApiClientError> {
let url = format!("{}/nft/mint", self.config.base_url);
let token = self.token().await.ok_or(ApiClientError::Unauthorized)?;

Expand All @@ -129,13 +132,13 @@ impl ApiClient {
})
.await;

if let Err(ApiClientError::Network(ref e)) = result {
if is_offline_error(e) {
self.queue
.push(QueuedAction::MintAchievement(achievement_id.to_string()))
.await;
return Err(ApiClientError::Offline);
}
if let Err(ApiClientError::Network(ref e)) = result
&& is_offline_error(e)
{
self.queue
.push(QueuedAction::MintAchievement(achievement_id.to_string()))
.await;
return Err(ApiClientError::Offline);
}
result
}
Expand All @@ -151,15 +154,11 @@ impl ApiClient {

for action in actions {
let result = match action {
QueuedAction::SubmitSession(json) => {
match serde_json::from_str::<Session>(&json) {
Ok(session) => self.submit_session(&session).await,
Err(e) => Err(ApiClientError::from(e)),
}
}
QueuedAction::MintAchievement(id) => {
self.mint_achievement(&id).await.map(|_| ())
}
QueuedAction::SubmitSession(json) => match serde_json::from_str::<Session>(&json) {
Ok(session) => self.submit_session(&session).await,
Err(e) => Err(ApiClientError::from(e)),
},
QueuedAction::MintAchievement(id) => self.mint_achievement(&id).await.map(|_| ()),
};
results.push(result);
}
Expand All @@ -168,4 +167,4 @@ impl ApiClient {
}

#[cfg(test)]
mod tests;
mod tests;
6 changes: 5 additions & 1 deletion src/api_client/offline_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ impl OfflineQueue {
pub async fn len(&self) -> usize {
self.inner.lock().await.len()
}

pub async fn is_empty(&self) -> bool {
self.inner.lock().await.is_empty()
}
}

/// Detects whether a reqwest error indicates the client is offline
/// (as opposed to a server-side error like 500).
pub fn is_offline_error(err: &reqwest::Error) -> bool {
err.is_connect() || err.is_timeout()
}
}
4 changes: 2 additions & 2 deletions src/api_client/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ where
}

fn is_retryable(err: &ApiClientError) -> bool {
matches!(err, ApiClientError::Network(e) if e.is_timeout() || e.is_connect())
}
matches!(err, ApiClientError::Network(e) if e.is_timeout() || e.is_connect() || e.status().is_some_and(|s| s.is_server_error()))
}
2 changes: 1 addition & 1 deletion src/api_client/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ pub struct Session {
pub score: u64,
pub duration_secs: u64,
pub completed_at: chrono::DateTime<chrono::Utc>,
}
}
2 changes: 1 addition & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl Config {
/// - Returns the parsed config on success.
/// - Returns [`Config::default`] if the file does not exist.
/// - Returns an error for IO errors or malformed TOML.
pub fn load(path: impl AsRef<Path>) -> Result<Self, AppError> {
pub fn load(path: impl AsRef<Path>) -> Result<Self, AppError> {
let path = path.as_ref();
if !path.exists() {
return Ok(Config::default());
Expand Down
1 change: 0 additions & 1 deletion src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use std::collections::HashMap;
use std::thread::sleep;
use std::time::Duration;


/// Core game engine that manages the main loop and lifecycle.
pub struct Engine {
tick_rate: Duration,
Expand Down
26 changes: 18 additions & 8 deletions src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,17 +253,27 @@ mod tests {
assert!(AppError::InputInvalid("x".into()).source().is_none());
assert!(AppError::InputEmpty.source().is_none());
assert!(AppError::PlayerNotFound("x".into()).source().is_none());
assert!(AppError::InventoryItemNotFound("x".into()).source().is_none());
assert!(
AppError::InventoryItemNotFound("x".into())
.source()
.is_none()
);
assert!(AppError::Puzzle("x".into()).source().is_none());
assert!(AppError::Leaderboard("x".into()).source().is_none());
assert!(AppError::PluginNotFound("x".into()).source().is_none());
assert!(AppError::PluginAlreadyRegistered("x".into()).source().is_none());
assert!(AppError::NftAlreadyMinted {
player_id: "p".into(),
milestone_type: "m".into(),
}
.source()
.is_none());
assert!(
AppError::PluginAlreadyRegistered("x".into())
.source()
.is_none()
);
assert!(
AppError::NftAlreadyMinted {
player_id: "p".into(),
milestone_type: "m".into(),
}
.source()
.is_none()
);
}

// ── From impl tests ──────────────────────────────────────────────────────
Expand Down
Loading
Loading