diff --git a/docs/docs/01-intro/01-overview.md b/docs/docs/01-intro/01-overview.md index 2e6c90ab9d9..089f37e786d 100644 --- a/docs/docs/01-intro/01-overview.md +++ b/docs/docs/01-intro/01-overview.md @@ -289,6 +289,124 @@ or at a specific time. +### Procedure + +A **procedure** is a function exported by a [database](#database), similar to a [reducer](#reducer). +Connected [clients](#client-side-sdks) can call procedures. +Procedures can perform additional operations not possible in reducers, including making HTTP requests to external services. +However, procedures don't automatically run in database transactions, +and must manually open and commit a transaction in order to read from or modify the database state. + +Procedures are currently in beta, and their API may change in upcoming SpacetimeDB releases. + + + + +Because procedures are unstable, Rust modules that define them must opt in to the `unstable` feature in their `Cargo.toml`: + +```toml +[dependencies] +spacetimedb = { version = "1.x", features = ["unstable"] } +``` + +Then, that module can define a procedure: + +```rust +#[spacetimedb::procedure] +pub fn make_request(ctx: &mut spacetimedb::ProcedureContext) -> String { + // ... +} +``` + +And a Rust [client](#client) can call that procedure: + +```rust +fn main() { + // ...setup code, then... + ctx.procedures.make_request(); +} +``` + +A Rust [client](#client) can also register a callback to run when a procedure call finishes, which will be invoked with that procedure's return value: + +```rust +fn main() { + // ...setup code, then... + ctx.procedures.make_request_then(|ctx, res| { + match res { + Ok(string) => log::info!("Procedure `make_request` returned {string}"), + Err(e) => log::error!("Procedure `make_request` failed! {e:?}"), + } + }) +} +``` + + + + +C# modules currently cannot define procedures. Support for defining procedures in C# modules will be released shortly. + +A C# [client](#client) can call a procedure defined by a Rust or TypeScript module: + +```csharp +void Main() +{ + // ...setup code, then... + ctx.Procedures.MakeRequest(); +} +``` + +A C# [client](#client) can also register a callback to run when a procedure call finishes, which will be invoked with that procedure's return value: + +```csharp +void Main() +{ + // ...setup code, then... + ctx.Procedures.MakeRequestThen((ctx, res) => + { + if (res.IsSuccess) + { + Log.Debug($"Procedure `make_request` returned {res.Value!}"); + } + else + { + throw new Exception($"Procedure `make_request` failed: {res.Error!}"); + } + }); +} +``` + + + + +A procedure can be defined in a TypeScript module: + +```typescript +spacetimedb.procedure("make_request", t.string(), ctx => { + // ... +}) +``` + +And a TypeScript [client](#client) can call that procedure: + +```typescript +ctx.procedures.makeRequest(); +``` + +A Rust [client](#client) can also register a callback to run when a procedure call finishes, which will be invoked with that procedure's return value: + +```typescript +ctx.procedures.makeRequest().then( + res => console.log(`Procedure make_request returned ${res}`), + err => console.error(`Procedure make_request failed! ${err}`), +); +``` + + + + +See [Procedures](/procedures) for more details about procedures. + ### Client A **client** is an application that connects to a [database](#database). A client logs in using an [identity](#identity) and receives an [connection id](#connectionid) to identify the connection. After that, it can call [reducers](#reducer) and query public [tables](#table). diff --git a/docs/docs/11-Procedures/01-overview.md b/docs/docs/11-Procedures/01-overview.md new file mode 100644 index 00000000000..f8acb7887e1 --- /dev/null +++ b/docs/docs/11-Procedures/01-overview.md @@ -0,0 +1,513 @@ +--- +Title: Overview +slug: /procedures +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Procedures - Overview + +A **procedure** is a function exported by a [database](/#database), similar to a [reducer](/#reducer). +Connected [clients](/#client-side-sdks** can call procedures. +Procedures can perform additional operations not possible in reducers, including making HTTP requests to external services. +However, procedures don't automatically run in database transactions, +and must manually open and commit a transaction in order to read from or modify the database state. +For this reason, prefer defining reducers rather than procedures unless you need to use one of the special procedure operators. + +:::warning +***Procedures are currently in beta, and their API may change in upcoming SpacetimeDB releases.*** +::: + +## Defining Procedures + + + + +Because procedures are unstable, Rust modules that define them must opt in to the `unstable` feature in their `Cargo.toml`: + +```toml +[dependencies] +spacetimedb = { version = "1.*", features = ["unstable"] } +``` + +Define a procedure by annotating a function with `#[spacetimedb::procedure]`. + +This function's first argument must be of type `&mut spacetimedb::ProcedureContext`. +By convention, this argument is named `ctx`. + +A procedure may accept any number of additional arguments. +Each argument must be of a type that implements `spacetimedb::SpacetimeType`. +When defining a `struct` or `enum`, annotate it with `#[derive(spacetimedb::SpacetimeType)]` +to make it usable as a procedure argument. +These argument values will not be broadcast to clients other than the caller. + +A procedure may return a value of any type that implements `spacetimedb::SpacetimeType`. +This return value will be sent to the caller, but will not be broadcast to any other clients. + +```rust +#[spacetimedb::procedure] +fn add_two_numbers(ctx: &mut spacetimedb::ProcedureContext, lhs: u32, rhs: u32) -> u64 { + lhs as u64 + rhs as u64 +} +``` + + + + +Support for procedures in C# modules is coming soon! + + + + +Define a procedure with `spacetimedb.procedure`: + +```typescript +spacetimedb.procedure( + "add_two_numbers", + { lhs: t.u32(), rhs: t.u32() }, + t.u64(), + (ctx, { lhs, rhs }) => BigInt(lhs) + BigInt(rhs), +); +``` + +The `spacetimedb.procedure` function takes: +* the procedure name, +* (optional) an object representing its parameter types, +* its return type, +* and the procedure function itself. + +The function will receive a `ProcedureContext` and an object of its arguments, and it must return +a value corresponding to its return type. This return value will be sent to the caller, but will +not be broadcast to any other clients. + + + + +### Accessing the database + + + + +Unlike reducers, procedures don't automatically run in database transactions. +This means there's no `ctx.db` field to access the database. +Instead, procedure code must manage transactions explicitly with `ProcedureContext::with_tx`. + +```rust +#[spacetimedb::table(name = my_table)] +struct MyTable { + a: u32, + b: String, +} + +#[spacetimedb::procedure] +fn insert_a_value(ctx: &mut ProcedureContext, a: u32, b: String) { + ctx.with_tx(|ctx| { + ctx.my_table().insert(MyTable { a, b }); + }); +} +``` + +`ProcedureContext::with_tx` takes a function of type `Fn(&TxContext) -> T`. +Within that function, the `&TxContext` can be used to access the database +[in all the same ways as a `ReducerContext`](https://docs.rs/spacetimedb/latest/spacetimedb/struct.ReducerContext.html). +When the function returns, the transaction will be committed, +and its changes to the database state will become permanent and be broadcast to clients. +If the function panics, the transaction will be rolled back, and its changes will be discarded. +However, for transactions that may fail, +[prefer calling `try_with_tx` and returning a `Result`](#fallible-database-operations) rather than panicking. + +:::warning +The function passed to `ProcedureContext::with_tx` may be invoked multiple times, +possibly seeing a different version of the database state each time. + +If invoked more than once with reference to the same database state, +it must perform the same operations and return the same result each time. + +If invoked more than once with reference to different database states, +values observed during prior runs must not influence the behavior of the function or the calling procedure. + +Avoid capturing mutable state within functions passed to `with_tx`. +::: + + + + +Unlike reducers, procedures don't automatically run in database transactions. +This means there's no `ctx.db` field to access the database. +Instead, procedure code must manage transactions explicitly with `ProcedureCtx.withTx`. + +```typescript +const MyTable = table( + { name: "my_table" }, + { + a: t.u32(), + b: t.string(), + }, +) + +const spacetimedb = schema(MyTable); + +#[spacetimedb::procedure] +spacetimedb.procedure("insert_a_value", { a: t.u32(), b: t.u32() }, t.unit(), (ctx, { a, b }) => { + ctx.withTx(ctx => { + ctx.myTable.insert({ a, b }); + }); + return {}; +}) +``` + +`ProcedureCtx.withTx` takes a function of `(ctx: TransactionCtx) => T`. +Within that function, the `TransactionCtx` can be used to access the database +[in all the same ways as a `ReducerCtx`](/modules/typescript#reducercontext) +When the function returns, the transaction will be committed, +and its changes to the database state will become permanent and be broadcast to clients. +If the function throws an error, the transaction will be rolled back, and its changes will be discarded. + +:::warning +The function passed to `ProcedureCtx.withTx` may be invoked multiple times, +possibly seeing a different version of the database state each time. + +If invoked more than once with reference to the same database state, +it must perform the same operations and return the same result each time. + +If invoked more than once with reference to different database states, +values observed during prior runs must not influence the behavior of the function or the calling procedure. + +Avoid capturing mutable state within functions passed to `withTx`. +::: + + + + +#### Fallible database operations + + + + +For fallible database operations, instead use `ProcedureContext::try_with_tx`: + +```rust +#[spacetimedb::procedure] +fn maybe_insert_a_value(ctx: &mut ProcedureContext, a: u32, b: String) { + ctx.try_with_tx(|ctx| { + if a < 10 { + return Err("a is less than 10!"); + } + ctx.my_table().insert(MyTable { a, b }); + Ok(()) + }); +} +``` + +`ProcedureContext::try_with_tx` takes a function of type `Fn(&TxContext) -> Result`. +If the function returns `Ok`, the transaction will be committed, +and its changes to the database state will become permanent and be broadcast to clients. +If that function returns `Err`, the transaction will be rolled back, and its changes will be discarded. + + + + +For fallible database operations, you can throw an error inside the transaction function: + +```typescript +spacetimedb.procedure("maybe_insert_a_value", { a: t.u32(), b: t.string() }, t.unit(), (ctx, { a, b }) => { + ctx.withTx(ctx => { + if (a < 10) { + throw new SenderError("a is less than 10!"); + } + ctx.myTable.insert({ a, b }); + }); +}) +``` + + + + +#### Reading values out of the database + + + + +Functions passed to +[`ProcedureContext::with_tx`](#accessing-the-database) and [`ProcedureContext::try_with_tx`](#fallible-database-operations) +may return a value, and that value will be returned to the calling procedure. + +Transaction return values are never saved or broadcast to clients, and are used only by the calling procedure. + +```rust +#[spacetimedb::table(name = player)] +struct Player { + id: spacetimedb::Identity, + level: u32, +} + +#[spacetimedb::procedure] +fn find_highest_level_player(ctx: &mut ProcedureContext) { + let highest_level_player = ctx.with_tx(|ctx| { + ctx.db.player().iter().max_by_key(|player| player.level) + }); + match highest_level_player { + Some(player) => log::info!("Congratulations to {}", player.id), + None => log::warn!("No players..."), + } +} +``` + + + + +Functions passed to +[`ProcedureCtx.withTx`](#accessing-the-database) +may return a value, and that value will be returned to the calling procedure. + +Transaction return values are never saved or broadcast to clients, and are used only by the calling procedure. + +```typescript +const Player = table( + { name: "player" }, + { + id: t.identity(), + level: t.u32(), + }, +); + +const spacetimedb = schema(Player); + +spacetimedb.procedure("find_highest_level_player", t.unit(), ctx => { + let highestLevelPlayer = ctx.withTx(ctx => + Iterator.from(ctx.db.player).reduce( + (a, b) => a == null || b.level > a.level ? b : a, + null + ) + ); + if (highestLevelPlayer != null) { + console.log("Congratulations to ", highestLevelPlayer.id); + } else { + console.warn("No players..."); + } + return {}; +}); +``` + + + + +## HTTP Requests + + + + +Procedures can make HTTP requests to external services using methods contained in `ctx.http`. + +`ctx.http.get` performs simple `GET` requests with no headers: + +```rust +#[spacetimedb::procedure] +fn get_request(ctx: &mut ProcedureContext) { + match ctx.http.get("https://example.invalid") { + Ok(response) => { + let (response, body) = response.into_parts(); + log::info!( + "Got response with status {} and body {}", + response.status, + body.into_string_lossy(), + ) + }, + Err(error) => log::error!("Request failed: {error:?}"), + } +} +``` + +`ctx.http.send` sends any [`http::Request`](https://docs.rs/http/latest/http/request/struct.Request.html) +whose body can be converted to `spacetimedb::http::Body`. +`http::Request` is re-exported as `spacetimedb::http::Request`. + +```rust +#[spacetimedb::procedure] +fn post_request(ctx: &mut spacetimedb::ProcedureContext) { + let request = spacetimedb::http::Request::builder() + .uri("https://example.invalid/upload") + .method("POST") + .header("Content-Type", "text/plain") + .body("This is the body of the HTTP request") + .expect("Building `Request` object failed"); + match ctx.http.send(request) { + Ok(response) => { + let (response, body) = response.into_parts(); + log::info!( + "Got response with status {} and body {}", + response.status, + body.into_string_lossy(), + ) + } + Err(error) => log::error!("Request failed: {error:?}"), + } +} +``` + +Each of these methods returns a [`http::Response`](https://docs.rs/http/latest/http/response/struct.Response.html#method.body) +containing a `spacetimedb::http::Body`. `http::Response` is re-exported as `spacetimedb::http::Response`. + +Set a timeout for a `ctx.http.send` request by including a `spacetimedb::http::Timeout` as an [`extension`](https://docs.rs/http/latest/http/request/struct.Builder.html#method.extension): + +```rust +#[spacetimedb::procedure] +fn get_request_with_short_timeout(ctx: &mut spacetimedb::ProcedureContext) { + let request = spacetimedb::http::Request::builder() + .uri("https://example.invalid") + .method("GET") + // Set a timeout of 10 ms. + .extension(spacetimedb::http::Timeout(std::time::Duration::from_millis(10).into())) + // Empty body for a `GET` request. + .body(()) + .expect("Building `Request` object failed"); + ctx.http.send(request).expect("HTTP request failed"); +} +``` + +Procedures can't send requests at the same time as holding open a [transaction](#accessing-the-database). + + + + +Procedures can make HTTP requests to external services using methods contained in `ctx.http`. + +`ctx.http.fetch` is similar to the browser `fetch()` API, but is synchronous. + +It can perform simple `GET` requests: + +```typescript +#[spacetimedb::procedure] +spacetimedb.procedure("get_request", t.unit(), ctx => { + try { + const response = ctx.http.fetch("https://example.invalid"); + const body = response.text(); + console.log(`Got response with status ${response.status} and body ${body}`); + } catch (e) { + console.error("Request failed: ", e); + } + return {}; +}); +``` + +It can also accept an options object to specify a body, headers, HTTP method, and timeout: + +```typescript +spacetimedb.procedure("post_request", t.unit(), ctx => { + try { + const response = ctx.http.fetch("https://example.invalid/upload", { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: "This is the body of the HTTP request", + }); + const body = response.text(); + console.log(`Got response with status ${response.status} and body {body}`); + } catch (e) { + console.error("Request failed: ", e); + } + return {}; +}); + +spacetimedb.procedure("get_request_with_short_timeout", t.unit(), ctx => { + try { + const response = ctx.http.fetch("https://example.invalid", { + method: "GET", + timeout: TimeDuration.fromMillis(10), + }); + const body = response.text(); + console.log(`Got response with status ${response.status} and body {body}`); + } catch (e) { + console.error("Request failed: ", e); + } + return {}; +}); +``` + +Procedures can't send requests at the same time as holding open a [transaction](#accessing-the-database). + + + + +## Calling procedures + + + + +Clients can invoke procedures using methods on `ctx.procedures`: + +```rust +ctx.procedures.insert_a_value(12, "Foo".to_string()); +``` + + + + +Clients can invoke procedures using methods on `ctx.Procedures`: + +```csharp +ctx.Procedures.InsertAValue(12, "Foo"); +``` + + + + +Clients can invoke procedures using methods on `ctx.procedures`: + +```typescript +ctx.procedures.insertAValue({ a: 12, b: "Foo" }); +``` + + + + +### Observing return values + + + + +A client can also invoke a procedure while registering a callback to run when it completes. +That callback will have access to the return value of the procedure, +or an error if the procedure fails. + +```rust +ctx.procedures.add_two_numbers_then(1, 2, |ctx, result| { + let sum = result.expect("Procedure failed"); + println!("1 + 2 = {sum}"); +}); +``` + + + + +A client can also invoke a procedure while registering a callback to run when it completes. +That callback will have access to the return value of the procedure, +or an error if the procedure fails. + +```csharp +ctx.Procedures.AddTwoNumbers(12, "Foo", (ctx, result) => +{ + if (result.IsSuccess) + { + Log.Info($"1 + 2 = {result.Value!}"); + } + else + { + throw result.Error!; + } +}); +``` + + + + +When a client invokes a procedure, it gets a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which resolves to the return value of the procedure. + +```typescript +ctx.procedures.addTwoNumbers({ lhs: 1, rhs: 2 }).then( + sum => console.log(`1 + 2 = ${sum}`) +); +``` + + + diff --git a/docs/docs/11-how-to/01-incremental-migrations.md b/docs/docs/12-how-to/01-incremental-migrations.md similarity index 100% rename from docs/docs/11-how-to/01-incremental-migrations.md rename to docs/docs/12-how-to/01-incremental-migrations.md diff --git a/docs/docs/11-how-to/02-reject-client-connections.md b/docs/docs/12-how-to/02-reject-client-connections.md similarity index 100% rename from docs/docs/11-how-to/02-reject-client-connections.md rename to docs/docs/12-how-to/02-reject-client-connections.md diff --git a/docs/docs/11-how-to/03-using-auth-claims.md b/docs/docs/12-how-to/03-using-auth-claims.md similarity index 100% rename from docs/docs/11-how-to/03-using-auth-claims.md rename to docs/docs/12-how-to/03-using-auth-claims.md diff --git a/docs/docs/11-how-to/_category_.json b/docs/docs/12-how-to/_category_.json similarity index 100% rename from docs/docs/11-how-to/_category_.json rename to docs/docs/12-how-to/_category_.json diff --git a/docs/docs/12-spacetimeauth/01-overview.md b/docs/docs/13-spacetimeauth/01-overview.md similarity index 100% rename from docs/docs/12-spacetimeauth/01-overview.md rename to docs/docs/13-spacetimeauth/01-overview.md diff --git a/docs/docs/12-spacetimeauth/02-creating-a-project.md b/docs/docs/13-spacetimeauth/02-creating-a-project.md similarity index 100% rename from docs/docs/12-spacetimeauth/02-creating-a-project.md rename to docs/docs/13-spacetimeauth/02-creating-a-project.md diff --git a/docs/docs/12-spacetimeauth/03-configuring-a-project.md b/docs/docs/13-spacetimeauth/03-configuring-a-project.md similarity index 100% rename from docs/docs/12-spacetimeauth/03-configuring-a-project.md rename to docs/docs/13-spacetimeauth/03-configuring-a-project.md diff --git a/docs/docs/12-spacetimeauth/04-testing.md b/docs/docs/13-spacetimeauth/04-testing.md similarity index 100% rename from docs/docs/12-spacetimeauth/04-testing.md rename to docs/docs/13-spacetimeauth/04-testing.md diff --git a/docs/docs/12-spacetimeauth/05-react-integration.md b/docs/docs/13-spacetimeauth/05-react-integration.md similarity index 100% rename from docs/docs/12-spacetimeauth/05-react-integration.md rename to docs/docs/13-spacetimeauth/05-react-integration.md diff --git a/docs/docs/13-http-api/01-authorization.md b/docs/docs/14-http-api/01-authorization.md similarity index 100% rename from docs/docs/13-http-api/01-authorization.md rename to docs/docs/14-http-api/01-authorization.md diff --git a/docs/docs/13-http-api/02-identity.md b/docs/docs/14-http-api/02-identity.md similarity index 100% rename from docs/docs/13-http-api/02-identity.md rename to docs/docs/14-http-api/02-identity.md diff --git a/docs/docs/13-http-api/03-database.md b/docs/docs/14-http-api/03-database.md similarity index 100% rename from docs/docs/13-http-api/03-database.md rename to docs/docs/14-http-api/03-database.md diff --git a/docs/docs/13-http-api/_category_.json b/docs/docs/14-http-api/_category_.json similarity index 100% rename from docs/docs/13-http-api/_category_.json rename to docs/docs/14-http-api/_category_.json diff --git a/docs/docs/14-internals/01-module-abi-reference.md b/docs/docs/15-internals/01-module-abi-reference.md similarity index 100% rename from docs/docs/14-internals/01-module-abi-reference.md rename to docs/docs/15-internals/01-module-abi-reference.md diff --git a/docs/docs/14-internals/02-sats-json.md b/docs/docs/15-internals/02-sats-json.md similarity index 100% rename from docs/docs/14-internals/02-sats-json.md rename to docs/docs/15-internals/02-sats-json.md diff --git a/docs/docs/14-internals/03-bsatn.md b/docs/docs/15-internals/03-bsatn.md similarity index 100% rename from docs/docs/14-internals/03-bsatn.md rename to docs/docs/15-internals/03-bsatn.md diff --git a/docs/docs/15-appendix/index.md b/docs/docs/16-appendix/index.md similarity index 100% rename from docs/docs/15-appendix/index.md rename to docs/docs/16-appendix/index.md