Files
Genarrative/server-rs/crates/spacetime-module/src/runtime/snapshots.rs
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

217 lines
7.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::*;
#[spacetimedb::table(accessor = runtime_snapshot)]
pub struct RuntimeSnapshotRow {
#[primary_key]
pub(crate) user_id: String,
pub(crate) version: u32,
pub(crate) saved_at: Timestamp,
pub(crate) bottom_tab: String,
pub(crate) game_state_json: String,
pub(crate) current_story_json: Option<String>,
pub(crate) created_at: Timestamp,
pub(crate) updated_at: Timestamp,
}
// 当前快照读取保持旧 Node 语义:无快照时返回 ok=true + record=None而不是默认空对象。
#[spacetimedb::procedure]
pub fn get_runtime_snapshot(
ctx: &mut ProcedureContext,
input: RuntimeSnapshotGetInput,
) -> RuntimeSnapshotProcedureResult {
match ctx.try_with_tx(|tx| get_runtime_snapshot_record(tx, input.clone())) {
Ok(record) => RuntimeSnapshotProcedureResult {
ok: true,
record,
error_message: None,
},
Err(message) => RuntimeSnapshotProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
// PUT snapshot 主链会同步刷新 dashboard / wallet / played_world / save_archive 四类 projection。
#[spacetimedb::procedure]
pub fn upsert_runtime_snapshot_and_return(
ctx: &mut ProcedureContext,
input: RuntimeSnapshotUpsertInput,
) -> RuntimeSnapshotProcedureResult {
match ctx.try_with_tx(|tx| upsert_runtime_snapshot_record(tx, input.clone())) {
Ok(record) => RuntimeSnapshotProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => RuntimeSnapshotProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
// 删除当前快照只影响 runtime_snapshot 主表,不联动清理 profile projection。
#[spacetimedb::procedure]
pub fn delete_runtime_snapshot_and_return(
ctx: &mut ProcedureContext,
input: RuntimeSnapshotDeleteInput,
) -> RuntimeSnapshotProcedureResult {
match ctx.try_with_tx(|tx| delete_runtime_snapshot_record(tx, input.clone())) {
Ok(record) => RuntimeSnapshotProcedureResult {
ok: true,
record,
error_message: None,
},
Err(message) => RuntimeSnapshotProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
pub(crate) fn get_runtime_snapshot_record(
ctx: &ReducerContext,
input: RuntimeSnapshotGetInput,
) -> Result<Option<RuntimeSnapshot>, String> {
let validated_input =
build_runtime_snapshot_get_input(input.user_id).map_err(|error| error.to_string())?;
Ok(ctx
.db
.runtime_snapshot()
.user_id()
.find(&validated_input.user_id)
.map(|row| build_runtime_snapshot_from_row(&row)))
}
pub(crate) fn upsert_runtime_snapshot_record(
ctx: &ReducerContext,
input: RuntimeSnapshotUpsertInput,
) -> Result<RuntimeSnapshot, String> {
let current_story_value = parse_optional_json_str(input.current_story_json.as_deref())?;
let game_state = parse_json_str(&input.game_state_json)?;
let prepared = build_runtime_snapshot_upsert_input(
input.user_id,
input.saved_at_micros,
input.bottom_tab,
game_state.clone(),
current_story_value.clone(),
input.updated_at_micros,
)
.map_err(|error| error.to_string())?;
let updated_at = Timestamp::from_micros_since_unix_epoch(prepared.updated_at_micros);
let saved_at = Timestamp::from_micros_since_unix_epoch(prepared.saved_at_micros);
let snapshot = match ctx.db.runtime_snapshot().user_id().find(&prepared.user_id) {
Some(existing) => {
ctx.db
.runtime_snapshot()
.user_id()
.delete(&existing.user_id);
ctx.db.runtime_snapshot().insert(RuntimeSnapshotRow {
user_id: existing.user_id.clone(),
version: SAVE_SNAPSHOT_VERSION,
saved_at,
bottom_tab: prepared.bottom_tab.clone(),
game_state_json: prepared.game_state_json.clone(),
current_story_json: prepared.current_story_json.clone(),
created_at: existing.created_at,
updated_at,
});
RuntimeSnapshot {
user_id: existing.user_id,
version: SAVE_SNAPSHOT_VERSION,
saved_at_micros: prepared.saved_at_micros,
bottom_tab: prepared.bottom_tab,
game_state_json: prepared.game_state_json,
current_story_json: prepared.current_story_json,
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
updated_at_micros: prepared.updated_at_micros,
}
}
None => {
ctx.db.runtime_snapshot().insert(RuntimeSnapshotRow {
user_id: prepared.user_id.clone(),
version: SAVE_SNAPSHOT_VERSION,
saved_at,
bottom_tab: prepared.bottom_tab.clone(),
game_state_json: prepared.game_state_json.clone(),
current_story_json: prepared.current_story_json.clone(),
created_at: updated_at,
updated_at,
});
RuntimeSnapshot {
user_id: prepared.user_id,
version: SAVE_SNAPSHOT_VERSION,
saved_at_micros: prepared.saved_at_micros,
bottom_tab: prepared.bottom_tab,
game_state_json: prepared.game_state_json,
current_story_json: prepared.current_story_json,
created_at_micros: prepared.updated_at_micros,
updated_at_micros: prepared.updated_at_micros,
}
}
};
sync_profile_projections_from_snapshot(ctx, &snapshot)?;
Ok(snapshot)
}
pub(crate) fn delete_runtime_snapshot_record(
ctx: &ReducerContext,
input: RuntimeSnapshotDeleteInput,
) -> Result<Option<RuntimeSnapshot>, String> {
let validated_input =
build_runtime_snapshot_delete_input(input.user_id).map_err(|error| error.to_string())?;
let existing = ctx
.db
.runtime_snapshot()
.user_id()
.find(&validated_input.user_id);
if let Some(existing) = existing {
let snapshot = build_runtime_snapshot_from_row(&existing);
ctx.db
.runtime_snapshot()
.user_id()
.delete(&existing.user_id);
return Ok(Some(snapshot));
}
Ok(None)
}
pub(crate) fn build_runtime_snapshot_from_row(row: &RuntimeSnapshotRow) -> RuntimeSnapshot {
RuntimeSnapshot {
user_id: row.user_id.clone(),
version: row.version,
saved_at_micros: row.saved_at.to_micros_since_unix_epoch(),
bottom_tab: row.bottom_tab.clone(),
game_state_json: row.game_state_json.clone(),
current_story_json: row.current_story_json.clone(),
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
}
}
pub(crate) fn parse_json_str(raw: &str) -> Result<JsonValue, String> {
serde_json::from_str::<JsonValue>(raw)
.map_err(|error| format!("game_state_json 解析失败: {error}"))
}
pub(crate) fn parse_optional_json_str(raw: Option<&str>) -> Result<Option<JsonValue>, String> {
match raw.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => serde_json::from_str::<JsonValue>(value)
.map(Some)
.map_err(|error| format!("current_story_json 解析失败: {error}")),
None => Ok(None),
}
}