Skip to main content

rust-server

SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust.

소스 정보

저장소
clockworklabs/SpacetimeDB
최근 소스 활동
2026년 11월 10일 16:31
감지된 SKILL.md 언어
영어
스타
25,541
포크
1,119

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
rust-server
description
SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust.
license
Apache-2.0
metadata
{"author":"clockworklabs","version":"2.0","role":"server","language":"rust","cursor_globs":"**/*.rs","cursor_always_apply":true}
# SpacetimeDB Rust SDK Reference ## Imports ```rust use spacetimedb::{ procedure, reducer, table, Filter, Identity, ProcedureContext, Query, ReducerContext, SpacetimeType, Table, ConnectionId, ScheduleAt, TimeDuration, Timestamp, Uuid, }; ``` **`Table` is required.** Without it, `ctx.db.*.insert()`, `.iter()`, `.find()` etc. won't compile (`no method named 'insert' found`). ## Tables `#[spacetimedb::table(...)]` on a `pub struct`. `accessor` must be snake_case: ```rust #[spacetimedb::table(accessor = entity, public)] pub struct Entity { #[primary_key] #[auto_inc] pub id: u64, pub owner: Identity, pub name: String, #[index(btree)] pub active: bool, } ``` Options: `accessor = snake_case` (required), `public`, `scheduled(reducer_fn)`, `index(...)` `ctx.db` accessors use the `accessor` name (snake_case). ## Column Types | Rust type | Notes | |-----------|-------| | `u8` / `u16` / `u32` / `u64` / `u128` | unsigned integers | | `i8` / `i16` / `i32` / `i64` / `i128` | signed integers | | `spacetimedb::sats::u256` / `spacetimedb::sats::i256` | 256-bit integers | | `f32` / `f64` | floats | | `bool` | boolean | | `String` | text | | `Vec<T>` | list/array | | `Identity` | user identity | | `ConnectionId` | connection handle | | `Timestamp` | server timestamp (microseconds since epoch) | | `TimeDuration` | duration in microseconds | | `Uuid` | UUID | | `Option<T>` | nullable column | ## Column Attributes ```rust #[primary_key] // primary key #[auto_inc] // auto-increment (use 0 as placeholder on insert) #[unique] // unique constraint #[index(btree)] // btree index (enables .filter() on this column) #[default(true)] // migration-safe default for a newly appended column ``` Defaults support compatible addition of a newly appended field. Do not place `#[default(...)]` on primary-key, unique, or auto-increment columns. ## Indexes Prefer `#[index(btree)]` inline for single-column. Multi-column uses table-level: ```rust // Inline (preferred for single-column): #[index(btree)] pub author_id: u64, // Access: ctx.db.post().author_id().filter(author_id) // Multi-column (table-level): #[spacetimedb::table(accessor = membership, public, index(accessor = by_group_user, btree(columns = [group_id, user_id])) )] pub struct Membership { pub group_id: u64, pub user_id: Identity, ... } // Access: ctx.db.membership().by_group_user().filter((group_id, &user_id)) ``` When you frequently look up rows by multiple columns, prefer a multi-column index over filtering by one column and looping over the results. ## Reducers ```rust #[spacetimedb::reducer] pub fn create_entity(ctx: &ReducerContext, name: String) { ctx.db.entity().insert(Entity { id: 0, owner: ctx.sender(), name, active: true }); } // Reducers can return Result<(), String> or Result<(), E> where E: Display #[spacetimedb::reducer] pub fn validate_entity(ctx: &ReducerContext, name: String) -> Result<(), String> { if name.is_empty() { return Err("Name cannot be empty".to_string()); } ctx.db.entity().try_insert(Entity { id: 0, owner: ctx.sender(), name, active: true })?; Ok(()) } ``` Note: `insert()` panics on constraint violations. Use `try_insert()` with `?` when returning `Result`. ## DB Operations ```rust ctx.db.entity().insert(Entity { id: 0, name: "Sample".into() }); // Insert (0 for autoInc) ctx.db.entity().id().find(entity_id); // Find by PK → Option<Entity> ctx.db.entity().identity().find(ctx.sender()); // Find by unique column → Option<Entity> ctx.db.item().author_id().filter(author_id); // Filter by index → iterator ctx.db.entity().iter(); // All rows → iterator ctx.db.entity().count(); // Count rows ctx.db.entity().id().update(Entity { name: new_name, ..existing }); // Update (override + spread) ctx.db.entity().id().delete(entity_id); // Delete by PK ctx.db.entity().name().delete("Alice".to_string()); // Delete by indexed String column ``` Note: `iter()` and `filter()` return iterators. Collect to Vec if you need `.sort()`, `.filter()`, `.map()`. Range queries on btree indexes: `filter(18..=65)`, `filter(18..)`, `filter(..18)`. String column accessors operate on the column's owned `String` type, not `&str`. Pass a `String` or `&String` to `find` and `delete`; index filters borrow the key, as in `ctx.db.product().category().filter(&"hardware".to_string())`. ## Lifecycle Hooks ```rust #[spacetimedb::reducer(init)] pub fn init(ctx: &ReducerContext) { ... } #[spacetimedb::reducer(client_connected)] pub fn on_connect(ctx: &ReducerContext) { ... } #[spacetimedb::reducer(client_disconnected)] pub fn on_disconnect(ctx: &ReducerContext) { ... } ``` The current connection ID is available through `ctx.connection_id()` (not a public field) and may be absent outside connection-scoped calls. ## Views ```rust // Anonymous view (same result for all clients): use spacetimedb::{view, AnonymousViewContext}; #[view(accessor = active_users, public)] fn active_users(ctx: &AnonymousViewContext) -> Vec<Entity> { ctx.db.entity().active().filter(true).collect() } // Per-user view (result varies by sender): use spacetimedb::{view, ViewContext}; #[view(accessor = my_profile, public)] fn my_profile(ctx: &ViewContext) -> Option<Entity> { ctx.db.entity().identity().find(ctx.sender()) } ``` Procedural-view table handles support indexed `find` and `filter` access, but not full-table `iter()`. Start a procedural view from an appropriate table index. Declare a procedural view primary key in the view attribute: ```rust #[view(accessor = catalog_entry, public, primary_key = sku)] fn catalog_entry(ctx: &AnonymousViewContext) -> Vec<CatalogEntry> { ... } ``` Procedural-view primary keys are explicit schema metadata. Add one only when the view itself is required to expose a primary key; a source table's primary key is not inherited by the view. Query-builder views use `ViewContext`, `ctx.from`, and return `impl Query<Row>`. Use `filter` for predicates and `right_semijoin` when the result should contain right-side rows that have a matching left-side row: ```rust ctx.from.article().filter(|article| article.published.eq(true)) ctx.from.subscription().right_semijoin( ctx.from.account(), |subscription, account| subscription.account_id.eq(account.id), ) ``` Query-builder comparisons against a `String` column take an owned `String`, for example `row.label.eq("active".to_string())`. Views can also accept arguments (2.13+), after the context: ```rust #[spacetimedb::view(accessor = entity_by_id, public)] pub fn entity_by_id(ctx: &spacetimedb::AnonymousViewContext, id: u64) -> Option<Entity> { ctx.db.entity().id().find(id) } ``` Floating-point arguments and primary keys are rejected on argument-bearing views. See client skills for SQL subscription syntax and restrictions. ## Client Visibility Filters ```rust use spacetimedb::Filter; #[spacetimedb::client_visibility_filter] const PRIVATE_NOTE_FILTER: Filter = Filter::Sql("SELECT * FROM owned_row WHERE owner = :sender"); ``` ## Reducer Context API `ReducerContext` (`ctx`) is the only source of sender identity, time, and randomness; stdlib clocks and RNG are unavailable in modules. ```rust // Auth: ctx.sender() is the caller's Identity if row.owner != ctx.sender() { panic!("unauthorized"); // or: return Err(anyhow::anyhow!("unauthorized")); } // Server timestamp (deterministic per reducer call) ctx.db.item().insert(Item { id: 0, owner: ctx.sender(), created_at: ctx.timestamp, .. }); // Timestamp arithmetic let expiry = ctx.timestamp + TimeDuration::from_micros(delay_micros); // Deterministic RNG: ctx.random() for a single value, ctx.rng() for the rand::Rng trait use spacetimedb::rand::Rng; let n: u32 = ctx.random(); // random u32 let roll: u32 = ctx.rng().gen_range(1..=6); // any rand::Rng method // Client: Timestamp → milliseconds since epoch timestamp.to_micros_since_unix_epoch() / 1000 ``` ## Environment Variables ```rust #[spacetimedb::env] pub struct Env { pub API_KEY: String, pub LOG_LEVEL: Option<String>, } // Inside a root entry point: // ctx.env.API_KEY() -> String // ctx.env.LOG_LEVEL() -> Option<String> // ctx.env.get("LOG_LEVEL") -> Option<String> ``` For constrained values, derive `spacetimedb::EnvironmentValue` on a payload-free enum and use it as a field type (or `Option<Enum>`). `#[env(value = "production")]` on a variant supplies its exact accepted string; without it, the Rust variant name is used. `#[env(values("info", "debug"))]` also constrains `String`/`Option<String>` fields. See the concepts skill for shared environment and secret-handling rules. ## Scheduled Tables ```rust #[spacetimedb::table(accessor = tick_timer, scheduled(tick), public)] pub struct TickTimer { #[primary_key] #[auto_inc] scheduled_id: u64, scheduled_at: spacetimedb::ScheduleAt, } #[spacetimedb::reducer] pub fn tick(ctx: &ReducerContext, timer: TickTimer) { // a one-time row is deleted after this runs; an interval row stays } // One-time: fires once at a specific time let at = ScheduleAt::Time(ctx.timestamp + std::time::Duration::from_secs(10)); // Repeating: fires on an interval let at = ScheduleAt::Interval(std::time::Duration::from_secs(5).into()); ctx.db.tick_timer().insert(TickTimer { scheduled_id: 0, scheduled_at: at }); ``` To let only the scheduler run a callback, reject calls where `ctx.sender() != ctx.database_identity()`. Construct a connection ID from a numeric representation with `ConnectionId::from_u128(value)`. ## Inter-Database Calls (Async IDC) A reducer calls a reducer in another database by inserting a row into an outbox table. The call is sent only if the transaction commits, runs exactly once, and runs in order with other calls through the same outbox to the same database. The sending reducer doesn't wait for it. Both databases must be on the same server. Generate receiver bindings by listing the receiving module under `dependencies` in the sending database's `spacetime.json`, then running `spacetime generate <sending-database>`. Rust bindings are written to `src/remote_bindings/`. ```json { "database": "lobby", "module-path": "./lobby", "dependencies": [{ "name": "game_world", "database": "game-world" }],
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기