mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-06 07:26:43 -04:00
c7af2d4cd3
## Summary - add `ReducerContext::database_identity()` in Rust bindings - deprecate `ReducerContext::identity()` and keep it as a compatibility alias - update reducer docs example to use `ctx.database_identity()` - add C# reducer-context equivalent: `DatabaseIdentity` and obsolete `Identity` alias - update Rust/C# module test callsites to the new API name - update C# codegen snapshots for generated `ReducerContext` API output ## Why Issue #3201 reports user confusion between reducer `ctx.identity()` (database/module identity) and `ctx.sender`. This change clarifies naming while preserving compatibility. ## Validation - `cargo check -p spacetimedb -p module-test` (Passed) - `dotnet test crates/bindings-csharp/Codegen.Tests/Codegen.Tests.csproj --nologo` (Passed) - `dotnet test crates/bindings-csharp/Runtime.Tests/Runtime.Tests.csproj --nologo` (Failed) pre-existing unrelated failure: - `Runtime.Tests/JwtClaimsTest.cs(10,23): CS1729: 'JwtClaims' does not contain a constructor that takes 2 arguments` ## Compatibility - Rust: `identity()` still works but is deprecated in favor of `database_identity()`. - C#: `Identity` still works but is marked `[Obsolete]` in favor of `DatabaseIdentity`. Closes #3201
315 lines
9.8 KiB
C++
315 lines
9.8 KiB
C++
#ifndef SPACETIMEDB_PROCEDURE_CONTEXT_H
|
|
#define SPACETIMEDB_PROCEDURE_CONTEXT_H
|
|
|
|
#include <spacetimedb/bsatn/types.h> // For Identity
|
|
#include <spacetimedb/bsatn/timestamp.h> // For Timestamp
|
|
#include <spacetimedb/bsatn/uuid.h> // For Uuid
|
|
#include <spacetimedb/tx_context.h> // For TxContext
|
|
#include <spacetimedb/abi/FFI.h> // For transaction syscalls
|
|
#include <spacetimedb/random.h> // For StdbRng
|
|
#ifdef SPACETIMEDB_UNSTABLE_FEATURES
|
|
#include <spacetimedb/http.h> // For HttpClient
|
|
#endif
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <stdexcept>
|
|
#include <type_traits>
|
|
#include <memory>
|
|
|
|
namespace SpacetimeDB {
|
|
|
|
/**
|
|
* @brief Context for procedures
|
|
*
|
|
* ProcedureContext provides access to call metadata (sender, timestamp, connection)
|
|
* but does NOT have direct database access. This is a key difference from ReducerContext.
|
|
*
|
|
* Features:
|
|
* - Pure computations with return values
|
|
* - Database access via explicit transactions (ctx.WithTx() or ctx.TryWithTx())
|
|
* - HTTP requests via ctx.http (when SPACETIMEDB_UNSTABLE_FEATURES enabled)
|
|
* - UUID generation (ctx.new_uuid_v4(), ctx.new_uuid_v7())
|
|
*
|
|
* Key differences from ReducerContext:
|
|
* - NO db field (database operations require explicit transactions)
|
|
* - Has connection_id (procedures track which connection called them)
|
|
* - Has rng() method for UUID generation
|
|
*
|
|
* Example usage (pure function):
|
|
* @code
|
|
* SPACETIMEDB_PROCEDURE(uint32_t, add_numbers, ProcedureContext ctx, uint32_t a, uint32_t b) {
|
|
* return a + b;
|
|
* }
|
|
* @endcode
|
|
*
|
|
* Example with transactions:
|
|
* @code
|
|
* SPACETIMEDB_PROCEDURE(Unit, insert_item, ProcedureContext ctx, Item item) {
|
|
* ctx.WithTx([&item](TxContext& tx) {
|
|
* tx.db[items].insert(item);
|
|
* });
|
|
* return Unit{};
|
|
* }
|
|
* @endcode
|
|
*/
|
|
struct ProcedureContext {
|
|
private:
|
|
// Caller's identity - who invoked this procedure
|
|
Identity sender_;
|
|
|
|
public:
|
|
// Timestamp when the procedure was invoked
|
|
Timestamp timestamp;
|
|
|
|
// Connection ID for the caller
|
|
// Used to track which client connection initiated this procedure
|
|
ConnectionId connection_id;
|
|
|
|
#ifdef SPACETIMEDB_UNSTABLE_FEATURES
|
|
// HTTP client for making external requests
|
|
// IMPORTANT: HTTP calls are NOT allowed inside transactions!
|
|
// Always call HTTP before with_tx() or try_with_tx()
|
|
HttpClient http;
|
|
#endif
|
|
|
|
private:
|
|
// Lazily initialized RNG for UUID generation
|
|
mutable std::shared_ptr<StdbRng> rng_instance;
|
|
|
|
// Monotonic counter for UUID v7 generation (31 bits, wraps around)
|
|
mutable uint32_t counter_uuid_ = 0;
|
|
|
|
public:
|
|
ProcedureContext() = default;
|
|
|
|
ProcedureContext(Identity s, Timestamp t, ConnectionId conn_id)
|
|
: sender_(s), timestamp(t), connection_id(conn_id) {}
|
|
|
|
Identity sender() const {
|
|
return sender_;
|
|
}
|
|
|
|
/**
|
|
* @brief Read the current module's Identity
|
|
*
|
|
* Returns the Identity (database address) of the module instance.
|
|
* This is useful for constructing URLs or making API calls to the module's own endpoints.
|
|
*
|
|
* Example:
|
|
* @code
|
|
* auto module_id = ctx.database_identity();
|
|
* std::string url = "http://localhost:3000/v1/database/" +
|
|
* module_id.to_hex() + "/schema?version=9";
|
|
* @endcode
|
|
*/
|
|
Identity database_identity() const {
|
|
std::array<uint8_t, 32> id_bytes;
|
|
::identity(id_bytes.data());
|
|
return Identity(id_bytes);
|
|
}
|
|
|
|
[[deprecated("Use database_identity() instead.")]]
|
|
Identity identity() const {
|
|
return database_identity();
|
|
}
|
|
|
|
/**
|
|
* @brief Get the random number generator for this procedure call
|
|
*
|
|
* Lazily initialized and seeded with the timestamp.
|
|
*/
|
|
StdbRng& rng() const {
|
|
if (!rng_instance) {
|
|
rng_instance = std::make_shared<StdbRng>(timestamp);
|
|
}
|
|
return *rng_instance;
|
|
}
|
|
|
|
/**
|
|
* Generate a new random UUID v4.
|
|
*
|
|
* Creates a random UUID using the procedure's RNG.
|
|
*
|
|
* Example:
|
|
* @code
|
|
* SPACETIMEDB_PROCEDURE(Uuid, generate_uuid_v4, ProcedureContext ctx) {
|
|
* return ctx.new_uuid_v4();
|
|
* }
|
|
* @endcode
|
|
*
|
|
* @return A new UUID v4
|
|
*/
|
|
Uuid new_uuid_v4() const {
|
|
// Get 16 random bytes from the context RNG
|
|
std::array<uint8_t, 16> random_bytes;
|
|
rng().fill_bytes(random_bytes.data(), 16);
|
|
|
|
// Generate UUID v4
|
|
return Uuid::from_random_bytes_v4(random_bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a new UUID v7.
|
|
*
|
|
* Creates a time-ordered UUID with the procedure's timestamp, a monotonic counter,
|
|
* and random bytes from the procedure's RNG.
|
|
*
|
|
* Example:
|
|
* @code
|
|
* SPACETIMEDB_PROCEDURE(Uuid, generate_uuid_v7, ProcedureContext ctx) {
|
|
* return ctx.new_uuid_v7();
|
|
* }
|
|
* @endcode
|
|
*
|
|
* @return A new UUID v7
|
|
*/
|
|
Uuid new_uuid_v7() const {
|
|
// Get 4 random bytes from the context RNG
|
|
std::array<uint8_t, 4> random_bytes;
|
|
rng().fill_bytes(random_bytes.data(), 4);
|
|
|
|
// Generate UUID v7 with timestamp and counter
|
|
return Uuid::from_counter_v7(counter_uuid_, timestamp, random_bytes);
|
|
}
|
|
|
|
#ifdef SPACETIMEDB_UNSTABLE_FEATURES
|
|
/**
|
|
* @brief Execute a callback within a database transaction
|
|
*
|
|
* Starts a mutable transaction, executes the callback, and commits on success.
|
|
* If the callback panics (via LOG_PANIC), the transaction is automatically rolled back.
|
|
*
|
|
* The callback receives a TxContext with database access. All database operations
|
|
* performed within the callback are part of the transaction.
|
|
*
|
|
* Usage:
|
|
* @code
|
|
* ctx.with_tx([&](TxContext& tx) {
|
|
* tx.db.users().insert(User{"alice"});
|
|
* tx.db.posts().insert(Post{"hello world"});
|
|
* // Both inserts commit together
|
|
* });
|
|
* @endcode
|
|
*
|
|
* @param body Callback to execute within the transaction
|
|
* @return The return value of the callback
|
|
*/
|
|
template<typename Func>
|
|
auto with_tx(Func&& body) -> decltype(body(std::declval<TxContext&>())) {
|
|
using ResultType = decltype(body(std::declval<TxContext&>()));
|
|
|
|
// Start transaction
|
|
int64_t tx_timestamp;
|
|
Status status = ::procedure_start_mut_tx(&tx_timestamp);
|
|
if (is_error(status)) {
|
|
LOG_PANIC("Failed to start transaction");
|
|
}
|
|
|
|
// Create a ReducerContext for this transaction
|
|
// Note: connection_id converted to std::optional
|
|
ReducerContext reducer_ctx(
|
|
sender(),
|
|
std::optional<ConnectionId>(connection_id),
|
|
Timestamp::from_micros_since_epoch(tx_timestamp)
|
|
);
|
|
|
|
// Create transaction context wrapping the reducer context
|
|
TxContext tx{reducer_ctx};
|
|
|
|
// Execute callback
|
|
if constexpr (std::is_void_v<ResultType>) {
|
|
body(tx);
|
|
|
|
// Commit transaction
|
|
status = ::procedure_commit_mut_tx();
|
|
if (is_error(status)) {
|
|
LOG_PANIC("Failed to commit transaction");
|
|
}
|
|
} else {
|
|
ResultType result = body(tx);
|
|
|
|
// Commit transaction
|
|
status = ::procedure_commit_mut_tx();
|
|
if (is_error(status)) {
|
|
LOG_PANIC("Failed to commit transaction");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Execute a callback within a database transaction, with explicit rollback control
|
|
*
|
|
* Similar to with_tx(), but allows the callback to indicate whether to commit or rollback.
|
|
* The callback should return true to commit, false to rollback.
|
|
*
|
|
* Usage:
|
|
* @code
|
|
* bool success = ctx.try_with_tx([&](TxContext& tx) -> bool {
|
|
* tx.db.users().insert(User{"alice"});
|
|
* if (some_condition) {
|
|
* return false; // Rollback
|
|
* }
|
|
* return true; // Commit
|
|
* });
|
|
* @endcode
|
|
*
|
|
* @param body Callback that returns true to commit, false to rollback
|
|
* @return The return value of the callback
|
|
*/
|
|
template<typename Func>
|
|
auto try_with_tx(Func&& body) -> decltype(body(std::declval<TxContext&>())) {
|
|
using ResultType = decltype(body(std::declval<TxContext&>()));
|
|
|
|
// Start transaction
|
|
int64_t tx_timestamp;
|
|
Status status = ::procedure_start_mut_tx(&tx_timestamp);
|
|
if (is_error(status)) {
|
|
LOG_PANIC("Failed to start transaction");
|
|
}
|
|
|
|
// Create a ReducerContext for this transaction
|
|
ReducerContext reducer_ctx(
|
|
sender(),
|
|
std::optional<ConnectionId>(connection_id),
|
|
Timestamp::from_micros_since_epoch(tx_timestamp)
|
|
);
|
|
|
|
// Create transaction context wrapping the reducer context
|
|
TxContext tx{reducer_ctx};
|
|
|
|
// Execute callback
|
|
ResultType result = body(tx);
|
|
|
|
// For bool results, use the value to decide commit/rollback
|
|
// For other types, always commit (caller can use LOG_PANIC to abort)
|
|
if constexpr (std::is_same_v<ResultType, bool>) {
|
|
if (result) {
|
|
status = ::procedure_commit_mut_tx();
|
|
if (is_error(status)) {
|
|
LOG_PANIC("Failed to commit transaction");
|
|
}
|
|
} else {
|
|
status = ::procedure_abort_mut_tx();
|
|
if (is_error(status)) {
|
|
LOG_PANIC("Failed to rollback transaction");
|
|
}
|
|
}
|
|
} else {
|
|
// For non-bool returns, always commit
|
|
status = ::procedure_commit_mut_tx();
|
|
if (is_error(status)) {
|
|
LOG_PANIC("Failed to commit transaction");
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
#endif
|
|
};
|
|
|
|
} // namespace SpacetimeDB
|
|
|
|
#endif // SPACETIMEDB_PROCEDURE_CONTEXT_H
|