feat(squad): generic game-scoped opaque extension + server fingerprint, atomic in replace_squad tx

This commit is contained in:
funman300
2026-08-12 01:39:04 +00:00
parent 36abd4b6fb
commit 615c5fd7a5
7 changed files with 531 additions and 1 deletions
+34
View File
@@ -0,0 +1,34 @@
//! Generic read/write for [`crate::models::game_ext`] opaque state.
//!
//! Core never interprets the payload. Writes happen INSIDE the owning entity's
//! transaction (see `squad::replace_squad_with_extension`) so the canonical
//! entity and its opaque extension commit atomically — there is deliberately no
//! standalone "write extension" entry point that could desync the two.
use crate::db::Pool;
use crate::error::AppResult;
use crate::models::game_ext::GameEntityExt;
/// Fetch the stored opaque extension for a scoped entity, or `None`. The caller
/// compares `canonical_fingerprint` against the entity's *current* fingerprint to
/// decide freshness — this layer does not know how to fingerprint any entity.
pub async fn get_ext(
pool: &Pool,
game_id: &str,
entity_kind: &str,
entity_id: &str,
namespace: &str,
) -> AppResult<Option<GameEntityExt>> {
let row = sqlx::query_as::<_, GameEntityExt>(
"SELECT game_id, entity_kind, entity_id, namespace, schema_version, \
canonical_fingerprint, payload, updated_at FROM game_entity_ext \
WHERE game_id = ? AND entity_kind = ? AND entity_id = ? AND namespace = ?",
)
.bind(game_id)
.bind(entity_kind)
.bind(entity_id)
.bind(namespace)
.fetch_optional(pool)
.await?;
Ok(row)
}