optimize puzzle gallery access

This commit is contained in:
kdletters
2026-05-16 18:14:00 +08:00
parent 49ffa6b901
commit 7f16e88e57
7 changed files with 508 additions and 101 deletions
+16
View File
@@ -83,6 +83,22 @@
- 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。
- 关联:`AGENTS.md``npm run check:encoding`
## SpacetimeDB 运行态查询不要绕过已有索引或用 procedure JSON 回传
- 现象:运行态接口看起来只查当前用户、作品或任务,却在 `spacetime-module` 中使用 `ctx.db.<table>().iter().filter(...)` 整表遍历;或者 procedure result 返回 `items_json/run_json/work_json` 等 JSON 字符串,`spacetime-client` mapper 再反序列化成旧兼容结构。
- 原因:新增索引或 typed snapshot 后,没有同步清理旧 mapper / 测试兼容层,也没有用静态检查拦截回退写法。
- 处理:表上已有主键、unique 或 `#[index]` 覆盖查询前缀时,先用对应 accessor `.find(...)` / `.filter(...)`,只对索引无法覆盖的条件做内存残余过滤;procedure result 返回 typed snapshot / typed value,不再跨层传 `*_json: Option<String>` 作为 payload。
- 验证:执行 `npm run check:spacetime-runtime-access``npm run check:server-rs-ddd`,涉及绑定变化时先执行 `npm run spacetime:generate``npm run check:spacetime-schema`
- 关联:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``scripts/check-spacetime-runtime-access.mjs``server-rs/crates/spacetime-module/src/*``server-rs/crates/spacetime-client/src/mapper.rs`
## 拼图广场列表不要每次 HTTP 请求调用 SpacetimeDB procedure
- 现象:`/api/runtime/puzzle/gallery` 每个请求都走 `spacetime-client.list_puzzle_gallery()` 调用 SpacetimeDB procedure,导致 SpacetimeDB WASM 侧重复组装全量列表,客户端再映射一遍;历史实现还出现过 procedure JSON 字符串往返。
- 原因:`api-server` 的服务器端 `spacetime-client` 没有订阅可公开读取的 gallery 投影,虽然 SDK 支持 client cache,但请求路径仍把列表读取当作 procedure 调用。
- 处理:`spacetime-module` 中用 public view `puzzle_gallery_view` 暴露已发布拼图作品;`spacetime-client` 建连接后订阅 `SELECT * FROM puzzle_gallery_view``SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` 并等待 `on_applied`HTTP gallery 只从 `connection.db().puzzle_gallery_view().iter()` 本地 cache 读取和排序,再用已同步的 `public_work_play_daily_stat` 在本地聚合 7 日播放数。旧 `list_puzzle_gallery` procedure 只作兼容,不再作为 HTTP gallery 主路径。
- 验证:搜索 `server-rs/crates/spacetime-client/src/puzzle.rs` 不应再出现 gallery 主路径调用 `list_puzzle_gallery_then`;执行 `cargo check --manifest-path server-rs/Cargo.toml -p spacetime-client``cargo check --manifest-path server-rs/Cargo.toml -p api-server` 和 schema/runtime access 检查。
- 关联:`server-rs/crates/spacetime-module/src/puzzle.rs``server-rs/crates/spacetime-client/src/lib.rs``server-rs/crates/spacetime-client/src/puzzle.rs``/api/runtime/puzzle/gallery`
## 忘记密码后仍提示手机号或密码错误先查认证快照同步
- 现象:用户通过“忘记密码”重设密码后,接口返回成功或页面进入登录态,但再次使用新密码登录仍提示“手机号或密码错误”;重启后还可能出现 `Bearer JWT 版本已失效`,日志里的 token version 与本地快照不一致。
@@ -84,14 +84,19 @@ npm run check:server-rs-ddd
## SpacetimeDB schema 变更规则
1. 任何 table、reducer、procedure、row shape 或 bindings 变化,都必须同步 `server-rs/crates/spacetime-module/src/migration.rs`、本文件表目录和生成绑定
1. 任何 table、view、reducer、procedure、row shape 或 bindings 变化,都必须同步本文件表 / view 目录和生成绑定;真实 table 变化还必须同步 `server-rs/crates/spacetime-module/src/migration.rs`,view 属于派生投影,不写入迁移导入导出表清单
2. 已有表新增字段必须放在 Rust 表结构体最后,并设置明确 `#[default(...)]`
3. 删除字段、改名、重排字段、改类型或修改字段属性前,必须先询问用户并确认迁移计划。
4. Vec 字段不要直接写无法 const 求值的 default;需要默认空集合时优先使用 `Option<Vec<T>>``#[default(None::<Vec<T>>)]`,业务层归一为空数组。
5. 修改后运行:
5. 运行态读表必须按已声明索引访问。只要 table 上存在覆盖查询前缀的 `#[index(...)]` 或主键 / unique accessor,列表、详情、快照组装和计数都先用对应 accessor `.filter(...)` / `.find(...)`,再在内存中处理索引无法覆盖的残余条件;不得用 `.iter().filter(...)` 扫整表替代现成索引。
6. 面向公开列表的只读投影优先做成 public view,并由 `api-server``spacetime-client` 长期订阅后读本地 cache。不要让 HTTP 列表接口每次请求都调用 procedure 重新组装全量列表;需要请求时间窗口的轻量统计可订阅公开统计表后在 `api-server` 本地聚合,需要写入副作用的详情、点赞、游玩记录仍可走 procedure / reducer。
7. 多列索引按 SpacetimeDB 绑定生成的元组参数直接传入,例如 `.filter((source_type, profile_id, played_day))`;前缀查询只传前缀元组,例如 `.filter((scope_kind, scope_id.as_str()))`。不要为了绕过类型问题退回整表遍历。
8. procedure result 必须返回 typed snapshot / typed value。`spacetime-client` mapper 不得再通过 `row_json/session_json/work_json/items_json/run_json/event_json/feedback_json: Option<String>` 做跨层 JSON 字符串传输,也不得在 mapper 里反序列化旧 `*JsonRecord` 兼容结构。业务内部持久化字段如 `profile_payload_json``levels_json` 等不属于 procedure result 载荷例外,仍按各自表契约处理。
9. 修改后运行:
```bash
npm run spacetime:generate
npm run check:spacetime-runtime-access
npm run check:spacetime-schema
npm run check:server-rs-ddd
```
@@ -460,6 +465,13 @@ npm run check:server-rs-ddd
- Rust 结构体:`PuzzleWorkProfileRow`
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
### `puzzle_gallery_view`
- Rust view`puzzle_gallery_view`
- 返回类型:`Vec<PuzzleWorkProfile>`
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
- 说明:拼图广场公开列表投影,只暴露 `publication_status = Published` 的作品;`api-server``spacetime-client` 长期订阅 `SELECT * FROM puzzle_gallery_view``SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` 后,从本地 cache 构造 `/api/runtime/puzzle/gallery` 响应,并在本地按当前请求时间聚合 `recentPlayCount7d`,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。
### `quest_log`
- Rust 结构体:`QuestLog`
+67 -19
View File
@@ -142,24 +142,6 @@ use module_npc::{
NpcStanceProfile as DomainNpcStanceProfile, NpcStateSnapshot as DomainNpcStateSnapshot,
ResolveNpcInteractionInput as DomainResolveNpcInteractionInput,
};
use module_puzzle::{
PuzzleAgentMessageSnapshot as DomainPuzzleAgentMessageSnapshot,
PuzzleAgentSessionSnapshot as DomainPuzzleAgentSessionSnapshot,
PuzzleAgentSuggestedAction as DomainPuzzleAgentSuggestedAction,
PuzzleAnchorItem as DomainPuzzleAnchorItem, PuzzleAnchorPack as DomainPuzzleAnchorPack,
PuzzleBoardSnapshot as DomainPuzzleBoardSnapshot,
PuzzleCellPosition as DomainPuzzleCellPosition,
PuzzleCreatorIntent as DomainPuzzleCreatorIntent, PuzzleDraftLevel as DomainPuzzleDraftLevel,
PuzzleGeneratedImageCandidate as DomainPuzzleGeneratedImageCandidate,
PuzzleMergedGroupState as DomainPuzzleMergedGroupState,
PuzzlePieceState as DomainPuzzlePieceState, PuzzleResultDraft as DomainPuzzleResultDraft,
PuzzleResultPreviewBlocker as DomainPuzzleResultPreviewBlocker,
PuzzleResultPreviewEnvelope as DomainPuzzleResultPreviewEnvelope,
PuzzleResultPreviewFinding as DomainPuzzleResultPreviewFinding,
PuzzleRunSnapshot as DomainPuzzleRunSnapshot,
PuzzleRuntimeLevelSnapshot as DomainPuzzleRuntimeLevelSnapshot,
PuzzleWorkProfile as DomainPuzzleWorkProfile,
};
use module_runtime::{
AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse, RuntimeBrowseHistoryRecord,
RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord,
@@ -222,7 +204,7 @@ use module_story::{
build_story_continue_input, build_story_session_input, build_story_session_state_input,
};
use shared_kernel::format_timestamp_micros;
use spacetimedb_sdk::DbContext;
use spacetimedb_sdk::{DbContext, Table};
use tokio::{
sync::{OwnedSemaphorePermit, Semaphore, oneshot},
time::timeout,
@@ -285,6 +267,7 @@ struct PooledConnectionSlot {
struct PooledConnection {
connection: DbConnection,
_gallery_subscription: Vec<SubscriptionHandle>,
runner: Option<JoinHandle<()>>,
broken: Arc<AtomicBool>,
}
@@ -377,6 +360,26 @@ impl SpacetimeClient {
final_result
}
async fn read_after_connect<T>(
&self,
read: impl FnOnce(&DbConnection) -> Result<T, SpacetimeClientError> + Send + 'static,
) -> Result<T, SpacetimeClientError>
where
T: Send + 'static,
{
let lease = self.acquire_connection().await?;
let final_result = if let Some(connection) = lease.connection.as_ref() {
read(&connection.connection)
} else {
Err(SpacetimeClientError::Runtime(
"SpacetimeDB 连接租约缺少连接".to_string(),
))
};
self.release_connection(lease).await;
final_result
}
async fn acquire_connection(&self) -> Result<PooledConnectionLease, SpacetimeClientError> {
let permit = timeout(
self.config.procedure_timeout,
@@ -465,13 +468,58 @@ impl SpacetimeClient {
.map_err(|_| SpacetimeClientError::Timeout)?
.map_err(|_| SpacetimeClientError::ConnectDropped)??;
let gallery_subscription = self
.subscribe_puzzle_gallery_views(&connection, broken.clone())
.await?;
Ok(PooledConnection {
connection,
_gallery_subscription: gallery_subscription,
runner: Some(runner),
broken,
})
}
async fn subscribe_puzzle_gallery_views(
&self,
connection: &DbConnection,
broken: Arc<AtomicBool>,
) -> Result<Vec<SubscriptionHandle>, SpacetimeClientError> {
let mut subscriptions = Vec::new();
for query in [
"SELECT * FROM puzzle_gallery_view",
"SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'",
] {
let (sender, receiver) = oneshot::channel::<Result<(), SpacetimeClientError>>();
let applied_sender = Arc::new(Mutex::new(Some(sender)));
let on_applied_sender = applied_sender.clone();
let on_error_sender = applied_sender.clone();
let broken_flag = broken.clone();
let subscription = connection
.subscription_builder()
.on_applied(move |_| {
send_connect_once(&on_applied_sender, Ok(()));
})
.on_error(move |_, error| {
broken_flag.store(true, Ordering::SeqCst);
send_connect_once(
&on_error_sender,
Err(SpacetimeClientError::Procedure(error.to_string())),
);
})
.subscribe(query);
timeout(self.config.procedure_timeout, receiver)
.await
.map_err(|_| SpacetimeClientError::Timeout)?
.map_err(|_| SpacetimeClientError::ConnectDropped)??;
subscriptions.push(subscription);
}
Ok(subscriptions)
}
async fn release_connection(&self, mut lease: PooledConnectionLease) {
let mut slot_guard = self.pool.slots[lease.slot_index].lock().await;
slot_guard.in_use = false;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::puzzle_anchor_pack_type::PuzzleAnchorPack;
use super::puzzle_draft_level_type::PuzzleDraftLevel;
use super::puzzle_publication_status_type::PuzzlePublicationStatus;
use super::puzzle_work_profile_type::PuzzleWorkProfile;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `puzzle_gallery_view`.
///
/// Obtain a handle from the [`PuzzleGalleryViewTableAccess::puzzle_gallery_view`] method on [`super::RemoteTables`],
/// like `ctx.db.puzzle_gallery_view()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.puzzle_gallery_view().on_insert(...)`.
pub struct PuzzleGalleryViewTableHandle<'ctx> {
imp: __sdk::TableHandle<PuzzleWorkProfile>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `puzzle_gallery_view`.
///
/// Implemented for [`super::RemoteTables`].
pub trait PuzzleGalleryViewTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`PuzzleGalleryViewTableHandle`], which mediates access to the table `puzzle_gallery_view`.
fn puzzle_gallery_view(&self) -> PuzzleGalleryViewTableHandle<'_>;
}
impl PuzzleGalleryViewTableAccess for super::RemoteTables {
fn puzzle_gallery_view(&self) -> PuzzleGalleryViewTableHandle<'_> {
PuzzleGalleryViewTableHandle {
imp: self
.imp
.get_table::<PuzzleWorkProfile>("puzzle_gallery_view"),
ctx: std::marker::PhantomData,
}
}
}
pub struct PuzzleGalleryViewInsertCallbackId(__sdk::CallbackId);
pub struct PuzzleGalleryViewDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for PuzzleGalleryViewTableHandle<'ctx> {
type Row = PuzzleWorkProfile;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = PuzzleWorkProfile> + '_ {
self.imp.iter()
}
type InsertCallbackId = PuzzleGalleryViewInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> PuzzleGalleryViewInsertCallbackId {
PuzzleGalleryViewInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: PuzzleGalleryViewInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = PuzzleGalleryViewDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> PuzzleGalleryViewDeleteCallbackId {
PuzzleGalleryViewDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: PuzzleGalleryViewDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<PuzzleWorkProfile>("puzzle_gallery_view");
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<PuzzleWorkProfile>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<PuzzleWorkProfile>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `PuzzleWorkProfile`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait puzzle_gallery_viewQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `PuzzleWorkProfile`.
fn puzzle_gallery_view(&self) -> __sdk::__query_builder::Table<PuzzleWorkProfile>;
}
impl puzzle_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor {
fn puzzle_gallery_view(&self) -> __sdk::__query_builder::Table<PuzzleWorkProfile> {
__sdk::__query_builder::Table::new("puzzle_gallery_view")
}
}
@@ -5,6 +5,45 @@ use crate::module_bindings::delete_puzzle_work_procedure::delete_puzzle_work;
use crate::module_bindings::record_puzzle_work_like_procedure::record_puzzle_work_like;
use crate::module_bindings::remix_puzzle_work_procedure::remix_puzzle_work;
use crate::module_bindings::save_puzzle_ui_background_procedure::save_puzzle_ui_background;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000;
const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
fn current_unix_micros() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_micros() as i64)
.unwrap_or(0)
}
fn current_public_work_day() -> i64 {
current_unix_micros().div_euclid(PUBLIC_WORK_PLAY_DAY_MICROS)
}
fn puzzle_gallery_recent_play_counts(connection: &DbConnection) -> HashMap<String, u32> {
let current_day = current_public_work_day();
let first_day = current_day - (PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1);
let mut counts = HashMap::new();
for row in connection
.db()
.public_work_play_daily_stat()
.iter()
{
if row.source_type != "puzzle"
|| row.played_day < first_day
|| row.played_day > current_day
{
continue;
}
let entry: &mut u32 = counts.entry(row.profile_id).or_insert(0);
*entry = (*entry).saturating_add(row.play_count);
}
counts
}
impl SpacetimeClient {
pub async fn create_puzzle_agent_session(
@@ -397,15 +436,21 @@ impl SpacetimeClient {
pub async fn list_puzzle_gallery(
&self,
) -> Result<Vec<PuzzleWorkProfileRecord>, SpacetimeClientError> {
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.list_puzzle_gallery_then(move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_puzzle_works_procedure_result);
send_once(&sender, mapped);
});
self.read_after_connect(move |connection| {
let mut items = connection.db().puzzle_gallery_view().iter().collect::<Vec<_>>();
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
let recent_play_counts = puzzle_gallery_recent_play_counts(connection);
Ok(items
.into_iter()
.map(|item| {
let mut record = map_puzzle_work_profile(item);
record.recent_play_count_7d = recent_play_counts
.get(&record.profile_id)
.copied()
.unwrap_or(0);
record
})
.collect())
})
.await
}
+99 -71
View File
@@ -31,7 +31,9 @@ use module_runtime::visible_runtime_profile_user_tags;
use serde_json::from_str as json_from_str;
use serde_json::json;
use serde_json::to_string as json_to_string;
use spacetimedb::{ProcedureContext, SpacetimeType, Table, Timestamp, TxContext};
use spacetimedb::{
AnonymousViewContext, ProcedureContext, SpacetimeType, Table, Timestamp, TxContext,
};
use crate::auth::user_account;
@@ -112,6 +114,33 @@ pub struct PuzzleWorkProfileRow {
point_incentive_claimed_points: u64,
}
/// 拼图广场公开列表投影。
///
/// `puzzle_work_profile` 是私有真相表,HTTP gallery 只订阅这个 view
/// 避免每次请求回到 procedure 重新扫表、组装列表和跨层 JSON 往返。
#[spacetimedb::view(accessor = puzzle_gallery_view, public)]
pub fn puzzle_gallery_view(ctx: &AnonymousViewContext) -> Vec<PuzzleWorkProfile> {
let mut items = ctx
.db
.puzzle_work_profile()
.by_puzzle_work_publication_status()
.filter(PuzzlePublicationStatus::Published)
.filter_map(|row| match build_puzzle_work_profile_from_row_without_recent_count(&row) {
Ok(profile) => Some(profile),
Err(error) => {
log::warn!(
"拼图广场 view 跳过损坏的作品投影 profile_id={}: {}",
row.profile_id,
error
);
None
}
})
.collect::<Vec<_>>();
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
items
}
/// 拼图创作事件类型。
///
/// 事件表只广播跨层订阅需要的轻量事实,作品真相仍以
@@ -187,12 +216,12 @@ pub fn create_puzzle_agent_session(
match ctx.try_with_tx(|tx| create_puzzle_agent_session_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -206,12 +235,12 @@ pub fn get_puzzle_agent_session(
match ctx.try_with_tx(|tx| get_puzzle_agent_session_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -225,12 +254,12 @@ pub fn submit_puzzle_agent_message(
match ctx.try_with_tx(|tx| submit_puzzle_agent_message_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -244,12 +273,12 @@ pub fn finalize_puzzle_agent_message_turn(
match ctx.try_with_tx(|tx| finalize_puzzle_agent_message_turn_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -263,12 +292,12 @@ pub fn compile_puzzle_agent_draft(
match ctx.try_with_tx(|tx| compile_puzzle_agent_draft_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -284,12 +313,12 @@ pub fn save_puzzle_form_draft(
match ctx.try_with_tx(|tx| save_puzzle_form_draft_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -303,12 +332,12 @@ pub fn save_puzzle_generated_images(
match ctx.try_with_tx(|tx| save_puzzle_generated_images_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -322,12 +351,12 @@ pub fn save_puzzle_ui_background(
match ctx.try_with_tx(|tx| save_puzzle_ui_background_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -341,12 +370,12 @@ pub fn select_puzzle_cover_image(
match ctx.try_with_tx(|tx| select_puzzle_cover_image_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -360,12 +389,12 @@ pub fn publish_puzzle_work(
match ctx.try_with_tx(|tx| publish_puzzle_work_tx(tx, input.clone())) {
Ok(item) => PuzzleWorkProcedureResult {
ok: true,
item_json: Some(serialize_json(&item)),
item: Some(item),
error_message: None,
},
Err(message) => PuzzleWorkProcedureResult {
ok: false,
item_json: None,
item: None,
error_message: Some(message),
},
}
@@ -379,12 +408,12 @@ pub fn list_puzzle_works(
match ctx.try_with_tx(|tx| list_puzzle_works_tx(tx, input.clone())) {
Ok(items) => PuzzleWorksProcedureResult {
ok: true,
items_json: Some(serialize_json(&items)),
items,
error_message: None,
},
Err(message) => PuzzleWorksProcedureResult {
ok: false,
items_json: None,
items: Vec::new(),
error_message: Some(message),
},
}
@@ -398,12 +427,12 @@ pub fn get_puzzle_work_detail(
match ctx.try_with_tx(|tx| get_puzzle_work_detail_tx(tx, input.clone())) {
Ok(item) => PuzzleWorkProcedureResult {
ok: true,
item_json: Some(serialize_json(&item)),
item: Some(item),
error_message: None,
},
Err(message) => PuzzleWorkProcedureResult {
ok: false,
item_json: None,
item: None,
error_message: Some(message),
},
}
@@ -417,12 +446,12 @@ pub fn update_puzzle_work(
match ctx.try_with_tx(|tx| update_puzzle_work_tx(tx, input.clone())) {
Ok(item) => PuzzleWorkProcedureResult {
ok: true,
item_json: Some(serialize_json(&item)),
item: Some(item),
error_message: None,
},
Err(message) => PuzzleWorkProcedureResult {
ok: false,
item_json: None,
item: None,
error_message: Some(message),
},
}
@@ -436,12 +465,12 @@ pub fn delete_puzzle_work(
match ctx.try_with_tx(|tx| delete_puzzle_work_tx(tx, input.clone())) {
Ok(items) => PuzzleWorksProcedureResult {
ok: true,
items_json: Some(serialize_json(&items)),
items,
error_message: None,
},
Err(message) => PuzzleWorksProcedureResult {
ok: false,
items_json: None,
items: Vec::new(),
error_message: Some(message),
},
}
@@ -452,12 +481,12 @@ pub fn list_puzzle_gallery(ctx: &mut ProcedureContext) -> PuzzleWorksProcedureRe
match ctx.try_with_tx(|tx| list_puzzle_gallery_tx(tx)) {
Ok(items) => PuzzleWorksProcedureResult {
ok: true,
items_json: Some(serialize_json(&items)),
items,
error_message: None,
},
Err(message) => PuzzleWorksProcedureResult {
ok: false,
items_json: None,
items: Vec::new(),
error_message: Some(message),
},
}
@@ -471,12 +500,12 @@ pub fn get_puzzle_gallery_detail(
match ctx.try_with_tx(|tx| get_puzzle_gallery_detail_tx(tx, input.clone())) {
Ok(item) => PuzzleWorkProcedureResult {
ok: true,
item_json: Some(serialize_json(&item)),
item: Some(item),
error_message: None,
},
Err(message) => PuzzleWorkProcedureResult {
ok: false,
item_json: None,
item: None,
error_message: Some(message),
},
}
@@ -490,12 +519,12 @@ pub fn record_puzzle_work_like(
match ctx.try_with_tx(|tx| record_puzzle_work_like_tx(tx, input.clone())) {
Ok(item) => PuzzleWorkProcedureResult {
ok: true,
item_json: Some(serialize_json(&item)),
item: Some(item),
error_message: None,
},
Err(message) => PuzzleWorkProcedureResult {
ok: false,
item_json: None,
item: None,
error_message: Some(message),
},
}
@@ -509,12 +538,12 @@ pub fn remix_puzzle_work(
match ctx.try_with_tx(|tx| remix_puzzle_work_tx(tx, input.clone())) {
Ok(session) => PuzzleAgentSessionProcedureResult {
ok: true,
session_json: Some(serialize_json(&session)),
session: Some(session),
error_message: None,
},
Err(message) => PuzzleAgentSessionProcedureResult {
ok: false,
session_json: None,
session: None,
error_message: Some(message),
},
}
@@ -528,12 +557,12 @@ pub fn start_puzzle_run(
match ctx.try_with_tx(|tx| start_puzzle_run_tx(tx, input.clone())) {
Ok(run) => PuzzleRunProcedureResult {
ok: true,
run_json: Some(serialize_json(&run)),
run: Some(run),
error_message: None,
},
Err(message) => PuzzleRunProcedureResult {
ok: false,
run_json: None,
run: None,
error_message: Some(message),
},
}
@@ -547,12 +576,12 @@ pub fn get_puzzle_run(
match ctx.try_with_tx(|tx| get_puzzle_run_tx(tx, input.clone())) {
Ok(run) => PuzzleRunProcedureResult {
ok: true,
run_json: Some(serialize_json(&run)),
run: Some(run),
error_message: None,
},
Err(message) => PuzzleRunProcedureResult {
ok: false,
run_json: None,
run: None,
error_message: Some(message),
},
}
@@ -566,12 +595,12 @@ pub fn swap_puzzle_pieces(
match ctx.try_with_tx(|tx| swap_puzzle_pieces_tx(tx, input.clone())) {
Ok(run) => PuzzleRunProcedureResult {
ok: true,
run_json: Some(serialize_json(&run)),
run: Some(run),
error_message: None,
},
Err(message) => PuzzleRunProcedureResult {
ok: false,
run_json: None,
run: None,
error_message: Some(message),
},
}
@@ -585,12 +614,12 @@ pub fn drag_puzzle_piece_or_group(
match ctx.try_with_tx(|tx| drag_puzzle_piece_or_group_tx(tx, input.clone())) {
Ok(run) => PuzzleRunProcedureResult {
ok: true,
run_json: Some(serialize_json(&run)),
run: Some(run),
error_message: None,
},
Err(message) => PuzzleRunProcedureResult {
ok: false,
run_json: None,
run: None,
error_message: Some(message),
},
}
@@ -604,12 +633,12 @@ pub fn advance_puzzle_next_level(
match ctx.try_with_tx(|tx| advance_puzzle_next_level_tx(tx, input.clone())) {
Ok(run) => PuzzleRunProcedureResult {
ok: true,
run_json: Some(serialize_json(&run)),
run: Some(run),
error_message: None,
},
Err(message) => PuzzleRunProcedureResult {
ok: false,
run_json: None,
run: None,
error_message: Some(message),
},
}
@@ -623,12 +652,12 @@ pub fn update_puzzle_run_pause(
match ctx.try_with_tx(|tx| update_puzzle_run_pause_tx(tx, input.clone())) {
Ok(run) => PuzzleRunProcedureResult {
ok: true,
run_json: Some(serialize_json(&run)),
run: Some(run),
error_message: None,
},
Err(message) => PuzzleRunProcedureResult {
ok: false,
run_json: None,
run: None,
error_message: Some(message),
},
}
@@ -642,12 +671,12 @@ pub fn use_puzzle_runtime_prop(
match ctx.try_with_tx(|tx| use_puzzle_runtime_prop_tx(tx, input.clone())) {
Ok(run) => PuzzleRunProcedureResult {
ok: true,
run_json: Some(serialize_json(&run)),
run: Some(run),
error_message: None,
},
Err(message) => PuzzleRunProcedureResult {
ok: false,
run_json: None,
run: None,
error_message: Some(message),
},
}
@@ -661,12 +690,12 @@ pub fn claim_puzzle_work_point_incentive(
match ctx.try_with_tx(|tx| claim_puzzle_work_point_incentive_tx(tx, input.clone())) {
Ok(item) => PuzzleWorkProcedureResult {
ok: true,
item_json: Some(serialize_json(&item)),
item: Some(item),
error_message: None,
},
Err(message) => PuzzleWorkProcedureResult {
ok: false,
item_json: None,
item: None,
error_message: Some(message),
},
}
@@ -680,12 +709,12 @@ pub fn submit_puzzle_leaderboard_entry(
match ctx.try_with_tx(|tx| submit_puzzle_leaderboard_entry_tx(tx, input.clone())) {
Ok(run) => PuzzleRunProcedureResult {
ok: true,
run_json: Some(serialize_json(&run)),
run: Some(run),
error_message: None,
},
Err(message) => PuzzleRunProcedureResult {
ok: false,
run_json: None,
run: None,
error_message: Some(message),
},
}
@@ -1264,8 +1293,8 @@ fn list_puzzle_works_tx(
let mut items = ctx
.db
.puzzle_work_profile()
.iter()
.filter(|row| row.owner_user_id == input.owner_user_id)
.by_puzzle_work_owner_user_id()
.filter(&input.owner_user_id)
.map(|row| build_puzzle_work_profile_from_row(&row))
.collect::<Result<Vec<_>, _>>()?;
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
@@ -1446,8 +1475,8 @@ fn delete_puzzle_work_tx(
for message in ctx
.db
.puzzle_agent_message()
.iter()
.filter(|message| message.session_id == *session_id)
.by_puzzle_agent_message_session_id()
.filter(session_id)
.collect::<Vec<_>>()
{
ctx.db
@@ -1459,10 +1488,9 @@ fn delete_puzzle_work_tx(
for run in ctx
.db
.puzzle_runtime_run()
.iter()
.filter(|run| {
run.owner_user_id == input.owner_user_id && run.entry_profile_id == input.profile_id
})
.by_puzzle_runtime_run_owner_user_id()
.filter(&input.owner_user_id)
.filter(|run| run.entry_profile_id == input.profile_id)
.collect::<Vec<_>>()
{
ctx.db.puzzle_runtime_run().run_id().delete(&run.run_id);
@@ -1481,8 +1509,8 @@ fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, Str
let rows = ctx
.db
.puzzle_work_profile()
.iter()
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
.by_puzzle_work_publication_status()
.filter(PuzzlePublicationStatus::Published)
.collect::<Vec<_>>();
let profile_ids = rows
.iter()
@@ -2542,8 +2570,8 @@ fn list_session_messages(ctx: &TxContext, session_id: &str) -> Vec<PuzzleAgentMe
let mut items = ctx
.db
.puzzle_agent_message()
.iter()
.filter(|message| message.session_id == session_id)
.by_puzzle_agent_message_session_id()
.filter(&session_id.to_string())
.map(|message| PuzzleAgentMessageSnapshot {
message_id: message.message_id.clone(),
session_id: message.session_id.clone(),
@@ -3152,8 +3180,8 @@ fn replace_generated_candidate(
fn list_published_puzzle_profiles(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, String> {
ctx.db
.puzzle_work_profile()
.iter()
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
.by_puzzle_work_publication_status()
.filter(PuzzlePublicationStatus::Published)
.map(|row| build_puzzle_work_profile_from_row(&row))
.collect()
}
@@ -3319,8 +3347,8 @@ fn list_puzzle_leaderboard_entries(
let mut rows = ctx
.db
.puzzle_leaderboard_entry()
.iter()
.filter(|row| row.profile_id == profile_id && row.grid_size == grid_size)
.by_puzzle_leaderboard_profile_grid()
.filter((profile_id, grid_size))
.collect::<Vec<_>>();
rows.sort_by(|left, right| {
left.best_elapsed_ms