Files
SpacetimeDB/crates/bindings-csharp/Runtime/ProcedureContext.cs
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

140 lines
4.6 KiB
C#

namespace SpacetimeDB;
#pragma warning disable STDB_UNSTABLE
public abstract class ProcedureContextBase : Internal.IInternalProcedureContext
{
public static Identity Identity => Internal.IProcedureContext.GetIdentity();
public Identity Sender { get; }
public ConnectionId? ConnectionId { get; }
public Random Rng => txState.Rng;
public Timestamp Timestamp => txState.Timestamp;
public AuthCtx SenderAuth { get; }
// NOTE: The host rejects procedure HTTP requests while a mut transaction is open
// (WOULD_BLOCK_TRANSACTION). Avoid calling `Http.*` inside WithTx.
public HttpClient Http { get; } = new();
// **Note:** must be 0..=u32::MAX
protected int CounterUuid = 0;
private readonly TransactionalContextState<ProcedureTxContextBase> txState;
protected ProcedureContextBase(
Identity sender,
ConnectionId? connectionId,
Random random,
Timestamp time
)
{
Sender = sender;
ConnectionId = connectionId;
SenderAuth = AuthCtx.BuildFromSystemTables(connectionId, sender);
txState = new(
random,
time,
timestamp => new Internal.TxContext(
CreateLocal(),
Sender,
ConnectionId,
timestamp,
SenderAuth,
random
),
inner => CreateTxContext(inner)
);
}
protected abstract ProcedureTxContextBase CreateTxContext(Internal.TxContext inner);
protected internal abstract LocalBase CreateLocal();
public Internal.TxContext EnterTxContext(long timestampMicros) =>
txState.EnterTxContext(timestampMicros);
public void ExitTxContext() => txState.ExitTxContext();
public readonly struct TxOutcome<TResult>(bool isSuccess, TResult? value, Exception? error)
{
public bool IsSuccess { get; } = isSuccess;
public TResult? Value { get; } = value;
public Exception? Error { get; } = error;
public static TxOutcome<TResult> Success(TResult value) => new(true, value, null);
public static TxOutcome<TResult> Failure(Exception error) => new(false, default, error);
public TResult UnwrapOrThrow() =>
IsSuccess
? Value!
: throw (
Error
?? new InvalidOperationException("Transaction failed without an error object.")
);
public TResult UnwrapOrThrow(Func<Exception> fallbackFactory) =>
IsSuccess ? Value! : throw (Error ?? fallbackFactory());
}
public TResult WithTx<TResult>(Func<ProcedureTxContextBase, TResult> body) =>
txState.WithTx(body);
public TxOutcome<TResult> TryWithTx<TResult, TError>(
Func<ProcedureTxContextBase, Result<TResult, TError>> body
)
where TError : Exception
{
var outcome = txState.TryWithTx(body);
return outcome.IsSuccess
? TxOutcome<TResult>.Success(outcome.Value!)
: TxOutcome<TResult>.Failure(
outcome.Error
?? new InvalidOperationException("Transaction failed without an error object.")
);
}
}
public abstract class ProcedureTxContextBase(Internal.TxContext inner) : IRefreshableTxContext
{
internal Internal.TxContext Inner { get; private set; } = inner;
internal void Refresh(Internal.TxContext inner) => Inner = inner;
void IRefreshableTxContext.Refresh(Internal.TxContext inner) => Refresh(inner);
public LocalBase Db => (LocalBase)Inner.Db;
public Identity Sender => Inner.Sender;
public ConnectionId? ConnectionId => Inner.ConnectionId;
public Timestamp Timestamp => Inner.Timestamp;
public AuthCtx SenderAuth => Inner.SenderAuth;
public Random Rng => Inner.Rng;
}
public abstract class LocalBase : Internal.Local { }
internal sealed partial class RuntimeProcedureContext(
Identity sender,
ConnectionId? connectionId,
Random random,
Timestamp timestamp
) : ProcedureContextBase(sender, connectionId, random, timestamp)
{
private readonly RuntimeLocal _db = new();
protected internal override LocalBase CreateLocal() => _db;
protected override ProcedureTxContextBase CreateTxContext(Internal.TxContext inner) =>
_cached ??= new RuntimeProcedureTxContext(inner);
private RuntimeProcedureTxContext? _cached;
}
internal sealed class RuntimeProcedureTxContext : ProcedureTxContextBase
{
internal RuntimeProcedureTxContext(Internal.TxContext inner)
: base(inner) { }
public new RuntimeLocal Db => (RuntimeLocal)base.Db;
}
internal sealed class RuntimeLocal : LocalBase { }
#pragma warning restore STDB_UNSTABLE