Files
Tyler Cloutier d5c75d944d Ungate procedures from the unstable feature (#5164)
# Description of Changes

Graduate **procedures** (and the outgoing HTTP client used from
procedures) out of the `unstable` feature gate /
`SPACETIMEDB_UNSTABLE_FEATURES` across all module libraries. The
`procedure` macro, `ProcedureContext`, `with_tx`/`try_with_tx`,
scheduled procedures, and `ctx.http` are now available without opting
into `unstable`.

**Still gated** (unchanged): HTTP handlers/webhooks, views, RLS /
`client_visibility_filter`, and immediate-scheduling
(`volatile_nonatomic_schedule_immediate`).

Per library:
- **`bindings-sys`**: ungate the `procedure` host-call module + raw ABI
imports (`procedure_start/commit/abort_mut_tx`,
`procedure_http_request`) and `call_no_ret`. Scheduling ABI stays gated.
- **`bindings` (Rust)**: ungate the `procedure` macro,
`ProcedureContext`, `TxContext`/`with_tx`/`try_with_tx`,
`db_read_only`/`get_read_only`, the procedure traits,
`register_procedure`, `__call_procedure__`, and procedure RNG. The
`http` module (previously gated as a unit) now has fine-grained gating
so the outgoing `HttpClient` is exposed while
`HandlerContext`/`Router`/handler macros stay gated.
- **`bindings-csharp`**: drop `[Experimental("STDB_UNSTABLE")]` from
`ProcedureContext.WithTx/TryWithTx` (runtime + codegen) and the
generated `ProcedureTxContext`; FFI snapshots regenerated. Handler
context members + RLS attribute stay gated.
- **`bindings-cpp`**: ungate the procedure ABI (`abi.h`/`FFI.h`),
`tx_execution.h`, the outgoing HTTP client (`http.h`/`http_convert.h`),
and `procedure_context.h`.
`handler_context.h`/`router.h`/`http_handler_macros.h` still `#error`
without `SPACETIMEDB_UNSTABLE_FEATURES`.
- **docs**: remove the procedures beta notices (HTTP handlers stay
marked beta for a later release); regenerate `static/llms.md`.
- **`sdk-test-procedure`**: no longer needs `features = ["unstable"]`.

# API and ABI breaking changes

Not breaking. This is purely additive to the stable surface: procedures
become available **without** the `unstable` feature, while modules that
already opt into `unstable` are unaffected. The wasm host-ABI imports
were already implemented host-side and merely gated module-side, so
there is no ABI version change. (No breaking-change label needed.)

# Expected complexity level and risk

**3/5.** The diff itself is mostly removing
`#[cfg]`/`#ifdef`/`[Experimental]` guards, but the gate bundled
procedures + outgoing HTTP + handlers + the ABI crate together, so the
work was in *separating* procedures from the features that stay gated.
Areas reviewers may want to scrutinize:
- The Rust `http` module went from "gated as a whole" to fine-grained
per-item gating; the outgoing `HttpClient` is exposed while handler
types/macros stay behind `unstable`.
- The `unstable` cut reaches the ABI crate (`bindings-sys`), which is
the only way procedures can compile without the feature.
- C++ separation of the outgoing HTTP client from HTTP handlers across
several headers.

# Testing

- [x] Rust: `cargo check -p spacetimedb` passes in default, `--features
unstable`, and `--no-default-features --features unstable`.
- [x] Rust end-to-end: `sdk-test-procedure` (uses procedures,
`with_tx`/`try_with_tx`, `ctx.http.get`, `new_uuid_v7`, scheduled
procedures) builds **without** `unstable`.
- [x] C#: Codegen + Runtime build; `Codegen.Tests` pass (6/6) after FFI
snapshot regen.
- [x] C++: host syntax-check confirms procedures + `ctx.http.send()`
compile **without** the flag, full headers compile **with** it, and
`handler_context.h` still `#error`s without it. (Not built for the real
wasm/emscripten target locally — relying on CI.)
- [x] `cargo ci lint` components reproduced locally: rustfmt, clippy
(`-D warnings`, default + `unstable`), csharpier, and `cargo doc --deny
warnings`.
- [ ] Reviewer: confirm the wasm bindings + C#/C++ test suites pass in
CI (especially the C++ wasm build, which couldn't run locally).

---------

Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
2026-06-05 16:09:36 +00:00

241 lines
7.4 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/internal/tx_execution.h>
#include <spacetimedb/random.h> // For StdbRng
#include <spacetimedb/http.h> // For HttpClient
#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;
// 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;
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);
}
/**
* @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&>())) {
auto make_reducer_ctx = [this](Timestamp tx_timestamp) {
return ReducerContext(
sender(),
std::optional<ConnectionId>(connection_id),
tx_timestamp
);
};
return Internal::with_tx(make_reducer_ctx, body);
}
/**
* @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&>())) {
auto make_reducer_ctx = [this](Timestamp tx_timestamp) {
return ReducerContext(
sender(),
std::optional<ConnectionId>(connection_id),
tx_timestamp
);
};
return Internal::try_with_tx(make_reducer_ctx, body);
}
};
} // namespace SpacetimeDB
#endif // SPACETIMEDB_PROCEDURE_CONTEXT_H