更新 SpacetimeDB 本地技能
更新 SpacetimeDB CLI、概念和 Rust 模块 skill 到 2.5 口径 删除 TypeScript、C# 和 Unity SpacetimeDB 本地 skill 同步 AGENTS 与 Hermes 决策记录中的 skill 维护范围 补充 2.2.0 到 2.5.0 项目相关差异和 event table 规则
This commit is contained in:
@@ -1,345 +1,105 @@
|
||||
---
|
||||
name: spacetimedb-concepts
|
||||
description: Understand SpacetimeDB architecture and core concepts. Use when learning SpacetimeDB or making architectural decisions.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: clockworklabs
|
||||
version: "2.0"
|
||||
description: Understand SpacetimeDB 2.5 architecture, reducer/procedure/table/view semantics, schema evolution, subscriptions, identity, and Genarrative-specific backend boundaries. Use when designing or reviewing SpacetimeDB-backed features.
|
||||
---
|
||||
|
||||
# SpacetimeDB Core Concepts
|
||||
|
||||
SpacetimeDB is a relational database that is also a server. It lets you upload application logic directly into the database via WebAssembly modules, eliminating the traditional web/game server layer entirely.
|
||||
SpacetimeDB is a relational database that also executes application logic in uploaded modules. In Genarrative, it is the data and transaction layer behind `server-rs + Axum + SpacetimeDB`, not a replacement for the `api-server` BFF or external platform adapters.
|
||||
|
||||
---
|
||||
## Genarrative Boundaries
|
||||
|
||||
## Critical Rules (Read First)
|
||||
- Domain rules live in `module-*`.
|
||||
- SpacetimeDB tables, reducers, procedures, migrations, row mappers, and read models live in `spacetime-module`.
|
||||
- Backend access goes through `spacetime-client` facades.
|
||||
- HTTP/SSE/BFF and external orchestration stay in `api-server`.
|
||||
- External side effects stay in `platform-*`.
|
||||
- Frontend renders backend truth and must not bypass BFF/projections to invent formal business state.
|
||||
|
||||
These five rules prevent the most common SpacetimeDB mistakes:
|
||||
## Critical Rules
|
||||
|
||||
1. **Reducers are transactional** — they do not return data to callers. Use subscriptions to read data.
|
||||
2. **Reducers must be deterministic** — no filesystem, network, timers, or random. All state must come from tables.
|
||||
3. **Read data via tables/subscriptions** — not reducer return values. Clients get data through subscribed queries.
|
||||
4. **Auto-increment IDs are not sequential** — gaps are normal, do not use for ordering. Use timestamps or explicit sequence columns.
|
||||
5. **`ctx.sender()` is the authenticated principal** — never trust identity passed as arguments. Always use `ctx.sender()` for authorization.
|
||||
|
||||
---
|
||||
|
||||
## Feature Implementation Checklist
|
||||
|
||||
When implementing a feature that spans backend and client:
|
||||
|
||||
1. **Backend:** Define table(s) to store the data
|
||||
2. **Backend:** Define reducer(s) to mutate the data
|
||||
3. **Client:** Subscribe to the table(s)
|
||||
4. **Client:** Call the reducer(s) from UI — **do not skip this step**
|
||||
5. **Client:** Render the data from the table(s)
|
||||
|
||||
**Common mistake:** Building backend tables/reducers but forgetting to wire up the client to call them.
|
||||
|
||||
---
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
When things are not working:
|
||||
|
||||
1. Is SpacetimeDB server running? (`spacetime start`)
|
||||
2. Is the module published? (`spacetime publish`)
|
||||
3. Are client bindings generated? (`spacetime generate`)
|
||||
4. Check server logs for errors (`spacetime logs <db-name>`)
|
||||
5. **Is the reducer actually being called from the client?**
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
spacetime start
|
||||
spacetime publish <db-name> --module-path <module-path>
|
||||
spacetime publish <db-name> --clear-database -y --module-path <module-path>
|
||||
spacetime generate --lang <lang> --out-dir <out> --module-path <module-path>
|
||||
spacetime logs <db-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What SpacetimeDB Is
|
||||
|
||||
SpacetimeDB combines a database and application server into a single deployable unit. Clients connect directly to the database and execute application logic inside it. The system is optimized for real-time applications requiring maximum speed and minimum latency.
|
||||
|
||||
Key characteristics:
|
||||
|
||||
- **In-memory execution**: Application state is served from memory for very low-latency access
|
||||
- **Persistent storage**: Data is automatically persisted to a write-ahead log (WAL) for durability
|
||||
- **Real-time synchronization**: Changes are automatically pushed to subscribed clients
|
||||
- **Single deployment**: No separate servers, containers, or infrastructure to manage
|
||||
|
||||
## The Five Zen Principles
|
||||
|
||||
1. **Everything is a Table**: Your entire application state lives in tables. No separate cache layer, no Redis, no in-memory state to synchronize.
|
||||
2. **Everything is Persistent**: SpacetimeDB persists state by default (for example via WAL-backed durability).
|
||||
3. **Everything is Real-Time**: Clients are replicas of server state. Subscribe to data and it flows automatically.
|
||||
4. **Everything is Transactional**: Every reducer runs atomically. Either all changes succeed or all roll back.
|
||||
5. **Everything is Programmable**: Modules are real code (Rust, C#, TypeScript) running inside the database.
|
||||
1. **Reducers are transactional**: they do not return data to callers. Read through subscriptions, read models, views, or BFF endpoints.
|
||||
2. **Reducers are deterministic**: no filesystem, network, wall-clock, or external RNG. Use `ctx.timestamp`, `ctx.rng()` / `ctx.random()`, and tables.
|
||||
3. **Procedures are stable in 2.5**: they can use explicit transactions and outgoing HTTP via `ctx.http`.
|
||||
4. **Identity comes from context**: use `ctx.sender()` or language equivalent for authorization. Never trust identity passed as an argument.
|
||||
5. **Auto-increment IDs are not ordering guarantees**: gaps are normal. Use timestamps or explicit sequence columns for ordering.
|
||||
6. **Schema changes need migration discipline**: existing Genarrative table fields must be appended with defaults; update migration code, table catalog, generated bindings, and run `npm run check:spacetime-schema`.
|
||||
|
||||
## Tables
|
||||
|
||||
Tables store all data in SpacetimeDB. They use the relational model and support SQL queries for subscriptions.
|
||||
|
||||
### Defining Tables
|
||||
|
||||
Tables are defined using language-specific attributes. In 2.0, use `accessor` (not `name`) for the API name:
|
||||
|
||||
**Rust:**
|
||||
```rust
|
||||
#[spacetimedb::table(accessor = player, public)]
|
||||
pub struct Player {
|
||||
#[primary_key]
|
||||
#[auto_inc]
|
||||
id: u32,
|
||||
#[index(btree)]
|
||||
name: String,
|
||||
#[unique]
|
||||
email: String,
|
||||
}
|
||||
```
|
||||
|
||||
**C#:**
|
||||
```csharp
|
||||
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
|
||||
public partial struct Player
|
||||
{
|
||||
[SpacetimeDB.PrimaryKey]
|
||||
[SpacetimeDB.AutoInc]
|
||||
public uint Id;
|
||||
[SpacetimeDB.Index.BTree]
|
||||
public string Name;
|
||||
[SpacetimeDB.Unique]
|
||||
public string Email;
|
||||
}
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const players = table(
|
||||
{ name: 'players', public: true },
|
||||
{
|
||||
id: t.u32().primaryKey().autoInc(),
|
||||
name: t.string().index('btree'),
|
||||
email: t.string().unique(),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Table Visibility
|
||||
|
||||
- **Private tables** (default): Only accessible by reducers and the database owner
|
||||
- **Public tables**: Exposed for client read access through subscriptions. Writes still require reducers.
|
||||
|
||||
### Table Design Principles
|
||||
|
||||
Organize data by access pattern, not by entity:
|
||||
|
||||
**Decomposed approach (recommended):**
|
||||
```
|
||||
Player PlayerState PlayerStats
|
||||
id <-- player_id player_id
|
||||
name position_x total_kills
|
||||
position_y total_deaths
|
||||
velocity_x play_time
|
||||
```
|
||||
|
||||
Benefits: reduced bandwidth, cache efficiency, schema evolution, semantic clarity.
|
||||
- Private tables are the default; only reducers/procedures and database owners can access them.
|
||||
- Public tables are exposed to clients through subscriptions. Writes still go through reducers/procedures.
|
||||
- Organize data by access pattern when bandwidth or update frequency differs.
|
||||
- Existing persistent tables in Genarrative are conservative: no rename, delete, reorder, or type changes without a user-approved migration plan.
|
||||
|
||||
## Reducers
|
||||
|
||||
Reducers are transactional functions that modify database state. They are the primary client-invoked mutation path; procedures can also mutate tables by running explicit transactions.
|
||||
Reducers are deterministic transactional functions. They are the primary client-invoked mutation path.
|
||||
|
||||
### Key Properties
|
||||
- No global mutable state.
|
||||
- No filesystem, network, timers, or non-deterministic RNG.
|
||||
- Return `Result<(), String>` for expected sender-visible errors.
|
||||
- Use `ctx.sender()` for authorization.
|
||||
- Store persistent state in tables.
|
||||
|
||||
- **Transactional**: Run in isolated database transactions
|
||||
- **Atomic**: Either all changes succeed or all roll back
|
||||
- **Isolated**: Cannot interact with the outside world (no network, no filesystem)
|
||||
- **Callable**: Clients invoke reducers as remote procedure calls
|
||||
## Procedures
|
||||
|
||||
### Critical Reducer Rules
|
||||
Procedures are stable in 2.5. They can be scheduled, can open explicit transactions with `with_tx` / `try_with_tx`, and can use outgoing HTTP (`ctx.http`).
|
||||
|
||||
1. **No global state**: Relying on static variables is undefined behavior
|
||||
2. **No side effects**: Reducers cannot make network requests or access files
|
||||
3. **Store state in tables**: All persistent state must be in tables
|
||||
4. **No return data**: Reducers do not return data to callers — use subscriptions
|
||||
5. **Must be deterministic**: No random, no timers, no external I/O
|
||||
Genarrative default: keep external provider protocols in `platform-*` and orchestration in `api-server` unless a task explicitly moves a workflow into a module procedure.
|
||||
|
||||
### Defining Reducers
|
||||
Module HTTP handlers/webhooks, unstable view features, and RLS `client_visibility_filter` remain gated behind unstable according to the 2.5 release notes.
|
||||
|
||||
**Rust:**
|
||||
```rust
|
||||
#[spacetimedb::reducer]
|
||||
pub fn create_user(ctx: &ReducerContext, name: String, email: String) -> Result<(), String> {
|
||||
if name.is_empty() {
|
||||
return Err("Name cannot be empty".to_string());
|
||||
}
|
||||
ctx.db.user().insert(User { id: 0, name, email });
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
## Views
|
||||
|
||||
**C#:**
|
||||
```csharp
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void CreateUser(ReducerContext ctx, string name, string email)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("Name cannot be empty");
|
||||
ctx.Db.User.Insert(new User { Id = 0, Name = name, Email = email });
|
||||
}
|
||||
```
|
||||
Views expose computed read-only data. In 2.4.1 Rust and TypeScript gained primary key support for procedural views; in 2.5 C# gained the same. Clients can receive `OnUpdate` events when subscribed to such views with primary keys. Ensure the view never returns duplicate primary keys, because that can fail view refresh and roll back the triggering transaction.
|
||||
|
||||
### ReducerContext
|
||||
## Event Tables
|
||||
|
||||
Every reducer receives a `ReducerContext` providing:
|
||||
- **Database**: `ctx.db` (Rust field, TS property) / `ctx.Db` (C# property)
|
||||
- **Sender**: `ctx.sender()` (Rust method) / `ctx.Sender` (C# property) / `ctx.sender` (TS property)
|
||||
- **Connection ID**: `ctx.connection_id()` (Rust method) / `ctx.ConnectionId` (C# property) / `ctx.connectionId` (TS property)
|
||||
- **Timestamp**: `ctx.timestamp` (Rust field, TS property) / `ctx.Timestamp` (C# property)
|
||||
Event tables broadcast reducer/procedure-specific facts to subscribers and must be subscribed explicitly. They are excluded from `subscribe_to_all_tables()`.
|
||||
|
||||
## Event Tables (2.0)
|
||||
2.5 adds broader layout-altering automigrations for event tables, including column removal, reordering, and type changes that regular tables reject. This relaxed migration behavior is for event-only tables, not persistent tables.
|
||||
|
||||
Event tables are the preferred way to broadcast reducer-specific data to clients.
|
||||
Event-table primary keys and constraints are transaction-scoped. They can reject duplicate event rows within one transaction, but event rows are not retained in client cache, so clients observe event tables through insert callbacks only. Do not design Genarrative event tables around `OnUpdate` / `on_update` / `onUpdate`; use a persistent table or a primary-keyed procedural view when update callbacks are required.
|
||||
|
||||
```rust
|
||||
#[table(accessor = damage_event, public, event)]
|
||||
pub struct DamageEvent {
|
||||
pub target: Identity,
|
||||
pub amount: u32,
|
||||
}
|
||||
|
||||
#[reducer]
|
||||
fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
|
||||
ctx.db.damage_event().insert(DamageEvent { target, amount });
|
||||
}
|
||||
```
|
||||
|
||||
Clients subscribe to event tables and use `on_insert` callbacks. Event tables must be subscribed explicitly and are excluded from `subscribe_to_all_tables()`.
|
||||
Official 2.4.1/2.5 release notes document primary-key-backed update callbacks for procedural views, not event tables.
|
||||
|
||||
## Subscriptions
|
||||
|
||||
Subscriptions replicate database rows to clients in real-time.
|
||||
1. Subscribe to SQL queries or generated table/query builders.
|
||||
2. Receive initial matching rows.
|
||||
3. Receive updates when subscribed rows change.
|
||||
4. Render from subscribed data, not reducer return values.
|
||||
|
||||
### How Subscriptions Work
|
||||
Best practices:
|
||||
|
||||
1. **Subscribe**: Register SQL queries describing needed data
|
||||
2. **Receive initial data**: All matching rows are sent immediately
|
||||
3. **Receive updates**: Real-time updates when subscribed rows change
|
||||
4. **React to changes**: Use callbacks (`onInsert`, `onDelete`, `onUpdate`)
|
||||
- Group subscriptions by lifetime.
|
||||
- Subscribe to new data before unsubscribing old data during transitions.
|
||||
- Avoid overlapping queries that duplicate row delivery.
|
||||
- Use indexes for subscribed filters.
|
||||
|
||||
### Subscription Best Practices
|
||||
## 2.2.0 to 2.5.0 Delta
|
||||
|
||||
1. **Group subscriptions by lifetime**: Keep always-needed data separate from temporary subscriptions
|
||||
2. **Subscribe before unsubscribing**: When updating subscriptions, subscribe to new data first
|
||||
3. **Avoid overlapping queries**: Distinct queries returning overlapping data cause redundant processing
|
||||
4. **Use indexes**: Queries on indexed columns are efficient; full table scans are expensive
|
||||
Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
|
||||
|
||||
## Modules
|
||||
- **2.2.0**: v3 WebSocket transport and TS SDK default, safer production operations (`lock`/`unlock`, safer `delete`, better `publish --yes`), TS React `useProcedure`, table clearing APIs, empty-table drop automigration, primary-key migration fixes, bytes-key B-tree support, durability hardening.
|
||||
- **2.3.0**: first-party Godot SDK, more WebSocket pipelining/batching, HTTP/2 backend support, Vue `useProcedure`, Unity 6 WebGL support, commitlog compression/throughput improvements, Rust `DbContext` generics, `ReducerContext::identity` deprecated in favor of `database_identity`, connection lifecycle and unsubscribe fixes.
|
||||
- **2.4.0**: unstable module HTTP handlers/webhooks, faster synchronous WASM reducer runtime, commitlog resume truncation fix for silent data loss risk, better commitlog decode context, V8 heap metrics for procedure workers, JS execution-time billing regression reverted.
|
||||
- **2.4.1**: Rust and TypeScript procedural views can declare primary keys, enabling `OnUpdate` events for subscribed views; fixed index schema from ST tables.
|
||||
- **2.5.0**: procedures are stable, C# procedural views gain primary keys, event tables allow broader layout-altering automigrations, BTreeSet storage makes row insertion deterministic and avoids accidentally quadratic bulk insert behavior, `wasm_memory_bytes` billing metric semantics changed, template version constraints unified, `publish --delete-data` config fallback fixed, CLI `call` accepts hex Identity arguments.
|
||||
|
||||
Modules are WebAssembly bundles containing application logic that runs inside the database.
|
||||
## Debugging Checklist
|
||||
|
||||
### Module Components
|
||||
|
||||
- **Tables**: Define the data schema
|
||||
- **Reducers**: Define callable functions that modify state
|
||||
- **Views**: Define read-only computed queries
|
||||
- **Event Tables**: Broadcast reducer-specific data to clients (2.0)
|
||||
- **Procedures**: (Beta) Functions that can have side effects (HTTP requests)
|
||||
|
||||
### Module Languages
|
||||
|
||||
Server-side modules can be written in: Rust, C#, TypeScript (beta)
|
||||
|
||||
### Module Lifecycle
|
||||
|
||||
1. **Write**: Define tables and reducers in your chosen language
|
||||
2. **Compile**: Build to WebAssembly using the SpacetimeDB CLI
|
||||
3. **Publish**: Upload to a SpacetimeDB host with `spacetime publish`
|
||||
4. **Hot-swap**: Republish to update code without disconnecting clients
|
||||
|
||||
## Identity
|
||||
|
||||
Identity is SpacetimeDB's authentication system based on OpenID Connect (OIDC).
|
||||
|
||||
- **Identity**: A long-lived, globally unique identifier for a user.
|
||||
- **ConnectionId**: Identifies a specific client connection.
|
||||
|
||||
```rust
|
||||
#[spacetimedb::reducer]
|
||||
pub fn do_something(ctx: &ReducerContext) {
|
||||
let caller_identity = ctx.sender(); // Who is calling?
|
||||
// NEVER trust identity passed as a reducer argument
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Providers
|
||||
|
||||
SpacetimeDB works with many OIDC providers, including SpacetimeAuth (built-in), Auth0, Clerk, Keycloak, Google, and GitHub.
|
||||
|
||||
## When to Use SpacetimeDB
|
||||
|
||||
### Ideal Use Cases
|
||||
|
||||
- **Real-time games**: MMOs, multiplayer games, turn-based games
|
||||
- **Collaborative applications**: Document editing, whiteboards, design tools
|
||||
- **Chat and messaging**: Real-time communication with presence
|
||||
- **Live dashboards**: Streaming analytics and monitoring
|
||||
|
||||
### Key Decision Factors
|
||||
|
||||
Choose SpacetimeDB when you need:
|
||||
- Sub-10ms latency for reads and writes
|
||||
- Automatic real-time synchronization
|
||||
- Transactional guarantees for all operations
|
||||
- Simplified architecture (no separate cache, queue, or server)
|
||||
|
||||
### Less Suitable For
|
||||
|
||||
- **Batch analytics**: Optimized for OLTP, not OLAP
|
||||
- **Large blob storage**: Better suited for structured relational data
|
||||
- **Stateless APIs**: Traditional REST APIs do not need real-time sync
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Authentication check in reducer:**
|
||||
```rust
|
||||
#[spacetimedb::reducer]
|
||||
fn admin_action(ctx: &ReducerContext) -> Result<(), String> {
|
||||
let admin = ctx.db.admin().identity().find(&ctx.sender())
|
||||
.ok_or("Not an admin")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Scheduled reducer:**
|
||||
```rust
|
||||
#[spacetimedb::table(accessor = reminder, scheduled(send_reminder))]
|
||||
pub struct Reminder {
|
||||
#[primary_key]
|
||||
#[auto_inc]
|
||||
id: u64,
|
||||
scheduled_at: ScheduleAt,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
fn send_reminder(ctx: &ReducerContext, reminder: Reminder) {
|
||||
log::info!("Reminder: {}", reminder.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
1. Is the Genarrative SpacetimeDB server running? Use `npm run dev:spacetime` locally or host-local `systemctl`.
|
||||
2. Is the module published to the same server the API uses?
|
||||
3. Are generated bindings current? Use `npm run spacetime:generate`.
|
||||
4. Is `api-server` using the same database and token?
|
||||
5. Is the reducer/procedure actually called?
|
||||
6. Did `/healthz` / `/readyz` pass while business SpacetimeDB calls still timeout? Inspect API logs and public route behavior.
|
||||
|
||||
## Editing Behavior
|
||||
|
||||
When modifying SpacetimeDB code:
|
||||
|
||||
- Make the smallest change necessary
|
||||
- Do NOT touch unrelated files, configs, or dependencies
|
||||
- Do NOT invent new SpacetimeDB APIs — use only what exists in docs or this repo
|
||||
- Make the smallest change necessary.
|
||||
- Do not invent SpacetimeDB APIs; verify against current docs, generated bindings, or source.
|
||||
- For Genarrative schema edits, update migration code, table catalog/docs, generated bindings, and relevant tests.
|
||||
- After schema edits, run `npm run spacetime:generate` and `npm run check:spacetime-schema`.
|
||||
|
||||
Reference in New Issue
Block a user