mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-06-29 09:19:05 -04:00
d5c75d944d
# 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>
241 lines
7.4 KiB
C++
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
|