Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions crates/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ pub struct ReducerContext {
///
/// Will be `None` for certain reducers invoked automatically by the host,
/// including `init` and scheduled reducers.
pub connection_id: Option<ConnectionId>,
connection_id: Option<ConnectionId>,

sender_auth: AuthCtx,

Expand Down Expand Up @@ -1024,6 +1024,14 @@ impl ReducerContext {
self.sender
}

/// The `ConnectionId` of the client that invoked the reducer.
///
/// Will be `None` for certain reducers invoked automatically by the host,
/// including `init` and scheduled reducers.
pub fn connection_id(&self) -> Option<ConnectionId> {
self.connection_id
}

/// Returns the authorization information for the caller of this reducer.
pub fn sender_auth(&self) -> &AuthCtx {
&self.sender_auth
Expand Down Expand Up @@ -1142,7 +1150,7 @@ pub struct ProcedureContext {
/// The `ConnectionId` of the client that invoked the procedure.
///
/// Will be `None` for certain scheduled procedures.
pub connection_id: Option<ConnectionId>,
connection_id: Option<ConnectionId>,

/// Methods for performing HTTP requests.
pub http: crate::http::HttpClient,
Expand Down Expand Up @@ -1178,6 +1186,13 @@ impl ProcedureContext {
self.sender
}

/// The `ConnectionId` of the client that invoked the procedure.
///
/// Will be `None` for certain scheduled procedures.
pub fn connection_id(&self) -> Option<ConnectionId> {
self.connection_id
}

/// Read the current module's [`Identity`].
pub fn identity(&self) -> Identity {
// Hypothetically, we *could* read the module identity out of the system tables.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ ctx.Rng // Random number generator
```rust
ctx.db // Database access
ctx.sender() // Identity of caller
ctx.connection_id // Option<ConnectionId>
ctx.connection_id() // Option<ConnectionId>
ctx.timestamp // Timestamp
ctx.identity() // Module's identity
ctx.rng() // Random number generator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ public static void OnConnect(ReducerContext ctx)
pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> {
log::info!("Client connected: {}", ctx.sender());

// ctx.connection_id is guaranteed to be Some(...)
let conn_id = ctx.connection_id.unwrap();
// ctx.connection_id() is guaranteed to be Some(...)
let conn_id = ctx.connection_id().unwrap();

// Initialize client session
ctx.db.sessions().try_insert(Session {
Expand All @@ -152,7 +152,7 @@ pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> {

The `client_connected` reducer:
- Cannot take arguments beyond `ReducerContext`
- `ctx.connection_id` is guaranteed to be present
- `ctx.connection_id()` is guaranteed to be present
- Failure disconnects the client
- Runs for each distinct connection (WebSocket, HTTP call)

Expand Down Expand Up @@ -200,8 +200,8 @@ public static void OnDisconnect(ReducerContext ctx)
pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> {
log::info!("Client disconnected: {}", ctx.sender());

// ctx.connection_id is guaranteed to be Some(...)
let conn_id = ctx.connection_id.unwrap();
// ctx.connection_id() is guaranteed to be Some(...)
let conn_id = ctx.connection_id().unwrap();

// Clean up client session
ctx.db.sessions().connection_id().delete(&conn_id);
Expand All @@ -215,7 +215,7 @@ pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> {

The `client_disconnected` reducer:
- Cannot take arguments beyond `ReducerContext`
- `ctx.connection_id` is guaranteed to be present
- `ctx.connection_id()` is guaranteed to be present
- Failure is logged but doesn't prevent disconnection
- Runs when connection ends (close, timeout, error)

Expand All @@ -231,5 +231,5 @@ Reducers can be triggered at specific times using schedule tables. See [Schedule
:::info Scheduled Reducer Context
Scheduled reducer calls originate from SpacetimeDB itself, not from a client. Therefore:
- `ctx.sender()` will be the module's own identity
- `ctx.connection_id` will be `None`/`null`/`undefined`
- `ctx.connection_id()` will be `None`/`null`/`undefined`
:::
34 changes: 17 additions & 17 deletions docs/static/llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ pub struct PlayerSessionData {
fn example_reducer(ctx: &spacetimedb::ReducerContext) {
// Reducers interact with the specific table handles:
let session = PlayerSessionData {
player_id: ctx.sender, // Example: Use sender identity
player_id: ctx.sender(), // Example: Use sender identity
session_id: 0, // Assuming auto_inc
last_activity: ctx.timestamp,
};
Expand All @@ -446,12 +446,12 @@ fn example_reducer(ctx: &spacetimedb::ReducerContext) {
}

// Find a player in the 'players_in_lobby' table by primary key
if let Some(lobby_player) = ctx.db.players_in_lobby().player_id().find(&ctx.sender) {
if let Some(lobby_player) = ctx.db.players_in_lobby().player_id().find(&ctx.sender()) {
spacetimedb::log::info!("Player {} found in lobby.", lobby_player.player_id);
}

// Delete from the 'logged_in_players' table using the PK index
ctx.db.logged_in_players().player_id().delete(&ctx.sender);
ctx.db.logged_in_players().player_id().delete(&ctx.sender());
}
```

Expand Down Expand Up @@ -499,7 +499,7 @@ use spacetimedb::{reducer, ReducerContext, Table, Identity, Timestamp, log};
// Example: Basic reducer to set a user's name
#[reducer]
pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
let sender_id = ctx.sender;
let sender_id = ctx.sender();
let name = validate_name(name)?; // Use helper for validation

// Find the user row by primary key
Expand All @@ -519,13 +519,13 @@ pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
#[reducer]
pub fn send_message(ctx: &ReducerContext, text: String) -> Result<(), String> {
let text = validate_message(text)?; // Use helper for validation
log::info!("User {} sent message: {}", ctx.sender, text);
log::info!("User {} sent message: {}", ctx.sender(), text);

// Insert a new row into the Message table
// Note: id is auto_inc, so we provide 0. insert() panics on constraint violation.
let new_message = Message {
id: 0,
sender: ctx.sender,
sender: ctx.sender(),
text,
sent: ctx.timestamp,
};
Expand Down Expand Up @@ -567,8 +567,8 @@ Reducers can indicate failure either by returning `Err` from a function with a `
Special reducers handle specific events:

- `#[reducer(init)]`: Runs once when the module is first published **and** any time the database is manually cleared (e.g., via `spacetime publish -c` or `spacetime server clear`). Failure prevents publishing or clearing. Often used for initial data setup.
- `#[reducer(client_connected)]`: Runs when any distinct client connection (e.g., WebSocket, HTTP call) is established. Failure disconnects the client. `ctx.connection_id` is guaranteed to be `Some(...)` within this reducer.
- `#[reducer(client_disconnected)]`: Runs when any distinct client connection terminates. Failure is logged but does not prevent disconnection. `ctx.connection_id` is guaranteed to be `Some(...)` within this reducer.
- `#[reducer(client_connected)]`: Runs when any distinct client connection (e.g., WebSocket, HTTP call) is established. Failure disconnects the client. `ctx.connection_id()` is guaranteed to be `Some(...)` within this reducer.
- `#[reducer(client_disconnected)]`: Runs when any distinct client connection terminates. Failure is logged but does not prevent disconnection. `ctx.connection_id()` is guaranteed to be `Some(...)` within this reducer.

These reducers cannot take arguments beyond `&ReducerContext`.

Expand Down Expand Up @@ -611,14 +611,14 @@ pub fn initialize_database(ctx: &ReducerContext) {
// Example client_connected reducer
#[reducer(client_connected)]
pub fn handle_connect(ctx: &ReducerContext) {
log::info!("Client connected: {}, Connection ID: {:?}", ctx.sender, ctx.connection_id);
log::info!("Client connected: {}, Connection ID: {:?}", ctx.sender(), ctx.connection_id());
// ... setup initial state for ctx.sender ...
}

// Example client_disconnected reducer
#[reducer(client_disconnected)]
pub fn handle_disconnect(ctx: &ReducerContext) {
log::info!("Client disconnected: {}, Connection ID: {:?}", ctx.sender, ctx.connection_id);
log::info!("Client disconnected: {}, Connection ID: {:?}", ctx.sender(), ctx.connection_id());
// ... cleanup state for ctx.sender ...
}
```
Expand Down Expand Up @@ -800,7 +800,7 @@ struct SendMessageSchedule {
#[reducer]
fn send_message(ctx: &ReducerContext, args: SendMessageSchedule) -> Result<(), String> {
// Security check is important!
if ctx.sender != ctx.identity() {
if ctx.sender() != ctx.identity() {
return Err("Reducer `send_message` may not be invoked by clients, only via scheduling.".into());
}

Expand Down Expand Up @@ -851,7 +851,7 @@ Refer to the [official Rust Module SDK documentation on docs.rs](https://docs.rs

- **Best-Effort Scheduling:** Scheduled reducers are called on a best-effort basis and may be slightly delayed in their execution when a database is under heavy load.

- **Restricting Access (Security):** Scheduled reducers are normal reducers and _can_ still be called directly by clients. If a scheduled reducer should _only_ be called by the scheduler, it is crucial to begin the reducer with a check comparing the caller's identity (`ctx.sender`) to the module's own identity (`ctx.identity()`).
- **Restricting Access (Security):** Scheduled reducers are normal reducers and _can_ still be called directly by clients. If a scheduled reducer should _only_ be called by the scheduler, it is crucial to begin the reducer with a check comparing the caller's identity (`ctx.sender()`) to the module's own identity (`ctx.identity()`).

```rust
use spacetimedb::{reducer, ReducerContext};
Expand All @@ -860,7 +860,7 @@ Refer to the [official Rust Module SDK documentation on docs.rs](https://docs.rs

#[reducer]
fn my_scheduled_reducer(ctx: &ReducerContext, args: MyScheduleArgs) -> Result<(), String> {
if ctx.sender != ctx.identity() {
if ctx.sender() != ctx.identity() {
return Err("Reducer `my_scheduled_reducer` may not be invoked by clients, only via scheduling.".into());
}
// ... Reducer body proceeds only if called by scheduler ...
Expand All @@ -869,7 +869,7 @@ Refer to the [official Rust Module SDK documentation on docs.rs](https://docs.rs
```

:::info Scheduled Reducers and Connections
Scheduled reducer calls originate from the SpacetimeDB scheduler itself, not from an external client connection. Therefore, within a scheduled reducer, `ctx.sender` will be the module's own identity, and `ctx.connection_id` will be `None`.
Scheduled reducer calls originate from the SpacetimeDB scheduler itself, not from an external client connection. Therefore, within a scheduled reducer, `ctx.sender()` will be the module's own identity, and `ctx.connection_id()` will be `None`.
:::

#### View Functions
Expand Down Expand Up @@ -922,7 +922,7 @@ pub struct PlayerAndLevel {
// Returns Option<T> for at-most-one row
#[view(name = my_player, public)]
fn my_player(ctx: &ViewContext) -> Option<Player> {
ctx.db.player().identity().find(ctx.sender)
ctx.db.player().identity().find(ctx.sender())
}

// View that returns all players at a specific level (same for all callers)
Expand Down Expand Up @@ -953,7 +953,7 @@ fn players_for_level(ctx: &AnonymousViewContext) -> Vec<PlayerAndLevel> {

Views use one of two context types:

- **`ViewContext`**: Provides access to the caller's `Identity` through `ctx.sender`. Use this when the view depends on who is querying it (e.g., "get my player").
- **`ViewContext`**: Provides access to the caller's `Identity` through `ctx.sender()`. Use this when the view depends on who is querying it (e.g., "get my player").
- **`AnonymousViewContext`**: Does not provide caller information. Use this when the view produces the same results regardless of who queries it (e.g., "get top 10 players").

Both contexts provide read-only access to tables and indexes through `ctx.db`.
Expand Down Expand Up @@ -984,7 +984,7 @@ use spacetimedb::{view, ViewContext, Query};
fn my_messages(ctx: &ViewContext) -> Query<Message> {
// Build a typed query using the query builder
ctx.db.message()
.filter(|cols| cols.sender.eq(ctx.sender))
.filter(|cols| cols.sender.eq(ctx.sender()))
.build()
}

Expand Down
8 changes: 4 additions & 4 deletions modules/sdk-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,23 +664,23 @@ fn insert_caller_pk_identity(ctx: &ReducerContext, data: i32) -> anyhow::Result<
#[spacetimedb::reducer]
fn insert_caller_one_connection_id(ctx: &ReducerContext) -> anyhow::Result<()> {
ctx.db.one_connection_id().insert(OneConnectionId {
a: ctx.connection_id.context("No connection id in reducer context")?,
a: ctx.connection_id().context("No connection id in reducer context")?,
});
Ok(())
}

#[spacetimedb::reducer]
fn insert_caller_vec_connection_id(ctx: &ReducerContext) -> anyhow::Result<()> {
ctx.db.vec_connection_id().insert(VecConnectionId {
a: vec![ctx.connection_id.context("No connection id in reducer context")?],
a: vec![ctx.connection_id().context("No connection id in reducer context")?],
});
Ok(())
}

#[spacetimedb::reducer]
fn insert_caller_unique_connection_id(ctx: &ReducerContext, data: i32) -> anyhow::Result<()> {
ctx.db.unique_connection_id().insert(UniqueConnectionId {
a: ctx.connection_id.context("No connection id in reducer context")?,
a: ctx.connection_id().context("No connection id in reducer context")?,
data,
});
Ok(())
Expand All @@ -689,7 +689,7 @@ fn insert_caller_unique_connection_id(ctx: &ReducerContext, data: i32) -> anyhow
#[spacetimedb::reducer]
fn insert_caller_pk_connection_id(ctx: &ReducerContext, data: i32) -> anyhow::Result<()> {
ctx.db.pk_connection_id().insert(PkConnectionId {
a: ctx.connection_id.context("No connection id in reducer context")?,
a: ctx.connection_id().context("No connection id in reducer context")?,
data,
});
Ok(())
Expand Down
5 changes: 3 additions & 2 deletions smoketests/tests/zz_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ class DockerRestartAutoDisconnect(Smoketest):
fn on_connect(ctx: &ReducerContext) {
ctx.db.connected_client().insert(ConnectedClient {
identity: ctx.sender(),
connection_id: ctx.connection_id.expect("sender connection id unset"),
connection_id: ctx.connection_id().expect("sender connection id unset"),
});
}

#[spacetimedb::reducer(client_disconnected)]
fn on_disconnect(ctx: &ReducerContext) {
let sender_identity = &ctx.sender();
let sender_connection_id = ctx.connection_id.as_ref().expect("sender connection id unset");
let connection_id = ctx.connection_id();
let sender_connection_id = connection_id.as_ref().expect("sender connection id unset");
let match_client = |row: &ConnectedClient| {
&row.identity == sender_identity && &row.connection_id == sender_connection_id
};
Expand Down
Loading