SpacetimeDB Overzicht
Een introductie tot SpacetimeDB en hoe het in dit project wordt gebruikt.
Wat is SpacetimeDB?
SpacetimeDB is een real-time database met ingebouwde applicatielogica. In plaats van een aparte database en backend server, draait alle logica direct in de database als WASM modules.
Traditioneel: SpacetimeDB:
┌─────────┐ ┌─────────────────┐
│ Client │ │ Client │
└────┬────┘ └────────┬────────┘
│ │
┌────▼────┐ ┌────────▼────────┐
│ Backend │ │ SpacetimeDB │
│ Server │ │ ┌───────────┐ │
└────┬────┘ │ │ WASM │ │
│ │ │ Module │ │
┌────▼────┐ │ │ (Rust) │ │
│Database │ │ └───────────┘ │
└─────────┘ └─────────────────┘
Kernconcepten
1. Tabellen
Tabellen slaan data op, vergelijkbaar met SQL tabellen:
#[table(name = player, public)]
pub struct Player {
#[primary_key]
pub identity: Identity,
pub username: String,
pub created_at: Timestamp,
}
2. Reducers
Reducers zijn functies die data wijzigen. Ze zijn de "API endpoints":
#[reducer]
pub fn create_player(ctx: &ReducerContext, username: String) -> Result<(), String> {
// Logica hier
ctx.db.player().insert(Player {
identity: ctx.sender,
username,
created_at: ctx.timestamp,
});
Ok(())
}
3. Identity
Elke client heeft een unieke cryptografische identiteit:
// ctx.sender bevat de identity van de aanroeper
let caller_identity: Identity = ctx.sender;
4. Subscriptions
Clients abonneren zich op data en krijgen automatisch updates:
# Godot voorbeeld
SpacetimeDB.subscribe(["SELECT * FROM player"])
Project Module Structuur
Onze SpacetimeDB module bevindt zich in server/src/lib.rs:
use spacetimedb::{table, reducer, Identity, Timestamp, ReducerContext};
// === TABELLEN ===
#[table(name = player, public)]
pub struct Player { ... }
#[table(name = player_stats, public)]
pub struct PlayerStats { ... }
// ... meer tabellen
// === REDUCERS ===
#[reducer]
pub fn register_player(...) { ... }
#[reducer]
pub fn update_player_position(...) { ... }
// ... meer reducers
Visibility
Tabellen kunnen public of private zijn:
| Type | Beschrijving |
|---|---|
public | Alle clients kunnen lezen |
private | Alleen via reducers toegankelijk |
#[table(name = player, public)] // Iedereen kan zien
pub struct Player { ... }
#[table(name = admin_log, private)] // Alleen server
pub struct AdminLog { ... }
Indexes
Automatische indexen voor snelle queries:
#[table(name = player, public)]
pub struct Player {
#[primary_key]
pub identity: Identity, // Automatisch geïndexeerd
#[unique]
pub username: String, // Unieke index
#[index(btree)]
pub level: u32, // B-tree index voor range queries
}
Scheduled Reducers
Reducers die op een later tijdstip draaien:
#[reducer]
pub fn harvest_crop(ctx: &ReducerContext, crop_id: u64) -> Result<(), String> {
// Direct aanroepen
}
// Scheduled - roept harvest_crop aan na X seconden
#[reducer]
pub fn schedule_harvest(ctx: &ReducerContext, crop_id: u64, grow_time: u64) {
ctx.schedule_reducer(
"harvest_crop",
Duration::from_secs(grow_time),
(crop_id,)
);
}
Error Handling
Reducers gebruiken Result voor error handling:
#[reducer]
pub fn buy_item(ctx: &ReducerContext, item_id: String) -> Result<(), String> {
let player = ctx.db.player()
.identity()
.find(&ctx.sender)
.ok_or("Player not found")?;
let item = ctx.db.item_definition()
.item_id()
.find(&item_id)
.ok_or("Item not found")?;
if player.acorns < item.price {
return Err("Not enough acorns".to_string());
}
// Koop item...
Ok(())
}
CLI Commando's
Essentiële Commando's
# Start lokale server
spacetime start
# Build module
spacetime build
# Publish naar lokale server
spacetime publish [database-name] --clear-database
# Bekijk logs
spacetime logs [database-name]
# Beschrijf database
spacetime describe [database-name]
# Genereer client bindings (TypeScript)
spacetime generate --lang typescript --out-dir ./bindings
# SQL queries uitvoeren
spacetime sql [database-name] "SELECT * FROM player"
Remote Server
# Login op remote server
spacetime login [server-url]
# Publish naar remote
spacetime publish [database-name] --server [server-url]
Best Practices
1. Kleine Reducers
Houd reducers klein en gefocust:
// Goed
#[reducer]
pub fn update_position(ctx: &ReducerContext, x: f32, y: f32) { ... }
// Vermijd
#[reducer]
pub fn do_everything(ctx: &ReducerContext, ...) { ... }
2. Validatie Altijd
Valideer altijd input in reducers:
#[reducer]
pub fn set_username(ctx: &ReducerContext, username: String) -> Result<(), String> {
if username.len() < 3 {
return Err("Username too short".to_string());
}
if username.len() > 20 {
return Err("Username too long".to_string());
}
// ...
Ok(())
}
3. Admin Checks
Altijd controleren voor admin functies:
fn is_admin(ctx: &ReducerContext) -> bool {
ctx.db.admin_user()
.identity()
.find(&ctx.sender)
.is_some()
}
#[reducer]
pub fn admin_give_item(ctx: &ReducerContext, ...) -> Result<(), String> {
if !is_admin(ctx) {
return Err("Not authorized".to_string());
}
// ...
Ok(())
}