feat(kanban): upload pending command with retry
req: sync/004\nreq: sync/009\nreq: sync/010\nreq: sync/016\nreq: sync/017
This commit is contained in:
@@ -9,13 +9,13 @@ use hemplate::Hemplate;
|
||||
use hemx::{Html, IntoEffect};
|
||||
use hemx_axum::{
|
||||
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
|
||||
EffectResponse, InteractionRequest, PageRequest,
|
||||
EffectResponse, InteractionRequest, PageRequest, PageResponse,
|
||||
};
|
||||
use hemx_kanban_example::ui::board::{self as board};
|
||||
use hemx_kanban_example::ui::board_card as card_board;
|
||||
use hemx_kanban_example::ui::{self, board as board_ui};
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -34,6 +34,8 @@ struct SyncState {
|
||||
next_sequence: u64,
|
||||
acknowledgements: BTreeMap<CommandId, SyncAcknowledgement>,
|
||||
reconnects: BTreeMap<String, u64>,
|
||||
fail_first_upload: bool,
|
||||
transient_failures: BTreeSet<CommandId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
@@ -68,6 +70,7 @@ struct SyncAcknowledgement {
|
||||
enum SyncRejection {
|
||||
BadRequest(&'static str),
|
||||
Conflict(&'static str),
|
||||
Transient,
|
||||
}
|
||||
|
||||
impl IntoResponse for SyncRejection {
|
||||
@@ -75,6 +78,7 @@ impl IntoResponse for SyncRejection {
|
||||
let (status, error) = match self {
|
||||
Self::BadRequest(error) => (StatusCode::BAD_REQUEST, error),
|
||||
Self::Conflict(error) => (StatusCode::CONFLICT, error),
|
||||
Self::Transient => (StatusCode::SERVICE_UNAVAILABLE, "transient sync failure"),
|
||||
};
|
||||
(status, Json(serde_json::json!({ "error": error }))).into_response()
|
||||
}
|
||||
@@ -99,6 +103,11 @@ struct AppShell {
|
||||
body: Html,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
struct SyncShell {
|
||||
runtime_src: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct BoardColumns {
|
||||
@@ -171,6 +180,7 @@ async fn main() {
|
||||
}),
|
||||
sync: Mutex::new(SyncState {
|
||||
next_sequence: 1,
|
||||
fail_first_upload: std::env::var_os("HEMX_KANBAN_FAIL_FIRST_SYNC").is_some(),
|
||||
..SyncState::default()
|
||||
}),
|
||||
});
|
||||
@@ -178,6 +188,8 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/", get(home).post(interact))
|
||||
.route("/events", get(events))
|
||||
.route("/sync-demo", get(sync_demo))
|
||||
.route("/sync.js", get(sync_js))
|
||||
.route("/sync/commands", post(sync_command))
|
||||
.route("/sync/acknowledgements", get(sync_acknowledgements))
|
||||
.route(runtime_js_path(), get(runtime))
|
||||
@@ -204,6 +216,24 @@ async fn runtime() -> impl IntoResponse {
|
||||
runtime_js()
|
||||
}
|
||||
|
||||
async fn sync_demo() -> impl IntoResponse {
|
||||
PageResponse::full(
|
||||
ui::page(&SyncShell {
|
||||
runtime_src: runtime_js_path(),
|
||||
})
|
||||
.into_string(),
|
||||
)
|
||||
.title("hemx Kanban sync")
|
||||
.fingerprint(ui::BUILD_FINGERPRINT)
|
||||
}
|
||||
|
||||
async fn sync_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("../static/sync.js"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn interact(
|
||||
State(state): State<Arc<AppState>>,
|
||||
request: InteractionRequest,
|
||||
@@ -254,6 +284,9 @@ async fn sync_command(
|
||||
}
|
||||
return Ok(Json(existing.clone()));
|
||||
}
|
||||
if sync.fail_first_upload && sync.transient_failures.insert(command_id.clone()) {
|
||||
return Err(SyncRejection::Transient);
|
||||
}
|
||||
|
||||
let mut board = state.board.lock().unwrap();
|
||||
let card = board
|
||||
|
||||
Reference in New Issue
Block a user