diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index f80c0903..79e0216d 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -83,6 +83,22 @@ - 验证:运行仓库已有编码检查;人工抽查修改文件中的中文内容。 - 关联:`AGENTS.md`、`npm run check:encoding`。 +## SpacetimeDB 运行态查询不要绕过已有索引或用 procedure JSON 回传 + +- 现象:运行态接口看起来只查当前用户、作品或任务,却在 `spacetime-module` 中使用 `ctx.db.().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` 作为 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 与本地快照不一致。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index f65a7b87..084b3364 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -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>` 加 `#[default(None::>)]`,业务层归一为空数组。 -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` 做跨层 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` +- 源码:`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` diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 95a674f4..e222f6b2 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -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, runner: Option>, broken: Arc, } @@ -377,6 +360,26 @@ impl SpacetimeClient { final_result } + async fn read_after_connect( + &self, + read: impl FnOnce(&DbConnection) -> Result + Send + 'static, + ) -> Result + 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 { 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, + ) -> Result, 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::>(); + 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; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index 379a2436..26fa2455 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -95,6 +95,7 @@ pub mod auth_store_snapshot_type; pub mod auth_store_snapshot_upsert_input_type; pub mod authorize_database_migration_operator_procedure; pub mod bark_battle_draft_config_row_type; +pub mod bark_battle_draft_config_snapshot_type; pub mod bark_battle_draft_config_table; pub mod bark_battle_draft_config_upsert_input_type; pub mod bark_battle_draft_create_input_type; @@ -107,8 +108,10 @@ pub mod bark_battle_published_config_row_type; pub mod bark_battle_published_config_table; pub mod bark_battle_run_finish_input_type; pub mod bark_battle_run_get_input_type; +pub mod bark_battle_run_snapshot_type; pub mod bark_battle_run_start_input_type; pub mod bark_battle_runtime_config_get_input_type; +pub mod bark_battle_runtime_config_snapshot_type; pub mod bark_battle_runtime_run_row_type; pub mod bark_battle_runtime_run_table; pub mod bark_battle_score_record_row_type; @@ -160,16 +163,20 @@ pub mod big_fish_run_get_input_type; pub mod big_fish_run_procedure_result_type; pub mod big_fish_run_start_input_type; pub mod big_fish_run_status_type; +pub mod big_fish_runtime_entity_snapshot_type; pub mod big_fish_runtime_params_type; pub mod big_fish_runtime_run_table; pub mod big_fish_runtime_run_type; +pub mod big_fish_runtime_snapshot_type; pub mod big_fish_session_create_input_type; pub mod big_fish_session_get_input_type; pub mod big_fish_session_procedure_result_type; pub mod big_fish_session_snapshot_type; +pub mod big_fish_vector_2_type; pub mod big_fish_work_delete_input_type; pub mod big_fish_work_like_record_input_type; pub mod big_fish_work_remix_input_type; +pub mod big_fish_work_summary_snapshot_type; pub mod big_fish_works_list_input_type; pub mod big_fish_works_procedure_result_type; pub mod bind_asset_object_to_entity_and_return_procedure; @@ -402,30 +409,38 @@ pub mod list_visual_novel_works_procedure; pub mod mark_profile_recharge_order_paid_and_return_procedure; pub mod match_3_d_agent_message_finalize_input_type; pub mod match_3_d_agent_message_row_type; +pub mod match_3_d_agent_message_snapshot_type; pub mod match_3_d_agent_message_submit_input_type; pub mod match_3_d_agent_message_table; pub mod match_3_d_agent_session_create_input_type; pub mod match_3_d_agent_session_get_input_type; pub mod match_3_d_agent_session_procedure_result_type; pub mod match_3_d_agent_session_row_type; +pub mod match_3_d_agent_session_snapshot_type; pub mod match_3_d_agent_session_table; pub mod match_3_d_click_item_procedure_result_type; +pub mod match_3_d_creator_config_snapshot_type; pub mod match_3_d_draft_compile_input_type; +pub mod match_3_d_draft_snapshot_type; +pub mod match_3_d_item_snapshot_type; pub mod match_3_d_run_click_input_type; pub mod match_3_d_run_get_input_type; pub mod match_3_d_run_procedure_result_type; pub mod match_3_d_run_restart_input_type; +pub mod match_3_d_run_snapshot_type; pub mod match_3_d_run_start_input_type; pub mod match_3_d_run_stop_input_type; pub mod match_3_d_run_time_up_input_type; pub mod match_3_d_runtime_run_row_type; pub mod match_3_d_runtime_run_table; +pub mod match_3_d_tray_slot_snapshot_type; pub mod match_3_d_work_delete_input_type; pub mod match_3_d_work_get_input_type; pub mod match_3_d_work_procedure_result_type; pub mod match_3_d_work_profile_row_type; pub mod match_3_d_work_profile_table; pub mod match_3_d_work_publish_input_type; +pub mod match_3_d_work_snapshot_type; pub mod match_3_d_work_update_input_type; pub mod match_3_d_works_list_input_type; pub mod match_3_d_works_procedure_result_type; @@ -499,33 +514,58 @@ pub mod puzzle_agent_message_finalize_input_type; pub mod puzzle_agent_message_kind_type; pub mod puzzle_agent_message_role_type; pub mod puzzle_agent_message_row_type; +pub mod puzzle_agent_message_snapshot_type; pub mod puzzle_agent_message_submit_input_type; pub mod puzzle_agent_message_table; pub mod puzzle_agent_session_create_input_type; pub mod puzzle_agent_session_get_input_type; pub mod puzzle_agent_session_procedure_result_type; pub mod puzzle_agent_session_row_type; +pub mod puzzle_agent_session_snapshot_type; pub mod puzzle_agent_session_table; pub mod puzzle_agent_stage_type; +pub mod puzzle_agent_suggested_action_type; +pub mod puzzle_anchor_item_type; +pub mod puzzle_anchor_pack_type; +pub mod puzzle_anchor_status_type; +pub mod puzzle_audio_asset_type; +pub mod puzzle_board_snapshot_type; +pub mod puzzle_cell_position_type; +pub mod puzzle_creator_intent_type; pub mod puzzle_draft_compile_input_type; +pub mod puzzle_draft_level_type; pub mod puzzle_event_kind_type; pub mod puzzle_event_table; pub mod puzzle_event_type; pub mod puzzle_form_draft_save_input_type; +pub mod puzzle_form_draft_type; +pub mod puzzle_gallery_view_table; +pub mod puzzle_generated_image_candidate_type; pub mod puzzle_generated_images_save_input_type; pub mod puzzle_leaderboard_entry_row_type; pub mod puzzle_leaderboard_entry_table; +pub mod puzzle_leaderboard_entry_type; pub mod puzzle_leaderboard_submit_input_type; +pub mod puzzle_merged_group_state_type; +pub mod puzzle_piece_state_type; pub mod puzzle_publication_status_type; pub mod puzzle_publish_input_type; +pub mod puzzle_recommended_next_work_type; +pub mod puzzle_result_draft_type; +pub mod puzzle_result_preview_blocker_type; +pub mod puzzle_result_preview_envelope_type; +pub mod puzzle_result_preview_finding_type; pub mod puzzle_run_drag_input_type; pub mod puzzle_run_get_input_type; pub mod puzzle_run_next_level_input_type; pub mod puzzle_run_pause_input_type; pub mod puzzle_run_procedure_result_type; pub mod puzzle_run_prop_input_type; +pub mod puzzle_run_snapshot_type; pub mod puzzle_run_start_input_type; pub mod puzzle_run_swap_input_type; +pub mod puzzle_runtime_level_snapshot_type; +pub mod puzzle_runtime_level_status_type; pub mod puzzle_runtime_run_row_type; pub mod puzzle_runtime_run_table; pub mod puzzle_select_cover_image_input_type; @@ -537,6 +577,7 @@ pub mod puzzle_work_point_incentive_claim_input_type; pub mod puzzle_work_procedure_result_type; pub mod puzzle_work_profile_row_type; pub mod puzzle_work_profile_table; +pub mod puzzle_work_profile_type; pub mod puzzle_work_remix_input_type; pub mod puzzle_work_upsert_input_type; pub mod puzzle_works_list_input_type; @@ -728,30 +769,41 @@ pub mod seed_analytics_date_dimensions_reducer; pub mod select_puzzle_cover_image_procedure; pub mod square_hole_agent_message_finalize_input_type; pub mod square_hole_agent_message_row_type; +pub mod square_hole_agent_message_snapshot_type; pub mod square_hole_agent_message_submit_input_type; pub mod square_hole_agent_message_table; pub mod square_hole_agent_session_create_input_type; pub mod square_hole_agent_session_get_input_type; pub mod square_hole_agent_session_procedure_result_type; pub mod square_hole_agent_session_row_type; +pub mod square_hole_agent_session_snapshot_type; pub mod square_hole_agent_session_table; +pub mod square_hole_creator_config_snapshot_type; pub mod square_hole_draft_compile_input_type; +pub mod square_hole_draft_snapshot_type; +pub mod square_hole_drop_feedback_snapshot_type; pub mod square_hole_drop_shape_procedure_result_type; +pub mod square_hole_hole_option_snapshot_type; +pub mod square_hole_hole_snapshot_type; pub mod square_hole_run_drop_input_type; pub mod square_hole_run_get_input_type; pub mod square_hole_run_procedure_result_type; pub mod square_hole_run_restart_input_type; +pub mod square_hole_run_snapshot_type; pub mod square_hole_run_start_input_type; pub mod square_hole_run_stop_input_type; pub mod square_hole_run_time_up_input_type; pub mod square_hole_runtime_run_row_type; pub mod square_hole_runtime_run_table; +pub mod square_hole_shape_option_snapshot_type; +pub mod square_hole_shape_snapshot_type; pub mod square_hole_work_delete_input_type; pub mod square_hole_work_get_input_type; pub mod square_hole_work_procedure_result_type; pub mod square_hole_work_profile_row_type; pub mod square_hole_work_profile_table; pub mod square_hole_work_publish_input_type; +pub mod square_hole_work_snapshot_type; pub mod square_hole_work_update_input_type; pub mod square_hole_works_list_input_type; pub mod square_hole_works_procedure_result_type; @@ -828,24 +880,31 @@ pub mod user_browse_history_table; pub mod user_browse_history_type; pub mod visual_novel_agent_message_finalize_input_type; pub mod visual_novel_agent_message_row_type; +pub mod visual_novel_agent_message_snapshot_type; pub mod visual_novel_agent_message_submit_input_type; pub mod visual_novel_agent_message_table; pub mod visual_novel_agent_session_create_input_type; pub mod visual_novel_agent_session_get_input_type; pub mod visual_novel_agent_session_procedure_result_type; pub mod visual_novel_agent_session_row_type; +pub mod visual_novel_agent_session_snapshot_type; pub mod visual_novel_agent_session_table; pub mod visual_novel_history_procedure_result_type; +pub mod visual_novel_json_field_type; +pub mod visual_novel_json_value_type; pub mod visual_novel_run_get_input_type; pub mod visual_novel_run_procedure_result_type; +pub mod visual_novel_run_snapshot_type; pub mod visual_novel_run_snapshot_upsert_input_type; pub mod visual_novel_run_start_input_type; pub mod visual_novel_runtime_event_procedure_result_type; pub mod visual_novel_runtime_event_record_input_type; +pub mod visual_novel_runtime_event_snapshot_type; pub mod visual_novel_runtime_event_table; pub mod visual_novel_runtime_event_type; pub mod visual_novel_runtime_history_append_input_type; pub mod visual_novel_runtime_history_entry_row_type; +pub mod visual_novel_runtime_history_entry_snapshot_type; pub mod visual_novel_runtime_history_entry_table; pub mod visual_novel_runtime_history_list_input_type; pub mod visual_novel_runtime_run_row_type; @@ -857,6 +916,7 @@ pub mod visual_novel_work_procedure_result_type; pub mod visual_novel_work_profile_row_type; pub mod visual_novel_work_profile_table; pub mod visual_novel_work_publish_input_type; +pub mod visual_novel_work_snapshot_type; pub mod visual_novel_work_update_input_type; pub mod visual_novel_works_list_input_type; pub mod visual_novel_works_procedure_result_type; @@ -950,6 +1010,7 @@ pub use auth_store_snapshot_type::AuthStoreSnapshot; pub use auth_store_snapshot_upsert_input_type::AuthStoreSnapshotUpsertInput; pub use authorize_database_migration_operator_procedure::authorize_database_migration_operator; pub use bark_battle_draft_config_row_type::BarkBattleDraftConfigRow; +pub use bark_battle_draft_config_snapshot_type::BarkBattleDraftConfigSnapshot; pub use bark_battle_draft_config_table::*; pub use bark_battle_draft_config_upsert_input_type::BarkBattleDraftConfigUpsertInput; pub use bark_battle_draft_create_input_type::BarkBattleDraftCreateInput; @@ -962,8 +1023,10 @@ pub use bark_battle_published_config_row_type::BarkBattlePublishedConfigRow; pub use bark_battle_published_config_table::*; pub use bark_battle_run_finish_input_type::BarkBattleRunFinishInput; pub use bark_battle_run_get_input_type::BarkBattleRunGetInput; +pub use bark_battle_run_snapshot_type::BarkBattleRunSnapshot; pub use bark_battle_run_start_input_type::BarkBattleRunStartInput; pub use bark_battle_runtime_config_get_input_type::BarkBattleRuntimeConfigGetInput; +pub use bark_battle_runtime_config_snapshot_type::BarkBattleRuntimeConfigSnapshot; pub use bark_battle_runtime_run_row_type::BarkBattleRuntimeRunRow; pub use bark_battle_runtime_run_table::*; pub use bark_battle_score_record_row_type::BarkBattleScoreRecordRow; @@ -1015,16 +1078,20 @@ pub use big_fish_run_get_input_type::BigFishRunGetInput; pub use big_fish_run_procedure_result_type::BigFishRunProcedureResult; pub use big_fish_run_start_input_type::BigFishRunStartInput; pub use big_fish_run_status_type::BigFishRunStatus; +pub use big_fish_runtime_entity_snapshot_type::BigFishRuntimeEntitySnapshot; pub use big_fish_runtime_params_type::BigFishRuntimeParams; pub use big_fish_runtime_run_table::*; pub use big_fish_runtime_run_type::BigFishRuntimeRun; +pub use big_fish_runtime_snapshot_type::BigFishRuntimeSnapshot; pub use big_fish_session_create_input_type::BigFishSessionCreateInput; pub use big_fish_session_get_input_type::BigFishSessionGetInput; pub use big_fish_session_procedure_result_type::BigFishSessionProcedureResult; pub use big_fish_session_snapshot_type::BigFishSessionSnapshot; +pub use big_fish_vector_2_type::BigFishVector2; pub use big_fish_work_delete_input_type::BigFishWorkDeleteInput; pub use big_fish_work_like_record_input_type::BigFishWorkLikeRecordInput; pub use big_fish_work_remix_input_type::BigFishWorkRemixInput; +pub use big_fish_work_summary_snapshot_type::BigFishWorkSummarySnapshot; pub use big_fish_works_list_input_type::BigFishWorksListInput; pub use big_fish_works_procedure_result_type::BigFishWorksProcedureResult; pub use bind_asset_object_to_entity_and_return_procedure::bind_asset_object_to_entity_and_return; @@ -1257,30 +1324,38 @@ pub use list_visual_novel_works_procedure::list_visual_novel_works; pub use mark_profile_recharge_order_paid_and_return_procedure::mark_profile_recharge_order_paid_and_return; pub use match_3_d_agent_message_finalize_input_type::Match3DAgentMessageFinalizeInput; pub use match_3_d_agent_message_row_type::Match3DAgentMessageRow; +pub use match_3_d_agent_message_snapshot_type::Match3DAgentMessageSnapshot; pub use match_3_d_agent_message_submit_input_type::Match3DAgentMessageSubmitInput; pub use match_3_d_agent_message_table::*; pub use match_3_d_agent_session_create_input_type::Match3DAgentSessionCreateInput; pub use match_3_d_agent_session_get_input_type::Match3DAgentSessionGetInput; pub use match_3_d_agent_session_procedure_result_type::Match3DAgentSessionProcedureResult; pub use match_3_d_agent_session_row_type::Match3DAgentSessionRow; +pub use match_3_d_agent_session_snapshot_type::Match3DAgentSessionSnapshot; pub use match_3_d_agent_session_table::*; pub use match_3_d_click_item_procedure_result_type::Match3DClickItemProcedureResult; +pub use match_3_d_creator_config_snapshot_type::Match3DCreatorConfigSnapshot; pub use match_3_d_draft_compile_input_type::Match3DDraftCompileInput; +pub use match_3_d_draft_snapshot_type::Match3DDraftSnapshot; +pub use match_3_d_item_snapshot_type::Match3DItemSnapshot; pub use match_3_d_run_click_input_type::Match3DRunClickInput; pub use match_3_d_run_get_input_type::Match3DRunGetInput; pub use match_3_d_run_procedure_result_type::Match3DRunProcedureResult; pub use match_3_d_run_restart_input_type::Match3DRunRestartInput; +pub use match_3_d_run_snapshot_type::Match3DRunSnapshot; pub use match_3_d_run_start_input_type::Match3DRunStartInput; pub use match_3_d_run_stop_input_type::Match3DRunStopInput; pub use match_3_d_run_time_up_input_type::Match3DRunTimeUpInput; pub use match_3_d_runtime_run_row_type::Match3DRuntimeRunRow; pub use match_3_d_runtime_run_table::*; +pub use match_3_d_tray_slot_snapshot_type::Match3DTraySlotSnapshot; pub use match_3_d_work_delete_input_type::Match3DWorkDeleteInput; pub use match_3_d_work_get_input_type::Match3DWorkGetInput; pub use match_3_d_work_procedure_result_type::Match3DWorkProcedureResult; pub use match_3_d_work_profile_row_type::Match3DWorkProfileRow; pub use match_3_d_work_profile_table::*; pub use match_3_d_work_publish_input_type::Match3DWorkPublishInput; +pub use match_3_d_work_snapshot_type::Match3DWorkSnapshot; pub use match_3_d_work_update_input_type::Match3DWorkUpdateInput; pub use match_3_d_works_list_input_type::Match3DWorksListInput; pub use match_3_d_works_procedure_result_type::Match3DWorksProcedureResult; @@ -1354,33 +1429,58 @@ pub use puzzle_agent_message_finalize_input_type::PuzzleAgentMessageFinalizeInpu pub use puzzle_agent_message_kind_type::PuzzleAgentMessageKind; pub use puzzle_agent_message_role_type::PuzzleAgentMessageRole; pub use puzzle_agent_message_row_type::PuzzleAgentMessageRow; +pub use puzzle_agent_message_snapshot_type::PuzzleAgentMessageSnapshot; pub use puzzle_agent_message_submit_input_type::PuzzleAgentMessageSubmitInput; pub use puzzle_agent_message_table::*; pub use puzzle_agent_session_create_input_type::PuzzleAgentSessionCreateInput; pub use puzzle_agent_session_get_input_type::PuzzleAgentSessionGetInput; pub use puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; pub use puzzle_agent_session_row_type::PuzzleAgentSessionRow; +pub use puzzle_agent_session_snapshot_type::PuzzleAgentSessionSnapshot; pub use puzzle_agent_session_table::*; pub use puzzle_agent_stage_type::PuzzleAgentStage; +pub use puzzle_agent_suggested_action_type::PuzzleAgentSuggestedAction; +pub use puzzle_anchor_item_type::PuzzleAnchorItem; +pub use puzzle_anchor_pack_type::PuzzleAnchorPack; +pub use puzzle_anchor_status_type::PuzzleAnchorStatus; +pub use puzzle_audio_asset_type::PuzzleAudioAsset; +pub use puzzle_board_snapshot_type::PuzzleBoardSnapshot; +pub use puzzle_cell_position_type::PuzzleCellPosition; +pub use puzzle_creator_intent_type::PuzzleCreatorIntent; pub use puzzle_draft_compile_input_type::PuzzleDraftCompileInput; +pub use puzzle_draft_level_type::PuzzleDraftLevel; pub use puzzle_event_kind_type::PuzzleEventKind; pub use puzzle_event_table::*; pub use puzzle_event_type::PuzzleEvent; pub use puzzle_form_draft_save_input_type::PuzzleFormDraftSaveInput; +pub use puzzle_form_draft_type::PuzzleFormDraft; +pub use puzzle_gallery_view_table::*; +pub use puzzle_generated_image_candidate_type::PuzzleGeneratedImageCandidate; pub use puzzle_generated_images_save_input_type::PuzzleGeneratedImagesSaveInput; pub use puzzle_leaderboard_entry_row_type::PuzzleLeaderboardEntryRow; pub use puzzle_leaderboard_entry_table::*; +pub use puzzle_leaderboard_entry_type::PuzzleLeaderboardEntry; pub use puzzle_leaderboard_submit_input_type::PuzzleLeaderboardSubmitInput; +pub use puzzle_merged_group_state_type::PuzzleMergedGroupState; +pub use puzzle_piece_state_type::PuzzlePieceState; pub use puzzle_publication_status_type::PuzzlePublicationStatus; pub use puzzle_publish_input_type::PuzzlePublishInput; +pub use puzzle_recommended_next_work_type::PuzzleRecommendedNextWork; +pub use puzzle_result_draft_type::PuzzleResultDraft; +pub use puzzle_result_preview_blocker_type::PuzzleResultPreviewBlocker; +pub use puzzle_result_preview_envelope_type::PuzzleResultPreviewEnvelope; +pub use puzzle_result_preview_finding_type::PuzzleResultPreviewFinding; pub use puzzle_run_drag_input_type::PuzzleRunDragInput; pub use puzzle_run_get_input_type::PuzzleRunGetInput; pub use puzzle_run_next_level_input_type::PuzzleRunNextLevelInput; pub use puzzle_run_pause_input_type::PuzzleRunPauseInput; pub use puzzle_run_procedure_result_type::PuzzleRunProcedureResult; pub use puzzle_run_prop_input_type::PuzzleRunPropInput; +pub use puzzle_run_snapshot_type::PuzzleRunSnapshot; pub use puzzle_run_start_input_type::PuzzleRunStartInput; pub use puzzle_run_swap_input_type::PuzzleRunSwapInput; +pub use puzzle_runtime_level_snapshot_type::PuzzleRuntimeLevelSnapshot; +pub use puzzle_runtime_level_status_type::PuzzleRuntimeLevelStatus; pub use puzzle_runtime_run_row_type::PuzzleRuntimeRunRow; pub use puzzle_runtime_run_table::*; pub use puzzle_select_cover_image_input_type::PuzzleSelectCoverImageInput; @@ -1392,6 +1492,7 @@ pub use puzzle_work_point_incentive_claim_input_type::PuzzleWorkPointIncentiveCl pub use puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; pub use puzzle_work_profile_row_type::PuzzleWorkProfileRow; pub use puzzle_work_profile_table::*; +pub use puzzle_work_profile_type::PuzzleWorkProfile; pub use puzzle_work_remix_input_type::PuzzleWorkRemixInput; pub use puzzle_work_upsert_input_type::PuzzleWorkUpsertInput; pub use puzzle_works_list_input_type::PuzzleWorksListInput; @@ -1583,30 +1684,41 @@ pub use seed_analytics_date_dimensions_reducer::seed_analytics_date_dimensions; pub use select_puzzle_cover_image_procedure::select_puzzle_cover_image; pub use square_hole_agent_message_finalize_input_type::SquareHoleAgentMessageFinalizeInput; pub use square_hole_agent_message_row_type::SquareHoleAgentMessageRow; +pub use square_hole_agent_message_snapshot_type::SquareHoleAgentMessageSnapshot; pub use square_hole_agent_message_submit_input_type::SquareHoleAgentMessageSubmitInput; pub use square_hole_agent_message_table::*; pub use square_hole_agent_session_create_input_type::SquareHoleAgentSessionCreateInput; pub use square_hole_agent_session_get_input_type::SquareHoleAgentSessionGetInput; pub use square_hole_agent_session_procedure_result_type::SquareHoleAgentSessionProcedureResult; pub use square_hole_agent_session_row_type::SquareHoleAgentSessionRow; +pub use square_hole_agent_session_snapshot_type::SquareHoleAgentSessionSnapshot; pub use square_hole_agent_session_table::*; +pub use square_hole_creator_config_snapshot_type::SquareHoleCreatorConfigSnapshot; pub use square_hole_draft_compile_input_type::SquareHoleDraftCompileInput; +pub use square_hole_draft_snapshot_type::SquareHoleDraftSnapshot; +pub use square_hole_drop_feedback_snapshot_type::SquareHoleDropFeedbackSnapshot; pub use square_hole_drop_shape_procedure_result_type::SquareHoleDropShapeProcedureResult; +pub use square_hole_hole_option_snapshot_type::SquareHoleHoleOptionSnapshot; +pub use square_hole_hole_snapshot_type::SquareHoleHoleSnapshot; pub use square_hole_run_drop_input_type::SquareHoleRunDropInput; pub use square_hole_run_get_input_type::SquareHoleRunGetInput; pub use square_hole_run_procedure_result_type::SquareHoleRunProcedureResult; pub use square_hole_run_restart_input_type::SquareHoleRunRestartInput; +pub use square_hole_run_snapshot_type::SquareHoleRunSnapshot; pub use square_hole_run_start_input_type::SquareHoleRunStartInput; pub use square_hole_run_stop_input_type::SquareHoleRunStopInput; pub use square_hole_run_time_up_input_type::SquareHoleRunTimeUpInput; pub use square_hole_runtime_run_row_type::SquareHoleRuntimeRunRow; pub use square_hole_runtime_run_table::*; +pub use square_hole_shape_option_snapshot_type::SquareHoleShapeOptionSnapshot; +pub use square_hole_shape_snapshot_type::SquareHoleShapeSnapshot; pub use square_hole_work_delete_input_type::SquareHoleWorkDeleteInput; pub use square_hole_work_get_input_type::SquareHoleWorkGetInput; pub use square_hole_work_procedure_result_type::SquareHoleWorkProcedureResult; pub use square_hole_work_profile_row_type::SquareHoleWorkProfileRow; pub use square_hole_work_profile_table::*; pub use square_hole_work_publish_input_type::SquareHoleWorkPublishInput; +pub use square_hole_work_snapshot_type::SquareHoleWorkSnapshot; pub use square_hole_work_update_input_type::SquareHoleWorkUpdateInput; pub use square_hole_works_list_input_type::SquareHoleWorksListInput; pub use square_hole_works_procedure_result_type::SquareHoleWorksProcedureResult; @@ -1683,24 +1795,31 @@ pub use user_browse_history_table::*; pub use user_browse_history_type::UserBrowseHistory; pub use visual_novel_agent_message_finalize_input_type::VisualNovelAgentMessageFinalizeInput; pub use visual_novel_agent_message_row_type::VisualNovelAgentMessageRow; +pub use visual_novel_agent_message_snapshot_type::VisualNovelAgentMessageSnapshot; pub use visual_novel_agent_message_submit_input_type::VisualNovelAgentMessageSubmitInput; pub use visual_novel_agent_message_table::*; pub use visual_novel_agent_session_create_input_type::VisualNovelAgentSessionCreateInput; pub use visual_novel_agent_session_get_input_type::VisualNovelAgentSessionGetInput; pub use visual_novel_agent_session_procedure_result_type::VisualNovelAgentSessionProcedureResult; pub use visual_novel_agent_session_row_type::VisualNovelAgentSessionRow; +pub use visual_novel_agent_session_snapshot_type::VisualNovelAgentSessionSnapshot; pub use visual_novel_agent_session_table::*; pub use visual_novel_history_procedure_result_type::VisualNovelHistoryProcedureResult; +pub use visual_novel_json_field_type::VisualNovelJsonField; +pub use visual_novel_json_value_type::VisualNovelJsonValue; pub use visual_novel_run_get_input_type::VisualNovelRunGetInput; pub use visual_novel_run_procedure_result_type::VisualNovelRunProcedureResult; +pub use visual_novel_run_snapshot_type::VisualNovelRunSnapshot; pub use visual_novel_run_snapshot_upsert_input_type::VisualNovelRunSnapshotUpsertInput; pub use visual_novel_run_start_input_type::VisualNovelRunStartInput; pub use visual_novel_runtime_event_procedure_result_type::VisualNovelRuntimeEventProcedureResult; pub use visual_novel_runtime_event_record_input_type::VisualNovelRuntimeEventRecordInput; +pub use visual_novel_runtime_event_snapshot_type::VisualNovelRuntimeEventSnapshot; pub use visual_novel_runtime_event_table::*; pub use visual_novel_runtime_event_type::VisualNovelRuntimeEvent; pub use visual_novel_runtime_history_append_input_type::VisualNovelRuntimeHistoryAppendInput; pub use visual_novel_runtime_history_entry_row_type::VisualNovelRuntimeHistoryEntryRow; +pub use visual_novel_runtime_history_entry_snapshot_type::VisualNovelRuntimeHistoryEntrySnapshot; pub use visual_novel_runtime_history_entry_table::*; pub use visual_novel_runtime_history_list_input_type::VisualNovelRuntimeHistoryListInput; pub use visual_novel_runtime_run_row_type::VisualNovelRuntimeRunRow; @@ -1712,6 +1831,7 @@ pub use visual_novel_work_procedure_result_type::VisualNovelWorkProcedureResult; pub use visual_novel_work_profile_row_type::VisualNovelWorkProfileRow; pub use visual_novel_work_profile_table::*; pub use visual_novel_work_publish_input_type::VisualNovelWorkPublishInput; +pub use visual_novel_work_snapshot_type::VisualNovelWorkSnapshot; pub use visual_novel_work_update_input_type::VisualNovelWorkUpdateInput; pub use visual_novel_works_list_input_type::VisualNovelWorksListInput; pub use visual_novel_works_procedure_result_type::VisualNovelWorksProcedureResult; @@ -2052,6 +2172,7 @@ pub struct DbUpdate { puzzle_agent_message: __sdk::TableUpdate, puzzle_agent_session: __sdk::TableUpdate, puzzle_event: __sdk::TableUpdate, + puzzle_gallery_view: __sdk::TableUpdate, puzzle_leaderboard_entry: __sdk::TableUpdate, puzzle_runtime_run: __sdk::TableUpdate, puzzle_work_profile: __sdk::TableUpdate, @@ -2287,6 +2408,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "puzzle_event" => db_update .puzzle_event .append(puzzle_event_table::parse_table_update(table_update)?), + "puzzle_gallery_view" => db_update + .puzzle_gallery_view + .append(puzzle_gallery_view_table::parse_table_update(table_update)?), "puzzle_leaderboard_entry" => db_update.puzzle_leaderboard_entry.append( puzzle_leaderboard_entry_table::parse_table_update(table_update)?, ), @@ -2842,6 +2966,10 @@ impl __sdk::DbUpdate for DbUpdate { &self.visual_novel_work_profile, ) .with_updates_by_pk(|row| &row.profile_id); + diff.puzzle_gallery_view = cache.apply_diff_to_table::( + "puzzle_gallery_view", + &self.puzzle_gallery_view, + ); diff } @@ -3041,6 +3169,9 @@ impl __sdk::DbUpdate for DbUpdate { "puzzle_event" => db_update .puzzle_event .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_gallery_view" => db_update + .puzzle_gallery_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "puzzle_leaderboard_entry" => db_update .puzzle_leaderboard_entry .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -3321,6 +3452,9 @@ impl __sdk::DbUpdate for DbUpdate { "puzzle_event" => db_update .puzzle_event .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_gallery_view" => db_update + .puzzle_gallery_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "puzzle_leaderboard_entry" => db_update .puzzle_leaderboard_entry .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3477,6 +3611,7 @@ pub struct AppliedDiff<'r> { puzzle_agent_message: __sdk::TableAppliedDiff<'r, PuzzleAgentMessageRow>, puzzle_agent_session: __sdk::TableAppliedDiff<'r, PuzzleAgentSessionRow>, puzzle_event: __sdk::TableAppliedDiff<'r, PuzzleEvent>, + puzzle_gallery_view: __sdk::TableAppliedDiff<'r, PuzzleWorkProfile>, puzzle_leaderboard_entry: __sdk::TableAppliedDiff<'r, PuzzleLeaderboardEntryRow>, puzzle_runtime_run: __sdk::TableAppliedDiff<'r, PuzzleRuntimeRunRow>, puzzle_work_profile: __sdk::TableAppliedDiff<'r, PuzzleWorkProfileRow>, @@ -3824,6 +3959,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.puzzle_event, event, ); + callbacks.invoke_table_row_callbacks::( + "puzzle_gallery_view", + &self.puzzle_gallery_view, + event, + ); callbacks.invoke_table_row_callbacks::( "puzzle_leaderboard_entry", &self.puzzle_leaderboard_entry, @@ -4665,6 +4805,7 @@ impl __sdk::SpacetimeModule for RemoteModule { puzzle_agent_message_table::register_table(client_cache); puzzle_agent_session_table::register_table(client_cache); puzzle_event_table::register_table(client_cache); + puzzle_gallery_view_table::register_table(client_cache); puzzle_leaderboard_entry_table::register_table(client_cache); puzzle_runtime_run_table::register_table(client_cache); puzzle_work_profile_table::register_table(client_cache); @@ -4756,6 +4897,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "puzzle_agent_message", "puzzle_agent_session", "puzzle_event", + "puzzle_gallery_view", "puzzle_leaderboard_entry", "puzzle_runtime_run", "puzzle_work_profile", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_view_table.rs new file mode 100644 index 00000000..24857cee --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_view_table.rs @@ -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, + 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::("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 + '_ { + 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) { + let _table = client_cache.get_or_make_table::("puzzle_gallery_view"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "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; +} + +impl puzzle_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn puzzle_gallery_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("puzzle_gallery_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/puzzle.rs b/server-rs/crates/spacetime-client/src/puzzle.rs index 30f21887..5426e756 100644 --- a/server-rs/crates/spacetime-client/src/puzzle.rs +++ b/server-rs/crates/spacetime-client/src/puzzle.rs @@ -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 { + 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, 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::>(); + 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 } diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs index 703e880e..28f75c1e 100644 --- a/server-rs/crates/spacetime-module/src/puzzle.rs +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -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 { + 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::>(); + 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::, _>>()?; 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::>() { 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::>() { ctx.db.puzzle_runtime_run().run_id().delete(&run.run_id); @@ -1481,8 +1509,8 @@ fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result, 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::>(); let profile_ids = rows .iter() @@ -2542,8 +2570,8 @@ fn list_session_messages(ctx: &TxContext, session_id: &str) -> Vec Result, 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::>(); rows.sort_by(|left, right| { left.best_elapsed_ms