feat(sync): add typed acknowledgement effect

req: sync/006
This commit is contained in:
slhx agent
2026-07-13 23:15:57 +02:00
parent 88598f181f
commit 5a82956c08
6 changed files with 163 additions and 7 deletions
+35 -1
View File
@@ -15,7 +15,7 @@ use hemx_axum::{
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 hemx_sync::{Channel, PresenceScope, PresenceTracker, PresenceUpdate};
use hemx_sync::{Channel, PresenceScope, PresenceTracker, PresenceUpdate, SyncEffect};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::convert::Infallible;
@@ -502,6 +502,7 @@ async fn main() {
.route("/", get(home).post(interact))
.route("/events", get(events))
.route("/sync/broadcast", get(sync_broadcast))
.route("/sync/ack", get(sync_ack))
.route("/sync-demo", get(sync_demo))
.route("/sync.js", get(sync_js))
.route("/sync/context", get(sync_context))
@@ -601,6 +602,39 @@ fn presence_changed(signal: PresenceSignal) -> impl hemx::IntoEffect {
})
}
async fn sync_ack(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Query(params): Query<BTreeMap<String, String>>,
) -> Result<Response, SyncRejection> {
let principal = current_sync_principal(&headers, &state.sync_sessions)?;
let command_id = CommandId::parse(params.get("command_id"))?;
let acknowledgement = {
let sync = state.sync.lock().unwrap();
let acknowledgement =
sync.acknowledgements
.get(&command_id)
.ok_or(SyncRejection::Conflict(
"command has no canonical acknowledgement",
))?;
if !visible_acknowledgement(principal, acknowledgement) {
return Err(SyncRejection::Forbidden(
"current tenant cannot access command acknowledgement",
));
}
acknowledgement.clone()
};
let batch = (
SyncEffect::ack(board::atoms::sync_ack),
board::sync_status.text(format!(
"Canonical acknowledgement {} at server sequence {}",
acknowledgement.command_id, acknowledgement.server_sequence
)),
)
.into_batch(ui::BUILD_FINGERPRINT);
Ok(sse(stream::iter([Ok::<_, Infallible>(batch)]).boxed()).into_response())
}
async fn sync_broadcast(
State(state): State<Arc<AppState>>,
Query(params): Query<BTreeMap<String, String>>,