Skip to content

Latest commit

 

History

History
163 lines (122 loc) · 6.51 KB

File metadata and controls

163 lines (122 loc) · 6.51 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

SpacetimeDB Workflow Engine - durable state machines for games. Workflows survive server restarts, with persistent timers, queryable state, and hierarchical composition.

Build & Test Commands

This project uses mise for tool management and tasks.

# First-time setup
mise trust && mise install   # Install Rust (stable), Node.js 22
mise run setup               # Install dependencies, verify environment

# Build
mise run build               # Debug build
mise run release             # Release build

# Test
mise run test                # All unit tests
mise run test-core           # Core library tests only
mise run tv                  # Verbose test output
mise run test-ts             # TypeScript integration tests
mise run integration         # Full suite (deploy + TypeScript tests)

# Lint & Format
mise run fmt                 # Format code
mise run clippy              # Run lints
mise run verify              # fmt + clippy + test

# SpacetimeDB
mise run publish             # Deploy to SpacetimeDB
mise run logs                # View logs (follow mode)
mise run generate-bindings   # Regenerate TypeScript bindings

Architecture

Workspace Structure

  • core/ (workflow-core): Pure Rust library with no SpacetimeDB dependencies

    • handler.rs: WorkflowHandler trait + DynWorkflowHandler for type erasure
    • types.rs: WorkflowResult, WorkflowContext, TimerRequest
    • registry.rs: Global workflow registry with lazy initialization
    • traits.rs: Timer, Signal traits for type-safe events
  • macros/ (workflow-macros): Proc macros

    • #[workflow]: Transforms sequential async-style code into state machines
    • #[derive(Timer)]: Generates name(), from_name() for unit enums
    • #[derive(Signal)]: Generates from_name_and_payload() with auto-deserialization
    • install!{}: Generates SpacetimeDB tables, reducers, and workflow registration
  • example/ (workflow-example): SpacetimeDB module demonstrating integration

Key Design Patterns

Macro-Based Workflows: The #[workflow] macro transforms sequential code into state machines:

  • Identifies await points (timer!, signal!, spawn!, procedure!, select!)
  • Generates state struct with phase tracking and mutable variable persistence
  • Creates WorkflowHandler implementation automatically

Lazy Registration: The install! macro generates __ensure_workflows_registered() with an AtomicBool flag. Each generated reducer calls this first, ensuring workflows survive module updates (WASM reloads reset static state, init doesn't re-run).

Generated Tables (from install!)

  • Workflow: Primary state table (id, workflow_type, entity_id, status, current_step, state_data)
  • WorkflowTimer: Scheduled table with scheduled_at for timer firing
  • WorkflowSubscription: Signal subscriptions for broadcast routing (signal_type, filter_value)
  • LastWorkflowId: Stores last created workflow ID (workaround for reducer return limitations)

Generated View Structs (from #[workflow])

Each workflow automatically generates a {Name}WorkflowView struct containing init + all tracked mutable variables. Use {Name}Workflow::view(state_data) to parse workflow state for querying (e.g., quest progress for NPC dialogs).

Generated Reducers

  • workflow_start: Create and start a workflow
  • workflow_signal: Send signal to running workflow (uses snake_case signal name)
  • workflow_broadcast_signal: Send signal to all matching subscriptions
  • workflow_cancel: Cancel a workflow
  • workflow_timer_fire: Called automatically by SpacetimeDB scheduler

Workflow Implementation Pattern

Write workflows as sequential code with the #[workflow] macro:

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
enum BuffTimer { Expire }

#[derive(Signal)]
enum BuffSignal {
    Dispel,
    Stack(u32),  // Payload auto-deserialized
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BuffInit { duration_secs: u64 }

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BuffResult { final_stacks: u32 }

#[workflow]
fn buff(init: BuffInit) -> Result<BuffResult> {
    let mut stacks: u32 = 1;  // Mutable vars need explicit type

    loop {
        select! {
            timer!(BuffTimer::Expire, init.duration_secs.secs()) => break,
            signal!(BuffSignal::Dispel) => break,
            signal!(BuffSignal::Stack(n)) => {
                stacks += n;
                continue
            },
        }.await;
    }

    Ok(BuffResult { final_stacks: stacks })
}

// Register in install! macro
install! {
    "buff" => BuffWorkflow,
}

Macro Syntax Quick Reference

Feature Syntax
Timer await timer!(Timer::Variant, duration).await
Signal in select signal!(Signal::Variant)
Payload binding signal!(Signal::Stack(n))
Spawn child spawn!("workflow", init).await
Procedure call procedure!("name", args).await
Select select! { timer!(...) => {...}, signal!(...) => {...} }.await
For loops for i in 0..n { spawn!(...).await }
Conditionals if cond { timer!(...).await } else { timer!(...).await }
Mutable vars let mut x: Type = val; (explicit type required)
Reducer call reducer!(reducer_ctx, my_reducer(args)) (fire-and-forget)
Subscribe subscribe!(workflow_subs, Signal::Variant(filter)) (for broadcast)

Testing

  • Unit tests: core/src/ has tests for handlers, types, traits
  • Property tests: core/src/proptest_tests.rs for serialization roundtrips
  • Integration tests: tests/typescript/integration.test.ts uses SpacetimeDB TypeScript SDK
  • Test reducers: example/src/integration_tests.rs provides test entry points

Important Caveats

  • Module updates reset static state: Always register workflows in install!{}, not init
  • Reducer return types: SpacetimeDB reducers can only return () or Result<(), E> - use LastWorkflowId table to retrieve created IDs
  • Timer variants must be unit: #[derive(Timer)] only works on unit enums; use #[derive(Signal)] for data
  • Workflow status after timer: Workflows with pending timers are Suspended, not Running
  • Mutable variables need explicit types: let mut count: u32 = 0; not let mut count = 0;
  • Signal names are snake_case: EnemyKilled"enemy_killed" for workflow_signal and workflow_broadcast_signal