Ga naar hoofdinhoud

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:

TypeBeschrijving
publicAlle clients kunnen lezen
privateAlleen 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(())
}