` — The args to pass to spacetimedb-update
+
+
+
+
+
+
+ This document was generated automatically by
+ clap-markdown.
+
+
diff --git a/docs/cli-reference/standalone-config.md b/docs/cli-reference/standalone-config.md
new file mode 100644
index 00000000..0ce6350d
--- /dev/null
+++ b/docs/cli-reference/standalone-config.md
@@ -0,0 +1,44 @@
+# `spacetimedb-standalone` configuration
+
+A local database instance (as started by `spacetime start`) can be configured in `{data-dir}/config.toml`, where `{data-dir}` is the database's data directory. This directory is printed when you run `spacetime start`:
+
+
+spacetimedb-standalone version: 1.0.0
+spacetimedb-standalone path: /home/user/.local/share/spacetime/bin/1.0.0/spacetimedb-standalone
+database running in data directory /home/user/.local/share/spacetime/data
+
+On Linux and macOS, this directory is by default `~/.local/share/spacetime/data`. On Windows, it's `%LOCALAPPDATA%\SpacetimeDB\data`.
+
+## `config.toml`
+
+- [`certificate-authority`](#certificate-authority)
+- [`logs`](#logs)
+
+### `certificate-authority`
+
+```toml
+[certificate-authority]
+jwt-priv-key-path = "/path/to/id_ecdsas"
+jwt-pub-key-path = "/path/to/id_ecdsas.pub"
+```
+
+The `certificate-authority` table lets you configure the public and private keys used by the database to sign tokens.
+
+### `logs`
+
+```toml
+[logs]
+level = "error"
+directives = [
+ "spacetimedb=warn",
+ "spacetimedb_standalone=info",
+]
+```
+
+#### `logs.level`
+
+Can be one of `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"`, or `"off"`, case-insensitive. Only log messages of the specified level or higher will be output; e.g. if set to `warn`, only `error` and `warn`-level messages will be logged.
+
+#### `logs.directives`
+
+A list of filtering directives controlling what messages get logged, which overwrite the global [`logs.level`](#logslevel). See [`tracing documentation`](https://docs.rs/tracing-subscriber/0.3/tracing_subscriber/filter/struct.EnvFilter.html#directives) for syntax. Note that this is primarily intended as a debugging tool, and log message fields and targets are not considered stable.
diff --git a/docs/deploying/maincloud.md b/docs/deploying/maincloud.md
new file mode 100644
index 00000000..ea14ebbd
--- /dev/null
+++ b/docs/deploying/maincloud.md
@@ -0,0 +1,30 @@
+# Deploy to Maincloud
+
+Maincloud is a managed cloud service that provides developers an easy way to deploy their SpacetimeDB apps to the cloud.
+
+## Deploy via CLI
+
+1. Install the SpacetimeDB CLI for your platform: [Install SpacetimeDB](/install)
+1. Create your module (see [Getting Started](/docs/getting-started))
+1. Publish to Maincloud:
+
+```bash
+spacetime publish -s maincloud my-cool-module
+```
+
+## Connecting your Identity to the Web Dashboard
+
+By logging in your CLI via spacetimedb.com, you can view your published modules on the web dashboard.
+
+If you did not log in with spacetimedb.com when publishing your module, you can log in by running:
+```bash
+spacetime logout
+spacetime login
+```
+
+1. Open the SpacetimeDB website and log in using your GitHub login.
+1. You should now be able to see your published modules [by navigating to your profile on the website](/profile).
+
+---
+
+With SpacetimeDB Maincloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment.
diff --git a/docs/deploying/testnet.md b/docs/deploying/testnet.md
deleted file mode 100644
index ce648043..00000000
--- a/docs/deploying/testnet.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# SpacetimeDB Cloud Deployment
-
-The SpacetimeDB Cloud is a managed cloud service that provides developers an easy way to deploy their SpacetimeDB apps to the cloud.
-
-Currently only the `testnet` is available for SpacetimeDB cloud which is subject to wipes. The `mainnet` will be available soon.
-
-## Deploy via CLI
-
-1. [Install](/install) the SpacetimeDB CLI.
-1. Configure your CLI to use the SpacetimeDB Cloud. To do this, run the `spacetime server` command:
-
-```bash
-spacetime server add --default "https://testnet.spacetimedb.com" testnet
-```
-
-## Connecting your Identity to the Web Dashboard
-
-By associating an email with your CLI identity, you can view your published modules on the web dashboard.
-
-1. Get your identity using the `spacetime identity list` command. Copy it to your clipboard.
-1. Connect your email address to your identity using the `spacetime identity set-email` command:
-
-```bash
-spacetime identity set-email
-```
-
-1. Open the SpacetimeDB website and log in using your email address.
-1. Choose your identity from the dropdown menu.
-1. Validate your email address by clicking the link in the email you receive.
-1. You should now be able to see your published modules on the web dashboard.
-
----
-
-With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment.
diff --git a/docs/how-to/incremental-migrations.md b/docs/how-to/incremental-migrations.md
new file mode 100644
index 00000000..3f9106b1
--- /dev/null
+++ b/docs/how-to/incremental-migrations.md
@@ -0,0 +1,369 @@
+# Incremental Migrations
+
+SpacetimeDB does not provide built-in support for general schema-modifying migrations. It does, however, allow adding new tables, and changing reducers' definitions in arbitrary ways. It's possible to run general migrations using an external tool, but this is tedious, necessitates downtime, and imposes the requirement that you update all your clients at the same time as publishing your new module version.
+
+Our friends at [Lightfox Games](https://www.lightfoxgames.com/) taught us a pattern they call "incremental migrations," which mitigates all these problems, and works perfectly with SpacetimeDB's capabilities. The short version is that, instead of altering an existing table, you add a new table with the desired new schema. Whenever your module wants to access a row from that table, it first checks the new table. If the row is present in the new table, then you've already migrated, so do whatever you want to do. If the new table doesn't have the row, instead look it up in the old table, compute and insert a row for the new table, and use that. (If the row isn't present in either the old or new table, it's just not present.) If possible, you should also update the row in the old table to match any mutations that happen in the new table, so that outdated clients can still function.
+
+This has several advantages:
+- SpacetimeDB's module hotswapping makes this a zero-downtime update. Write your new module, `spacetime publish` it, and watch the new table populate as it's used.
+- It amortizes the cost of transforming rows or computing new columns across many transactions. Rows will only be added to the new table when they're needed.
+- In many cases, old clients from before the update can coexist with new clients that use the new table. You can publish the updated module without disconnecting your clients, roll out the client update through normal channels, and allow your users to update at their own pace.
+
+For example, imagine we have a table `player` which stores information about our players:
+
+
+
+```rust
+#[spacetimedb::table(name = character, public)]
+pub struct Character {
+ #[primary_key]
+ player_id: Identity,
+ #[unique]
+ nickname: String,
+ level: u32,
+ class: Class,
+}
+
+#[derive(SpacetimeType, Debug, Copy, Clone)]
+pub enum Class {
+ Fighter,
+ Caster,
+ Medic,
+}
+```
+
+We'll write a few helper functions and some simple reducers:
+
+```rust
+#[spacetimedb::reducer]
+fn create_character(ctx: &ReducerContext, class: Class, nickname: String) {
+ log::info!(
+ "Creating new level 1 {class:?} named {nickname}",
+ );
+ ctx.db.character().insert(Character {
+ player_id: ctx.sender,
+ nickname,
+ level: 1,
+ class,
+ });
+}
+
+fn find_character_for_player(ctx: &ReducerContext) -> Character {
+ ctx.db
+ .character()
+ .player_id()
+ .find(ctx.sender)
+ .expect("Player has not created a character")
+}
+
+fn update_character(ctx: &ReducerContext, character: Character) {
+ ctx.db.character().player_id().update(character);
+}
+
+#[spacetimedb::reducer]
+fn rename_character(ctx: &ReducerContext, new_name: String) {
+ let character = find_character_for_player(ctx);
+ log::info!(
+ "Renaming {} to {}",
+ character.nickname,
+ new_name,
+ );
+ update_character(
+ ctx,
+ Character {
+ nickname: new_name,
+ ..character
+ },
+ );
+}
+
+#[spacetimedb::reducer]
+fn level_up_character(ctx: &ReducerContext) {
+ let character = find_character_for_player(ctx);
+ log::info!(
+ "Leveling up {} from {} to {}",
+ character.nickname,
+ character.level,
+ character.level + 1,
+ );
+ update_character(
+ ctx,
+ Character {
+ level: character.level + 1,
+ ..character
+ },
+ );
+}
+```
+
+We'll play around a bit with `spacetime call` to set up a character:
+
+```sh
+$ spacetime logs incr-migration-demo -f &
+
+$ spacetime call incr-migration-demo create_character '{ "Fighter": {} }' "Phoebe"
+
+2025-01-07T15:32:57.447286Z INFO: src/lib.rs:21: Creating new level 1 Fighter named Phoebe
+
+$ spacetime call -s local incr-migration-demo rename_character "Gefjon"
+
+2025-01-07T15:33:48.966134Z INFO: src/lib.rs:48: Renaming Phoebe to Gefjon
+
+$ spacetime call -s local incr-migration-demo level_up_character
+
+2025-01-07T15:34:01.437495Z INFO: src/lib.rs:66: Leveling up Gefjon from 1 to 2
+
+$ spacetime sql incr-migration-demo 'SELECT * FROM character'
+
+ player_id | nickname | level | class
+-----------+----------+-------+----------------
+ | "Gefjon" | 2 | (Fighter = ())
+```
+
+See [the SATS JSON reference](/docs/sats-json) for more on the encoding of arguments to `spacetime call`.
+
+Now we want to add a new feature: each player should be able to align themselves with the forces of good or evil, so we can get some healthy competition going between our players. We'll start each character off with `Alliance::Neutral`, and then offer them a reducer `choose_alliance` to set it to either `Alliance::Good` or `Alliance::Evil`. Our first attempt will be to add a new column to the type `Character`:
+
+```rust
+#[spacetimedb::table(name = character, public)]
+struct Character {
+ #[primary_key]
+ player_id: Identity,
+ nickname: String,
+ level: u32,
+ class: Class,
+ alliance: Alliance,
+}
+
+#[derive(SpacetimeType, Debug, Copy, Clone)]
+enum Alliance {
+ Good,
+ Neutral,
+ Evil,
+}
+
+#[spacetimedb::reducer]
+fn choose_alliance(ctx: &ReducerContext, alliance: Alliance) {
+ let character = find_character_for_player(ctx);
+ log::info!(
+ "Setting {}'s alliance to {:?} for player {}",
+ character.nickname,
+ alliance,
+ ctx.sender,
+ );
+ update_character(
+ ctx,
+ Character {
+ alliance,
+ ..character
+ },
+ );
+}
+```
+
+But that will fail, since SpacetimeDB doesn't know how to update our existing `character` rows with the new column:
+
+```
+Error: Database update rejected: Errors occurred:
+Adding a column alliance to table character requires a manual migration
+```
+
+Instead, we'll add a new table, `character_v2`, which will coexist with our original `character` table:
+
+```rust
+#[spacetimedb::table(name = character_v2, public)]
+struct CharacterV2 {
+ #[primary_key]
+ player_id: Identity,
+ nickname: String,
+ level: u32,
+ class: Class,
+ alliance: Alliance,
+}
+```
+
+When a new player creates a character, we'll make rows in both tables for them. This way, any old clients that are still subscribing to the original `character` table will continue to work, though of course they won't know about the character's alliance.
+
+```rust
+#[spacetimedb::reducer]
+fn create_character(ctx: &ReducerContext, class: Class, nickname: String) {
+ log::info!(
+ "Creating new level 1 {class:?} named {nickname} for player {}",
+ ctx.sender,
+ );
+
+ ctx.db.character().insert(Character {
+ player_id: ctx.sender,
+ nickname: nickname.clone(),
+ level: 1,
+ class,
+ });
+
+ ctx.db.character_v2().insert(CharacterV2 {
+ player_id: ctx.sender,
+ nickname,
+ level: 1,
+ class,
+ alliance: Alliance::Neutral,
+ });
+}
+```
+
+We'll update our helper functions so that they operate on `character_v2` rows. In `find_character_for_player`, if we don't see the player's row in `character_v2`, we'll migrate it from `character` on the fly. In this case, we'll make the player neutral, since they haven't chosen an alliance yet.
+
+```rust
+fn find_character_for_player(ctx: &ReducerContext) -> CharacterV2 {
+ if let Some(character) = ctx.db.character_v2().player_id().find(ctx.sender) {
+ // Already migrated; just return the new player.
+ return character;
+ }
+
+ // Not yet migrated; look up an old character and update it.
+ let old_character = ctx
+ .db
+ .character()
+ .player_id()
+ .find(ctx.sender)
+ .expect("Player has not created a character");
+
+ ctx.db.character_v2().insert(CharacterV2 {
+ player_id: old_character.player_id,
+ nickname: old_character.nickname,
+ level: old_character.level,
+ class: old_character.class,
+ alliance: Alliance::Neutral,
+ })
+}
+```
+
+Just like when creating a new character, when we update a `character_v2` row, we'll also update the old `character` row, so that outdated clients can continue to function. It's very important that we perform the same translation between `character` and `character_v2` rows here as in `create_character` and `find_character_for_player`.
+
+```rust
+fn update_character(ctx: &ReducerContext, character: CharacterV2) {
+ ctx.db.character().player_id().update(Character {
+ player_id: character.player_id,
+ nickname: character.nickname.clone(),
+ level: character.level,
+ class: character.class,
+ });
+ ctx.db.character_v2().player_id().update(character);
+}
+```
+
+Then we can make trivial modifications to the callers of `update_character` so that they pass in `CharacterV2` instances:
+
+```rust
+#[spacetimedb::reducer]
+fn rename_character(ctx: &ReducerContext, new_name: String) {
+ let character = find_character_for_player(ctx);
+ log::info!(
+ "Renaming {} to {}",
+ character.nickname,
+ new_name,
+ );
+ update_character(
+ ctx,
+ CharacterV2 {
+ nickname: new_name,
+ ..character
+ },
+ );
+}
+
+#[spacetimedb::reducer]
+fn level_up_character(ctx: &ReducerContext) {
+ let character = find_character_for_player(ctx);
+ log::info!(
+ "Leveling up {} from {} to {}",
+ character.nickname,
+ character.level,
+ character.level + 1,
+ );
+ update_character(
+ ctx,
+ CharacterV2 {
+ level: character.level + 1,
+ ..character
+ },
+ );
+}
+```
+
+And finally, we can define our new `choose_alliance` reducer:
+
+```rust
+#[spacetimedb::reducer]
+fn choose_alliance(ctx: &ReducerContext, alliance: Alliance) {
+ let character = find_character_for_player(ctx);
+ log::info!(
+ "Setting alliance of {} to {:?}",
+ character.nickname,
+ alliance,
+ );
+ update_character(
+ ctx,
+ CharacterV2 {
+ alliance,
+ ..character
+ },
+ );
+}
+```
+
+A bit more playing around with the CLI will show us that everything works as intended:
+
+```sh
+# Our row in `character` still exists:
+$ spacetime sql incr-migration-demo 'SELECT * FROM character'
+
+ player_id | nickname | level | class
+-----------+----------+-------+----------------
+ | "Gefjon" | 2 | (Fighter = ())
+
+# We haven't triggered the "Gefjon" row to migrate yet, so `character_v2` is empty:
+$ spacetime sql -s local incr-migration-demo 'SELECT * FROM character_v2'
+
+ player_id | nickname | level | class | alliance
+-----------+----------+-------+-------+----------
+
+# Accessing our character, e.g. by leveling up, will cause it to migrate into `character_v2`:
+$ spacetime call incr-migration-demo level_up_character
+
+2025-01-07T16:00:20.500600Z INFO: src/lib.rs:110: Leveling up Gefjon from 2 to 3
+
+# Now `character_v2` is populated:
+$ spacetime sql incr-migration-demo 'SELECT * FROM character_v2'
+
+ player_id | nickname | level | class | alliance
+-----------+----------+-------+----------------+----------------
+ | "Gefjon" | 3 | (Fighter = ()) | (Neutral = ())
+
+# The original row in `character` still got updated by `level_up_character`,
+# so outdated clients can continue to function:
+$ spacetime sql incr-migration-demo 'SELECT * FROM character'
+
+ player_id | nickname | level | class
+-----------+----------+-------+----------------
+ | "Gefjon" | 3 | (Fighter = ())
+
+# We can set our alliance:
+$ spacetime call incr-migration-demo choose_alliance '{ "Good": {} }'
+
+2025-01-07T16:13:53.816501Z INFO: src/lib.rs:129: Setting alliance of Gefjon to Good
+
+# And that change shows up in `character_v2`:
+$ spacetime sql incr-migration-demo 'SELECT * FROM character_v2'
+
+ player_id | nickname | level | class | alliance
+-----------+----------+-------+----------------+-------------
+ | "Gefjon" | 3 | (Fighter = ()) | (Good = ())
+
+# But `character` is not changed, since it doesn't know about alliances:
+$ spacetime sql incr-migration-demo 'SELECT * FROM character'
+
+ player_id | nickname | level | class
+-----------+----------+-------+----------------
+ | "Gefjon" | 3 | (Fighter = ())
+```
+
+Now that we know how to define incremental migrations, we can add new features that would seem to require breaking schema changes without cumbersome external migration tools and while maintaining compatibility of outdated clients! The complete for this tutorial is on GitHub in the `clockworklabs/incr-migration-demo` repository, in branches [`v1`](https://github.com/clockworklabs/incr-migration-demo/tree/v1), [`fails-publish`](https://github.com/clockworklabs/incr-migration-demo/tree/fails-publish) and [`v2`](https://github.com/clockworklabs/incr-migration-demo/tree/v2).
diff --git a/docs/http/database.md b/docs/http/database.md
index b23701e8..8a73759c 100644
--- a/docs/http/database.md
+++ b/docs/http/database.md
@@ -1,497 +1,406 @@
-# `/database` HTTP API
+# `/v1/database` HTTP API
-The HTTP endpoints in `/database` allow clients to interact with Spacetime databases in a variety of ways, including retrieving information, creating and deleting databases, invoking reducers and evaluating SQL queries.
+The HTTP endpoints in `/v1/database` allow clients to interact with Spacetime databases in a variety of ways, including retrieving information, creating and deleting databases, invoking reducers and evaluating SQL queries.
## At a glance
-| Route | Description |
-| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
-| [`/database/dns/:name GET`](#databasednsname-get) | Look up a database's address by its name. |
-| [`/database/reverse_dns/:address GET`](#databasereverse_dnsaddress-get) | Look up a database's name by its address. |
-| [`/database/set_name GET`](#databaseset_name-get) | Set a database's name, given its address. |
-| [`/database/ping GET`](#databaseping-get) | No-op. Used to determine whether a client can connect. |
-| [`/database/register_tld GET`](#databaseregister_tld-get) | Register a top-level domain. |
-| [`/database/publish POST`](#databasepublish-post) | Publish a database given its module code. |
-| [`/database/delete/:address POST`](#databasedeleteaddress-post) | Delete a database. |
-| [`/database/subscribe/:name_or_address GET`](#databasesubscribename_or_address-get) | Begin a [WebSocket connection](/docs/ws). |
-| [`/database/call/:name_or_address/:reducer POST`](#databasecallname_or_addressreducer-post) | Invoke a reducer in a database. |
-| [`/database/schema/:name_or_address GET`](#databaseschemaname_or_address-get) | Get the schema for a database. |
-| [`/database/schema/:name_or_address/:entity_type/:entity GET`](#databaseschemaname_or_addressentity_typeentity-get) | Get a schema for a particular table or reducer. |
-| [`/database/info/:name_or_address GET`](#databaseinfoname_or_address-get) | Get a JSON description of a database. |
-| [`/database/logs/:name_or_address GET`](#databaselogsname_or_address-get) | Retrieve logs from a database. |
-| [`/database/sql/:name_or_address POST`](#databasesqlname_or_address-post) | Run a SQL query against a database. |
-
-## `/database/dns/:name GET`
-
-Look up a database's address by its name.
+| Route | Description |
+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
+| [`POST /v1/database`](#post-v1database) | Publish a new database given its module code. |
+| [`POST /v1/database/:name_or_identity`](#post-v1databasename_or_identity) | Publish to a database given its module code. |
+| [`GET /v1/database/:name_or_identity`](#get-v1databasename_or_identity) | Get a JSON description of a database. |
+| [`DELETE /v1/database/:name_or_identity`](#post-v1databasename_or_identity) | Delete a database. |
+| [`GET /v1/database/:name_or_identity/names`](#get-v1databasename_or_identitynames) | Get the names this database can be identified by. |
+| [`POST /v1/database/:name_or_identity/names`](#post-v1databasename_or_identitynames) | Add a new name for this database. |
+| [`PUT /v1/database/:name_or_identity/names`](#put-v1databasename_or_identitynames) | Set the list of names for this database. |
+| [`GET /v1/database/:name_or_identity/identity`](#get-v1databasename_or_identityidentity) | Get the identity of a database. |
+| [`GET /v1/database/:name_or_identity/subscribe`](#get-v1databasename_or_identitysubscribe) | Begin a WebSocket connection. |
+| [`POST /v1/database/:name_or_identity/call/:reducer`](#post-v1databasename_or_identitycallreducer) | Invoke a reducer in a database. |
+| [`GET /v1/database/:name_or_identity/schema`](#get-v1databasename_or_identityschema) | Get the schema for a database. |
+| [`GET /v1/database/:name_or_identity/logs`](#get-v1databasename_or_identitylogs) | Retrieve logs from a database. |
+| [`POST /v1/database/:name_or_identity/sql`](#post-v1databasename_or_identitysql) | Run a SQL query against a database. |
+
+## `POST /v1/database`
+
+Publish a new database with no name.
-Accessible through the CLI as `spacetime dns lookup `.
-
-#### Parameters
-
-| Name | Value |
-| ------- | ------------------------- |
-| `:name` | The name of the database. |
-
-#### Returns
-
-If a database with that name exists, returns JSON in the form:
-
-```typescript
-{ "Success": {
- "domain": string,
- "address": string
-} }
-```
-
-If no database with that name exists, returns JSON in the form:
-
-```typescript
-{ "Failure": {
- "domain": string
-} }
-```
-
-## `/database/reverse_dns/:address GET`
+Accessible through the CLI as `spacetime publish`.
-Look up a database's name by its address.
+#### Required Headers
-Accessible through the CLI as `spacetime dns reverse-lookup `.
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
-#### Parameters
+#### Data
-| Name | Value |
-| ---------- | ---------------------------- |
-| `:address` | The address of the database. |
+A WebAssembly module in the [binary format](https://webassembly.github.io/spec/core/binary/index.html).
#### Returns
-Returns JSON in the form:
+If the database was successfully published, returns JSON in the form:
```typescript
-{ "names": array }
+{ "Success": {
+ "database_identity": string,
+ "op": "created" | "updated"
+} }
```
-where `` is a JSON array of strings, each of which is a name which refers to the database.
-
-## `/database/set_name GET`
+## `POST /v1/database/:name_or_identity`
-Set the name associated with a database.
+Publish to a database with the specified name or identity. If the name doesn't exist, creates a new database.
-Accessible through the CLI as `spacetime dns set-name `.
+Accessible through the CLI as `spacetime publish`.
#### Query Parameters
-| Name | Value |
-| -------------- | ------------------------------------------------------------------------- |
-| `address` | The address of the database to be named. |
-| `domain` | The name to register. |
-| `register_tld` | A boolean; whether to register the name as a TLD. Should usually be true. |
+| Name | Value |
+| ------- | --------------------------------------------------------------------------------- |
+| `clear` | A boolean; whether to clear any existing data when updating an existing database. |
#### Required Headers
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
-#### Returns
+#### Data
-If the name was successfully set, returns JSON in the form:
+A WebAssembly module in the [binary format](https://webassembly.github.io/spec/core/binary/index.html).
-```typescript
-{ "Success": {
- "domain": string,
- "address": string
-} }
-```
+#### Returns
-If the top-level domain is not registered, and `register_tld` was not specified, returns JSON in the form:
+If the database was successfully published, returns JSON in the form:
```typescript
-{ "TldNotRegistered": {
- "domain": string
+{ "Success": {
+ "domain": null | string,
+ "database_identity": string,
+ "op": "created" | "updated"
} }
```
-If the top-level domain is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form:
+If a database with the given name exists, but the identity provided in the `Authorization` header does not have permission to edit it, returns `401 UNAUTHORIZED` along with JSON in the form:
```typescript
{ "PermissionDenied": {
- "domain": string
+ "name": string
} }
```
-> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes.
+## `GET /v1/database/:name_or_identity`
-## `/database/ping GET`
+Get a database's identity, owner identity, host type, number of replicas and a hash of its WASM module.
-Does nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB.
+#### Returns
-## `/database/register_tld GET`
+Returns JSON in the form:
-Register a new Spacetime top-level domain. A TLD is the part of a database name before the first `/`. For example, in the name `tyler/bitcraft`, the TLD is `tyler`. Each top-level domain is owned by at most one identity, and only the owner can publish databases with that TLD.
+```typescript
+{
+ "database_identity": string,
+ "owner_identity": string,
+ "host_type": "wasm",
+ "initial_program": string
+}
+```
-> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes.
+| Field | Type | Meaning |
+| --------------------- | ------ | ---------------------------------------------------------------- |
+| `"database_identity"` | String | The Spacetime identity of the database. |
+| `"owner_identity"` | String | The Spacetime identity of the database's owner. |
+| `"host_type"` | String | The module host type; currently always `"wasm"`. |
+| `"initial_program"` | String | Hash of the WASM module with which the database was initialized. |
-Accessible through the CLI as `spacetime dns register-tld `.
+## `DELETE /v1/database/:name_or_identity`
-#### Query Parameters
+Delete a database.
-| Name | Value |
-| ----- | -------------------------------------- |
-| `tld` | New top-level domain name to register. |
+Accessible through the CLI as `spacetime delete `.
#### Required Headers
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
-
-#### Returns
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
-If the domain is successfully registered, returns JSON in the form:
+## `GET /v1/database/:name_or_identity/names`
-```typescript
-{ "Success": {
- "domain": string
-} }
-```
+Get the names this datbase can be identified by.
-If the domain is already registered to the caller, returns JSON in the form:
+Accessible through the CLI as `spacetime dns reverse-lookup `.
-```typescript
-{ "AlreadyRegistered": {
- "domain": string
-} }
-```
+#### Returns
-If the domain is already registered to another identity, returns JSON in the form:
+Returns JSON in the form:
```typescript
-{ "Unauthorized": {
- "domain": string
-} }
+{ "names": array }
```
-## `/database/publish POST`
-
-Publish a database.
-
-Accessible through the CLI as `spacetime publish`.
+where `` is a JSON array of strings, each of which is a name which refers to the database.
-#### Query Parameters
+## `POST /v1/database/:name_or_identity/names`
-| Name | Value |
-| ----------------- | ------------------------------------------------------------------------------------------------ |
-| `host_type` | Optional; a SpacetimeDB module host type. Currently, only `"wasmer"` is supported. |
-| `clear` | A boolean; whether to clear any existing data when updating an existing database. |
-| `name_or_address` | The name of the database to publish or update, or the address of an existing database to update. |
-| `register_tld` | A boolean; whether to register the database's top-level domain. |
+Add a new name for this database.
#### Required Headers
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
#### Data
-A WebAssembly module in the [binary format](https://webassembly.github.io/spec/core/binary/index.html).
+Takes as the request body a string containing the new name of the database.
#### Returns
-If the database was successfully published, returns JSON in the form:
+If the name was successfully set, returns JSON in the form:
```typescript
{ "Success": {
- "domain": null | string,
- "address": string,
- "op": "created" | "updated"
+ "domain": string,
+ "database_result": string
} }
```
-If the top-level domain for the requested name is not registered, returns JSON in the form:
+If the new name already exists but the identity provided in the `Authorization` header does not have permission to edit it, returns JSON in the form:
```typescript
-{ "TldNotRegistered": {
+{ "PermissionDenied": {
"domain": string
} }
```
-If the top-level domain for the requested name is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form:
+## `PUT /v1/database/:name_or_identity/names`
-```typescript
-{ "PermissionDenied": {
- "domain": string
-} }
-```
+Set the list of names for this database.
+
+#### Required Headers
-> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes.
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
-## `/database/delete/:address POST`
+#### Data
-Delete a database.
+Takes as the request body a list of names, as a JSON array of strings.
-Accessible through the CLI as `spacetime delete `.
+#### Returns
-#### Parameters
+If the name was successfully set, returns JSON in the form:
-| Name | Address |
-| ---------- | ---------------------------- |
-| `:address` | The address of the database. |
+```typescript
+{ "Success": null }
+```
-#### Required Headers
+If any of the new names already exist but the identity provided in the `Authorization` header does not have permission to edit it, returns `401 UNAUTHORIZED` along with JSON in the form:
+
+```typescript
+{ "PermissionDenied": null }
+```
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
+## `GET /v1/database/:name_or_identity/identity`
-## `/database/subscribe/:name_or_address GET`
+Get the identity of a database.
-Begin a [WebSocket connection](/docs/ws) with a database.
+Accessible through the CLI as `spacetime dns lookup `.
-#### Parameters
+#### Returns
-| Name | Value |
-| ------------------ | ---------------------------- |
-| `:name_or_address` | The address of the database. |
+Returns a hex string of the specified database's identity.
+
+## `GET /v1/database/:name_or_identity/subscribe`
+
+Begin a WebSocket connection with a database.
#### Required Headers
For more information about WebSocket headers, see [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455).
-| Name | Value |
-| ------------------------ | ---------------------------------------------------------------------------------------------------- |
-| `Sec-WebSocket-Protocol` | [`v1.bin.spacetimedb`](/docs/ws#binary-protocol) or [`v1.text.spacetimedb`](/docs/ws#text-protocol). |
-| `Connection` | `Updgrade` |
-| `Upgrade` | `websocket` |
-| `Sec-WebSocket-Version` | `13` |
-| `Sec-WebSocket-Key` | A 16-byte value, generated randomly by the client, encoded as Base64. |
+| Name | Value |
+| ------------------------ | --------------------------------------------------------------------- |
+| `Sec-WebSocket-Protocol` | `v1.bsatn.spacetimedb` or `v1.json.spacetimedb` |
+| `Connection` | `Updgrade` |
+| `Upgrade` | `websocket` |
+| `Sec-WebSocket-Version` | `13` |
+| `Sec-WebSocket-Key` | A 16-byte value, generated randomly by the client, encoded as Base64. |
+
+The SpacetimeDB binary WebSocket protocol, `v1.bsatn.spacetimedb`, encodes messages as well as reducer and row data using [BSATN](/docs/bsatn).
+Its messages are defined [here](https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/client-api-messages/src/websocket.rs).
+
+The SpacetimeDB text WebSocket protocol, `v1.json.spacetimedb`, encodes messages according to the [SATS-JSON format](/docs/sats-json).
#### Optional Headers
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
-## `/database/call/:name_or_address/:reducer POST`
+## `POST /v1/database/:name_or_identity/call/:reducer`
Invoke a reducer in a database.
-#### Parameters
+#### Path parameters
-| Name | Value |
-| ------------------ | ------------------------------------ |
-| `:name_or_address` | The name or address of the database. |
-| `:reducer` | The name of the reducer. |
+| Name | Value |
+| ---------- | ------------------------ |
+| `:reducer` | The name of the reducer. |
#### Required Headers
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
#### Data
A JSON array of arguments to the reducer.
-## `/database/schema/:name_or_address GET`
+## `GET /v1/database/:name_or_identity/schema`
Get a schema for a database.
-Accessible through the CLI as `spacetime describe `.
-
-#### Parameters
-
-| Name | Value |
-| ------------------ | ------------------------------------ |
-| `:name_or_address` | The name or address of the database. |
+Accessible through the CLI as `spacetime describe `.
#### Query Parameters
-| Name | Value |
-| -------- | ----------------------------------------------------------- |
-| `expand` | A boolean; whether to include full schemas for each entity. |
+| Name | Value |
+| --------- | ------------------------------------------------ |
+| `version` | The version of `RawModuleDef` to return, e.g. 9. |
#### Returns
-Returns a JSON object with two properties, `"entities"` and `"typespace"`. For example, on the default module generated by `spacetime init` with `expand=true`, returns:
+Returns a `RawModuleDef` in JSON form.
-```typescript
+
+Example response from `/schema?version=9` for the default module generated by `spacetime init`
+
+```json
{
- "entities": {
- "Person": {
- "arity": 1,
- "schema": {
- "elements": [
- {
- "algebraic_type": {
- "Builtin": {
+ "typespace": {
+ "types": [
+ {
+ "Product": {
+ "elements": [
+ {
+ "name": {
+ "some": "name"
+ },
+ "algebraic_type": {
"String": []
}
- },
- "name": {
- "some": "name"
}
- }
- ]
+ ]
+ }
+ }
+ ]
+ },
+ "tables": [
+ {
+ "name": "person",
+ "product_type_ref": 0,
+ "primary_key": [],
+ "indexes": [],
+ "constraints": [],
+ "sequences": [],
+ "schedule": {
+ "none": []
},
- "type": "table"
- },
- "__init__": {
- "arity": 0,
- "schema": {
- "elements": [],
- "name": "__init__"
+ "table_type": {
+ "User": []
},
- "type": "reducer"
- },
- "add": {
- "arity": 1,
- "schema": {
+ "table_access": {
+ "Private": []
+ }
+ }
+ ],
+ "reducers": [
+ {
+ "name": "add",
+ "params": {
"elements": [
{
- "algebraic_type": {
- "Builtin": {
- "String": []
- }
- },
"name": {
"some": "name"
+ },
+ "algebraic_type": {
+ "String": []
}
}
- ],
- "name": "add"
+ ]
},
- "type": "reducer"
+ "lifecycle": {
+ "none": []
+ }
},
- "say_hello": {
- "arity": 0,
- "schema": {
- "elements": [],
- "name": "say_hello"
+ {
+ "name": "identity_connected",
+ "params": {
+ "elements": []
},
- "type": "reducer"
- }
- },
- "typespace": [
+ "lifecycle": {
+ "some": {
+ "OnConnect": []
+ }
+ }
+ },
{
- "Product": {
- "elements": [
- {
- "algebraic_type": {
- "Builtin": {
- "String": []
- }
- },
- "name": {
- "some": "name"
- }
- }
- ]
+ "name": "identity_disconnected",
+ "params": {
+ "elements": []
+ },
+ "lifecycle": {
+ "some": {
+ "OnDisconnect": []
+ }
+ }
+ },
+ {
+ "name": "init",
+ "params": {
+ "elements": []
+ },
+ "lifecycle": {
+ "some": {
+ "Init": []
+ }
+ }
+ },
+ {
+ "name": "say_hello",
+ "params": {
+ "elements": []
+ },
+ "lifecycle": {
+ "none": []
}
}
- ]
-}
-```
-
-The `"entities"` will be an object whose keys are table and reducer names, and whose values are objects of the form:
-
-```typescript
-{
- "arity": number,
- "type": "table" | "reducer",
- "schema"?: ProductType
-}
-```
-
-| Entity field | Value |
-| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `arity` | For tables, the number of colums; for reducers, the number of arguments. |
-| `type` | For tables, `"table"`; for reducers, `"reducer"`. |
-| `schema` | A [JSON-encoded `ProductType`](/docs/satn); for tables, the table schema; for reducers, the argument schema. Only present if `expand` is supplied and true. |
-
-The `"typespace"` will be a JSON array of [`AlgebraicType`s](/docs/satn) referenced by the module. This can be used to resolve `Ref` types within the schema; the type `{ "Ref": n }` refers to `response["typespace"][n]`.
-
-## `/database/schema/:name_or_address/:entity_type/:entity GET`
-
-Get a schema for a particular table or reducer in a database.
-
-Accessible through the CLI as `spacetime describe `.
-
-#### Parameters
-
-| Name | Value |
-| ------------------ | ---------------------------------------------------------------- |
-| `:name_or_address` | The name or address of the database. |
-| `:entity_type` | `reducer` to describe a reducer, or `table` to describe a table. |
-| `:entity` | The name of the reducer or table. |
-
-#### Query Parameters
-
-| Name | Value |
-| -------- | ------------------------------------------------------------- |
-| `expand` | A boolean; whether to include the full schema for the entity. |
-
-#### Returns
-
-Returns a single entity in the same format as in the `"entities"` returned by [the `/database/schema/:name_or_address GET` endpoint](#databaseschemaname_or_address-get):
-
-```typescript
-{
- "arity": number,
- "type": "table" | "reducer",
- "schema"?: ProductType,
-}
-```
-
-| Field | Value |
-| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `arity` | For tables, the number of colums; for reducers, the number of arguments. |
-| `type` | For tables, `"table"`; for reducers, `"reducer"`. |
-| `schema` | A [JSON-encoded `ProductType`](/docs/satn); for tables, the table schema; for reducers, the argument schema. Only present if `expand` is supplied and true. |
-
-## `/database/info/:name_or_address GET`
-
-Get a database's address, owner identity, host type, number of replicas and a hash of its WASM module.
-
-#### Parameters
-
-| Name | Value |
-| ------------------ | ------------------------------------ |
-| `:name_or_address` | The name or address of the database. |
-
-#### Returns
-
-Returns JSON in the form:
-
-```typescript
-{
- "address": string,
- "owner_identity": string,
- "host_type": "wasm",
- "initial_program": string
+ ],
+ "types": [
+ {
+ "name": {
+ "scope": [],
+ "name": "Person"
+ },
+ "ty": 0,
+ "custom_ordering": true
+ }
+ ],
+ "misc_exports": [],
+ "row_level_security": []
}
```
-| Field | Type | Meaning |
-| ------------------- | ------ | ---------------------------------------------------------------- |
-| `"address"` | String | The address of the database. |
-| `"owner_identity"` | String | The Spacetime identity of the database's owner. |
-| `"host_type"` | String | The module host type; currently always `"wasm"`. |
-| `"initial_program"` | String | Hash of the WASM module with which the database was initialized. |
+
-## `/database/logs/:name_or_address GET`
+## `GET /v1/database/:name_or_identity/logs`
Retrieve logs from a database.
-Accessible through the CLI as `spacetime logs `.
-
-#### Parameters
-
-| Name | Value |
-| ------------------ | ------------------------------------ |
-| `:name_or_address` | The name or address of the database. |
+Accessible through the CLI as `spacetime logs `.
#### Query Parameters
@@ -502,31 +411,25 @@ Accessible through the CLI as `spacetime logs `.
#### Required Headers
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
#### Returns
Text, or streaming text if `follow` is supplied, containing log lines.
-## `/database/sql/:name_or_address POST`
+## `POST /v1/database/:name_or_identity/sql`
Run a SQL query against a database.
-Accessible through the CLI as `spacetime sql `.
-
-#### Parameters
-
-| Name | Value |
-| ------------------ | --------------------------------------------- |
-| `:name_or_address` | The name or address of the database to query. |
+Accessible through the CLI as `spacetime sql `.
#### Required Headers
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
+| Name | Value |
+| --------------- | --------------------------------------------------------------------- |
+| `Authorization` | A Spacetime token [as Bearer auth](/docs/http#authorization-headers). |
#### Data
@@ -543,6 +446,6 @@ Returns a JSON array of statement results, each of which takes the form:
}
```
-The `schema` will be a [JSON-encoded `ProductType`](/docs/satn) describing the type of the returned rows.
+The `schema` will be a [JSON-encoded `ProductType`](/docs/sats-json) describing the type of the returned rows.
-The `rows` will be an array of [JSON-encoded `ProductValue`s](/docs/satn), each of which conforms to the `schema`.
+The `rows` will be an array of [JSON-encoded `ProductValue`s](/docs/sats-json), each of which conforms to the `schema`.
diff --git a/docs/http/energy.md b/docs/http/energy.md
deleted file mode 100644
index 6f008314..00000000
--- a/docs/http/energy.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# `/energy` HTTP API
-
-The HTTP endpoints in `/energy` allow clients to query identities' energy balances. Spacetime databases expend energy from their owners' balances while executing reducers.
-
-## At a glance
-
-| Route | Description |
-| ------------------------------------------------ | --------------------------------------------------------- |
-| [`/energy/:identity GET`](#energyidentity-get) | Get the remaining energy balance for the user `identity`. |
-| [`/energy/:identity POST`](#energyidentity-post) | Set the energy balance for the user `identity`. |
-
-## `/energy/:identity GET`
-
-Get the energy balance of an identity.
-
-Accessible through the CLI as `spacetime energy status `.
-
-#### Parameters
-
-| Name | Value |
-| ----------- | ----------------------- |
-| `:identity` | The Spacetime identity. |
-
-#### Returns
-
-Returns JSON in the form:
-
-```typescript
-{
- "balance": string
-}
-```
-
-| Field | Value |
-| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `balance` | The identity's energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. |
-
-## `/energy/:identity POST`
-
-Set the energy balance for an identity.
-
-Note that in the SpacetimeDB 0.6 Testnet, this endpoint always returns code 401, `UNAUTHORIZED`. Testnet energy balances cannot be refilled.
-
-Accessible through the CLI as `spacetime energy set-balance `.
-
-#### Parameters
-
-| Name | Value |
-| ----------- | ----------------------- |
-| `:identity` | The Spacetime identity. |
-
-#### Query Parameters
-
-| Name | Value |
-| --------- | ------------------------------------------ |
-| `balance` | A decimal integer; the new balance to set. |
-
-#### Required Headers
-
-| Name | Value |
-| --------------- | --------------------------------------------------------------- |
-| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
-
-#### Returns
-
-Returns JSON in the form:
-
-```typescript
-{
- "balance": number
-}
-```
-
-| Field | Value |
-| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `balance` | The identity's new energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. |
diff --git a/docs/http/identity.md b/docs/http/identity.md
index 6f1e22c9..3cec4eb9 100644
--- a/docs/http/identity.md
+++ b/docs/http/identity.md
@@ -1,59 +1,23 @@
-# `/identity` HTTP API
+# `/v1/identity` HTTP API
-The HTTP endpoints in `/identity` allow clients to generate and manage Spacetime public identities and private tokens.
+The HTTP endpoints in `/v1/identity` allow clients to generate and manage Spacetime public identities and private tokens.
## At a glance
-| Route | Description |
-| ----------------------------------------------------------------------- | ------------------------------------------------------------------ |
-| [`/identity GET`](#identity-get) | Look up an identity by email. |
-| [`/identity POST`](#identity-post) | Generate a new identity and token. |
-| [`/identity/websocket_token POST`](#identitywebsocket_token-post) | Generate a short-lived access token for use in untrusted contexts. |
-| [`/identity/:identity/set-email POST`](#identityidentityset-email-post) | Set the email for an identity. |
-| [`/identity/:identity/databases GET`](#identityidentitydatabases-get) | List databases owned by an identity. |
-| [`/identity/:identity/verify GET`](#identityidentityverify-get) | Verify an identity and token. |
+| Route | Description |
+| -------------------------------------------------------------------------- | ------------------------------------------------------------------ |
+| [`POST /v1/identity`](#post-v1identity) | Generate a new identity and token. |
+| [`POST /v1/identity/websocket-token`](#post-v1identitywebsocket-token) | Generate a short-lived access token for use in untrusted contexts. |
+| [`GET /v1/identity/public-key`](#get-v1identitypublic-key) | Get the public key used for verifying tokens. |
+| [`GET /v1/identity/:identity/databases`](#get-v1identityidentitydatabases) | List databases owned by an identity. |
+| [`GET /v1/identity/:identity/verify`](#get-v1identityidentityverify) | Verify an identity and token. |
-## `/identity GET`
-
-Look up Spacetime identities associated with an email.
-
-Accessible through the CLI as `spacetime identity find `.
-
-#### Query Parameters
-
-| Name | Value |
-| ------- | ------------------------------- |
-| `email` | An email address to search for. |
-
-#### Returns
-
-Returns JSON in the form:
-
-```typescript
-{
- "identities": [
- {
- "identity": string,
- "email": string
- }
- ]
-}
-```
-
-The `identities` value is an array of zero or more objects, each of which has an `identity` and an `email`. Each `email` will be the same as the email passed as a query parameter.
-
-## `/identity POST`
+## `POST /v1/identity`
Create a new identity.
Accessible through the CLI as `spacetime identity new`.
-#### Query Parameters
-
-| Name | Value |
-| ------- | ----------------------------------------------------------------------------------------------------------------------- |
-| `email` | An email address to associate with the new identity. If unsupplied, the new identity will not have an associated email. |
-
#### Returns
Returns JSON in the form:
@@ -65,7 +29,7 @@ Returns JSON in the form:
}
```
-## `/identity/websocket_token POST`
+## `POST /v1/identity/websocket-token`
Generate a short-lived access token which can be used in untrusted contexts, e.g. embedded in URLs.
@@ -87,7 +51,15 @@ Returns JSON in the form:
The `token` value is a short-lived [JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519).
-## `/identity/:identity/set-email POST`
+## `GET /v1/identity/public-key`
+
+Fetches the public key used by the database to verify tokens.
+
+#### Returns
+
+Returns a response of content-type `application/pem-certificate-chain`.
+
+## `POST /v1/identity/:identity/set-email`
Associate an email with a Spacetime identity.
@@ -111,7 +83,7 @@ Accessible through the CLI as `spacetime identity set-email `.
| --------------- | --------------------------------------------------------------- |
| `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). |
-## `/identity/:identity/databases GET`
+## `GET /v1/identity/:identity/databases`
List all databases owned by an identity.
@@ -133,7 +105,7 @@ Returns JSON in the form:
The `addresses` value is an array of zero or more strings, each of which is the address of a database owned by the identity passed as a parameter.
-## `/identity/:identity/verify GET`
+## `GET /v1/identity/:identity/verify`
Verify the validity of an identity/token pair.
diff --git a/docs/http/index.md b/docs/http/index.md
index a59408e4..4f0973dc 100644
--- a/docs/http/index.md
+++ b/docs/http/index.md
@@ -1,42 +1,23 @@
# SpacetimeDB HTTP Authorization
-Rather than a password, each Spacetime identity is associated with a private token. These tokens are generated by SpacetimeDB when the corresponding identity is created, and cannot be changed.
-
-> Do not share your SpacetimeDB token with anyone, ever.
-
### Generating identities and tokens
-Clients can request a new identity and token via [the `/identity POST` HTTP endpoint](/docs/http/identity#identity-post).
-
-Alternately, a new identity and token will be generated during an anonymous connection via the [WebSocket API](/docs/ws), and passed to the client as [an `IdentityToken` message](/docs/ws#identitytoken).
+SpacetimeDB can derive an identity from the `sub` and `iss` claims of any [OpenID Connect](https://openid.net/developers/how-connect-works/) compliant [JSON Web Token](https://jwt.io/).
-### Encoding `Authorization` headers
+Clients can request a new identity and token signed by the SpacetimeDB host via [the `POST /v1/identity` HTTP endpoint](/docs/http/identity#post-v1identity). Such a token will not be portable to other SpacetimeDB clusters.
-Many SpacetimeDB HTTP endpoints either require or optionally accept a token in the `Authorization` header. SpacetimeDB authorization headers use `Basic` authorization with the username `token` and the token as the password. Because Spacetime tokens are not passwords, and SpacetimeDB Cloud uses TLS, usual security concerns about HTTP `Basic` authorization do not apply.
+Alternately, a new identity and token will be generated during an anonymous connection via the WebSocket API, and passed to the client as an `IdentityToken` message.
-To construct an appropriate `Authorization` header value for a `token`:
+### `Authorization` headers
-1. Prepend the string `token:`.
-2. Base64-encode.
-3. Prepend the string `Basic `.
+Many SpacetimeDB HTTP endpoints either require or optionally accept a token in the `Authorization` header. SpacetimeDB authorization headers are of the form `Authorization: Bearer ${token}`, where `token` is an [OpenID Connect](https://openid.net/developers/how-connect-works/) compliant [JSON Web Token](https://jwt.io/), such as the one returned from [the `POST /v1/identity` HTTP endpoint](/docs/http/identity#post-v1identity).
-#### Rust
+# Top level routes
-```rust
-fn auth_header_value(token: &str) -> String {
- let username_and_password = format!("token:{}", token);
- let base64_encoded = base64::prelude::BASE64_STANDARD.encode(username_and_password);
- format!("Basic {}", encoded)
-}
-```
+| Route | Description |
+| ----------------------------- | ------------------------------------------------------ |
+| [`GET /v1/ping`](#get-v1ping) | No-op. Used to determine whether a client can connect. |
-#### C#
+## `GET /v1/ping`
-```csharp
-public string AuthHeaderValue(string token)
-{
- var username_and_password = Encoding.UTF8.GetBytes($"token:{auth}");
- var base64_encoded = Convert.ToBase64String(username_and_password);
- return "Basic " + base64_encoded;
-}
-```
+Does nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB.
diff --git a/docs/index.md b/docs/index.md
index 974b543f..6e4a0b65 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -46,7 +46,7 @@ You write SQL queries specifying what information a client is interested in -- f
### Module Libraries
-Every SpacetimeDB database contains a collection of stored procedures called a **module**. Modules can be written in C# or Rust. They specify a database schema and the business logic that responds to client requests. Modules are administered using the `spacetime` CLI tool.
+Every SpacetimeDB database contains a collection of [stored procedures](https://en.wikipedia.org/wiki/Stored_procedure) and schema definitions. Such a collection is called a **module**, which can be written in C# or Rust. They specify a database schema and the business logic that responds to client requests. Modules are administered using the `spacetime` CLI tool.
- [Rust](/docs/modules/rust) - [(Quickstart)](/docs/modules/rust/quickstart)
- [C#](/docs/modules/c-sharp) - [(Quickstart)](/docs/modules/c-sharp/quickstart)
@@ -111,6 +111,27 @@ Tables marked `public` can also be read by [clients](#client).
A **reducer** is a function exported by a [database](#database).
Connected [clients](#client-side-sdks) can call reducers to interact with the database.
This is a form of [remote procedure call](https://en.wikipedia.org/wiki/Remote_procedure_call).
+
+:::server-rust
+A reducer can be written in Rust like so:
+
+```rust
+#[spacetimedb::reducer]
+pub fn set_player_name(ctx: &spacetimedb::ReducerContext, id: u64, name: String) -> Result<(), String> {
+ // ...
+}
+```
+
+And a Rust [client](#client) can call that reducer:
+
+```rust
+fn main() {
+ // ...setup code, then...
+ ctx.reducers.set_player_name(57, "Marceline".into());
+}
+```
+:::
+:::server-csharp
A reducer can be written in C# like so:
```csharp
@@ -120,14 +141,6 @@ public static void SetPlayerName(ReducerContext ctx, uint playerId, string name)
// ...
}
```
-
And a C# [client](#client) can call that reducer:
@@ -137,13 +150,78 @@ void Main() {
Connection.Reducer.SetPlayerName(57, "Marceline");
}
```
+:::
+
+These look mostly like regular function calls, but under the hood,
+the client sends a request over the internet, which the database processes and responds to.
+
+The `ReducerContext` is a reducer's only mandatory parameter
+and includes information about the caller's [identity](#identity).
+This can be used to authenticate the caller.
+
+Reducers are run in their own separate and atomic [database transactions](https://en.wikipedia.org/wiki/Database_transaction).
+When a reducer completes successfully, the changes the reducer has made,
+such as inserting a table row, are *committed* to the database.
+However, if the reducer instead returns an error, or throws an exception,
+the database will instead reject the request and *revert* all those changes.
+That is, reducers and transactions are all-or-nothing requests.
+It's not possible to keep the first half of a reducer's changes and discard the last.
+
+Transactions are only started by requests from outside the database.
+When a reducer calls another reducer directly, as in the example below,
+the changes in the called reducer does not happen in its own child transaction.
+Instead, when the nested reducer gracefully errors,
+and the overall reducer completes successfully,
+the changes in the nested one are still persisted.
+
+:::server-rust
+```rust
+#[spacetimedb::reducer]
+pub fn hello(ctx: &spacetimedb::ReducerContext) -> Result<(), String> {
+ if world(ctx).is_err() {
+ other_changes(ctx);
+ }
+}
-These look mostly like regular function calls, but under the hood, the client sends a request over the internet, which the database processes and responds to.
+#[spacetimedb::reducer]
+pub fn world(ctx: &spacetimedb::ReducerContext) -> Result<(), String> {
+ clear_all_tables(ctx);
+}
+```
+:::
+:::server-csharp
+```csharp
+[SpacetimeDB.Reducer]
+public static void Hello(ReducerContext ctx)
+{
+ if(!World(ctx))
+ {
+ OtherChanges(ctx);
+ }
+}
-The `ReducerContext` passed into a reducer includes information about the caller's [identity](#identity) and [address](#address). The database can reject any request it doesn't approve of.
+[SpacetimeDB.Reducer]
+public static void World(ReducerContext ctx)
+{
+ ClearAllTables(ctx);
+ // ...
+}
+```
+:::
+
+:::server-rust
+While SpacetimeDB doesn't support nested transactions,
+a reducer can [schedule another reducer](https://docs.rs/spacetimedb/latest/spacetimedb/attr.reducer.html#scheduled-reducers) to run at an interval,
+or at a specific time.
+:::
+:::server-csharp
+While SpacetimeDB doesn't support nested transactions,
+a reducer can [schedule another reducer](/docs/modules/c-sharp#scheduled-reducers) to run at an interval,
+or at a specific time.
+:::
### Client
-A **client** is an application that connects to a [database](#database). A client logs in using an [identity](#identity) and receives an [address](#address) to identify the connection. After that, it can call [reducers](#reducer) and query public [tables](#table).
+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).
Clients are written using the [client-side SDKs](#client-side-sdks). The `spacetime` CLI tool allows automatically generating code that works with the client-side SDKs to talk to a particular database.
@@ -159,15 +237,33 @@ Modules themselves also have Identities. When you `spacetime publish` a module,
Identities are issued using the [OpenID Connect](https://openid.net/developers/how-connect-works/) specification. Database developers are responsible for issuing Identities to their end users. OpenID Connect lets users log in to these accounts through standard services like Google and Facebook.
-
+Specifically, an identity is derived from the issuer and subject fields of a [JSON Web Token (JWT)](https://jwt.io/) hashed together. The psuedocode for this is as follows:
+
+```python
+def identity_from_claims(issuer: str, subject: str) -> [u8; 32]:
+ hash1: [u8; 32] = blake3_hash(issuer + "|" + subject)
+ id_hash: [u8; 26] = hash1[:26]
+ checksum_hash: [u8; 32] = blake3_hash([
+ 0xC2,
+ 0x00,
+ *id_hash
+ ])
+ identity_big_endian_bytes: [u8; 32] = [
+ 0xC2,
+ 0x00,
+ *checksum_hash[:4],
+ *id_hash
+ ]
+ return identity_big_endian_bytes
+```
-### Address
+
-
+### ConnectionId
-An `Address` identifies client connections to a SpacetimeDB module.
+A `ConnectionId` identifies client connections to a SpacetimeDB module.
-A user has a single [`Identity`](#identity), but may open multiple connections to your module. Each of these will receive a unique `Address`.
+A user has a single [`Identity`](#identity), but may open multiple connections to your module. Each of these will receive a unique `ConnectionId`.
### Energy
**Energy** is the currency used to pay for data storage and compute operations in a SpacetimeDB host.
diff --git a/docs/migration/v0.12.md b/docs/migration/v0.12.md
deleted file mode 100644
index 9384407f..00000000
--- a/docs/migration/v0.12.md
+++ /dev/null
@@ -1,341 +0,0 @@
-# Updating your app for SpacetimeDB v0.12
-
-We're excited to release SpacetimeDB v0.12, which includes a major overhaul of our Rust, C# and TypeScript APIs for both modules and clients. In no particular order, our goals with this rewrite were:
-
-- Our APIs should be as similar as possible in all three languages we support, and in clients and modules, so that you don't have to go to a ton of work figuring out why something works in one place but not somewhere else.
-- We should be very explicit about what operations interact with the database and how. In addition to good hygiene, this means that a client can now connect to multiple remote modules at the same time without getting confused. (Some day a module will be able to connect to remote modules too, but we're not there yet.)
-- Our APIs should expose low level database operations so you can program your applications to have predictable performance characteristics. An indexed lookup should look different in your code from a full scan, and writing the indexed lookup should be easier. This will help you write your apps as efficiently as possible as we add features to SpacetimeDB. (In the future, as we get more sophisticated at optimizing and evaluating queries, we will offer a higher level logical query API which let's us implement very high performance optimizations and abstract away concerns like indices.)
-
-The new APIs are a significant improvement to the developer experience of SpacetimeDB and enable some amazing features in the future. They're completely new APIs, so if you run into any trouble, please [ask us for help or share your feedback on Discord!](https://discord.gg/spacetimedb)
-
-To start migrating, update your SpacetimeDB CLI, and bump the `spacetimedb` and `spacetimedb-sdk` dependency versions to 0.12 in your module and client respectively.
-
-## Modules
-
-### The reducer context
-
-All your reducers must now accept a reducer context as their first argument. In Rust, this is now taken by reference, as `&ReducerContext`. All access to tables now go through methods on the `db` or `Db` field of the `ReducerContext`.
-
-```rust
-#[spacetimedb::reducer]
-fn my_reducer(ctx: &ReducerContext) {
- for row in ctx.db.my_table().iter() {
- // Do something with the row...
- }
-}
-```
-
-```csharp
-[SpacetimeDB.Reducer]
-public static void MyReducer(ReducerContext ctx) {
- foreach (var row in ctx.Db.MyTable.Iter()) {
- // Do something with the row...
- }
-}
-```
-
-### Table names and access methods
-
-You now must specify a name for every table, distinct from the type name. In Rust, write this as `#[spacetimedb::table(name = my_table)]`. The name you specify here will be the method on `ctx.db` you use to access the table.
-
-```rust
-#[spacetimedb::table(name = my_table)]
-struct MyTable {
- #[primary_key]
- #[auto_inc]
- id: u64,
- other_column: u32,
-}
-```
-
-```csharp
-[SpacetimeDB.Table(Name = "MyTable")]
-public partial struct MyTable
-{
- [SpacetimeDB.PrimaryKey]
- [SpacetimeDB.AutoInc]
- public long Id;
- public int OtherColumn;
-}
-```
-
-One neat upside of this is that you can now have multiple tables with the same row type!
-
-```rust
-#[spacetimedb::table(name = signed_in_user)]
-#[spacetimedb::table(name = signed_out_user)]
-struct User {
- #[primary_key]
- id: Identity,
- #[unique]
- username: String,
-}
-```
-
-```csharp
-[SpacetimeDB.Table(Name = "SignedInUser")]
-[SpacetimeDB.Table(Name = "SignedOutUser")]
-public partial struct User
-{
- [SpacetimeDB.PrimaryKey]
- public SpacetimeDB.Identity Id;
- [SpacetimeDB.Unique]
- public String Username;
-}
-```
-
-### Iterating, counting, inserting, deleting
-
-Each "table handle" `ctx.db.my_table()` has methods:
-
-| Rust name | C# name | Behavior |
-|-----------|----------|-----------------------------------------|
-| `iter` | `Iter` | Iterate over all rows in the table. |
-| `count` | `Count` | Return the number of rows in the table. |
-| `insert` | `Insert` | Add a new row to the table. |
-| `delete` | `Delete` | Delete a given row from the table. |
-
-### Index access
-
-Each table handle also has a method for each BTree index and/or unique constraint on the table, which allows you to filter, delete or update by that index. BTree indices' filter and delete methods accept both point and range queries.
-
-```rust
-#[spacetimedb::table(
- name = entity,
- index(name = location, btree = [x, y]),
-)]
-struct Entity {
- #[primary_key]
- #[auto_inc]
- id: u64,
- x: u32,
- y: u32,
- #[index(btree)]
- faction: String,
-}
-
-#[spacetimedb::reducer]
-fn move_entity(ctx: &ReducerContext, entity_id: u64, x: u32, y: u32) {
- let entity = ctx.db.entity().id().find(entity_id).expect("No such entity");
- ctx.db.entity.id().update(Entity { x, y, ..entity });
-}
-
-#[spacetimedb::reducer]
-fn log_entities_at_point(ctx: &ReducerContext, x: u32, y: u32) {
- for entity in ctx.db.entity().location().filter((x, y)) {
- log::info!("Entity {} is at ({}, {})", entity.id, x, y);
- }
-}
-
-#[spacetimedb::reducer]
-fn delete_faction(ctx: &ReducerContext, faction: String) {
- ctx.db.entity().faction().delete(&faction);
-}
-```
-
-```csharp
-[SpacetimeDB.Table(Name = "Entity")]
-[SpacetimeDB.Table(Name = "SignedOutUser")]
-[SpacetimeDB.Index(Name = "Location", BTree = ["X", "Y"])]
-[SpacetimeDB.Index(Name = "Faction", BTree = ["Faction"])]
-public partial struct Entity
-{
- [SpacetimeDB.PrimaryKey]
- [SpacetimeDB.AutoInc]
- public long Id;
- public int X;
- public int Y;
- public string Faction;
-}
-
-[SpacetimeDB.Reducer]
-public static void MoveEntity(SpacetimeDB.ReducerContext ctx, long entityId, int x, int y) {
- var entity = ctx.Db.Entity.Id.Find(entityId);
- ctx.Db.Entity.Id.Update(new Entity {
- Id = entityId,
- X = x,
- Y = y,
- Faction = entity.Faction,
- });
-}
-
-[SpacetimeDB.Reducer]
-public static void LogEntitiesAtPoint(SpacetimeDB.ReducerContext ctx, int x, int y) {
- foreach(var entity in ctx.Db.Entity.Location.Filter((x, y))) {
- SpacetimeDB.Log.Info($"Entity {entity.Id} is at ({x}, {y})");
- }
-}
-
-[SpacetimeDB.Reducer]
-public static void DeleteFaction(SpacetimeDB.ReducerContext ctx, string Faction) {
- ctx.Db.Entity.Faction.Delete(Faction);
-}
-```
-
-### `query`
-
-Note that the `query!` macro in Rust and the `.Query()` method in C# have been removed. We plan to replace them with something even better in the future, but for now, you should write your query explicitly, either by accessing an index or multi-column index by chaining `ctx.db.my_table().iter().filter(|row| predicate)`.
-
-### Built-in reducers
-
-The Rust syntax for declaring builtin lifecycles have changed. They are now:
-
-- `#[spacetimedb::reducer(client_connected)]`
-- `#[spacetimedb::reducer(client_disconnected)]`
-- `#[spacetimedb::reducer(init)]`
-
-In C# they are now:
-
-- `[SpacetimeDB.Reducer(SpacetimeDB.ReducerKind.ClientConnected)]`
-- `[SpacetimeDB.Reducer(SpacetimeDB.ReducerKind.ClientDisconnected)]`
-- `[SpacetimeDB.Reducer(SpacetimeDB.ReducerKind.Init)]`
-
-## Clients
-
-Make sure to run `spacetime generate` after updating your module!
-
-### The connection object
-
-Your connection to a remote module is now represented by a `DbConnection` object, which holds all state associated with the connection. We encourage you to name the variable that holds your connection `ctx`.
-
-Construct a `DbConnection` via the [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) with `DbConnection::builder()` or your language's equivalent. Register on-connect and on-disconnect callbacks while constructing the connection via the builder.
-
-> NOTE: The APIs for the the `DbConnection` and `ReducerContext` are quite similar, allowing you to write the same patterns on both the client and server.
-
-### Polling the `DbConnection`
-
-In Rust, you now must explicitly poll your `DbConnection` to advance, where previously it ran automatically in the background. This provides a much greater degree of flexibility to choose your own async runtime and to work under the variety of exciting constraints imposed by game development - for example, you can now arrange it so that all your callbacks run on the main thread if you want to make GUI calls. You can recreate the previous behavior by calling `ctx.run_threaded()` immediately after buidling your connection. You can also call `ctx.run_async()`, or manually call `ctx.frame_tick()` at an appropriate interval.
-
-In C# the existing API already required you explictly poll your `DbConnection`, so not much has changed there. The `Update()` method is now called `FrameTick()`.
-
-### Subscribing to queries
-
-We're planning a major overhaul of the API for subscribing to queries, but we're not quite there yet. This means that our subscription APIs are not yet as consistent as will soon be.
-
-#### Rust
-
-Subscribe to a set of queries by creating a subscription builder and calling `subscribe`.
-
-```rust
-ctx.subscription_builder()
- .on_applied(|ctx| { ... })
- .subscribe([
- "SELECT * FROM my_table",
- "SELECT * FROM other_table WHERE some_column = 123"
- ]);
-```
-
-The `on_applied` callback is optional. A temporarily limitation of this API is that you should add all your subscription queries at one time for any given connection.
-
-#### C#
-
-```csharp
-ctx.SubscriptionBuilder()
- .OnApplied(ctx => { ... })
- .Subscribe(
- "SELECT * FROM MyTable",
- "SELECT * FROM OtherTable WHERE SomeColumn = 123"
- );
-```
-
-#### TypeScript
-
-```ts
-ctx.subscriptionBuilder()
- .onApplied(ctx => { ... })
- .subscribe([
- "SELECT * FROM my_table",
- "SELECT * FROM other_table WHERE some_column = 123"
- ]);
-```
-
-### Accessing tables
-
-As in modules, all accesses to your connection's client cache now go through the `ctx.db`. Support for client-side indices is not yet consistent across all our SDKs, so for now you may find that you can't make some queries in clients which you could make in modules. The table handles also expose row callbacks.
-
-### Observing and invoking reducers
-
-Register reducer callbacks and request reducer invocations by going through `ctx.reducers`. You can also add functions to subscribe to reducer events that the server sends when a particular reducer is executed.
-
-#### Rust
-
-```rust
-ctx.reducers.my_reducer(my_first_arg, my_second_arg, ...);
-
-// Add a callback for each reducer event for `my_reducer`
-let callback_id = ctx.reducers.on_my_reducer(|ctx, first_arg, second_arg, ...| {
- ...
-});
-
-// Unregister the callback
-ctx.reducers.remove_my_reducer(callback_id);
-```
-
-#### C#
-
-```cs
-ctx.Reducers.MyReducer(myFirstArg, mySecondArg, ...);
-
-// Add a callback for each reducer event for `MyReducer`
-void OnMyReducerCallback(EventContext ctx) {
- ...
-}
-ctx.Reducers.OnMyReducer += OnMyReducerCallback;
-
-// Unregister the callback
-ctx.Reducers.OnMyReducer -= OnMyReducerCallback;
-```
-
-#### TypeScript
-
-```ts
-ctx.reducers.myReducer(myFirstArg, mySecondArg, ...);
-
-// Add a callback for each reducer event for `my_reducer`
-const callback = (ctx, firstArg, secondArg, ...) => {
- ...
-};
-ctx.reducers.onMyReducer(callback);
-
-// Unregister the callback
-ctx.reducers.removeMyReducer(callback);
-```
-
-### The event context
-
-Most callbacks now take a first argument of type `&EventContext`. This is just like your `DbConnection`, but it has an additional field `event: Event`. `Event` is an enum, tagged union, or sum type which encodes all the different events the SDK can observe. This fills the same role as `ReducerEvent` used to, but `Event` is more specific and more accurate to what actually happened.
-
-```rust
-ctx.reducers.on_my_reducer(|ctx, first_arg, second_arg, ...| {
- match ctx.event {
- Reducer(reducer_event) => {
- ...
- },
- _ => unreachable!();
- }
-});
-```
-
-#### C#
-
-```csharp
-ctx.Reducers.OnMyReducer += (ctx, firstArg, secondArg, ...) => {
- switch (ctx.Event) {
- case Event.Reducer (var value):
- var reducerEvent = value.Reducer;
- ...
- break;
- }
-};
-```
-
-#### TypeScript
-
-```ts
-ctx.reducers.onMyReducer((ctx, firstArg, secondArg, ...) => {
- if (ctx.event.tag === 'Reducer') {
- const reducerEvent = ctx.event.value;
- ...
- }
-});
-```
diff --git a/docs/modules/c-sharp/index.md b/docs/modules/c-sharp/index.md
index 2c31bb1c..fc2acc95 100644
--- a/docs/modules/c-sharp/index.md
+++ b/docs/modules/c-sharp/index.md
@@ -1,414 +1,1394 @@
-# SpacetimeDB C# Modules
+# SpacetimeDB C# Module Library
+
+[SpacetimeDB](https://spacetimedb.com/) allows using the C# language to write server-side applications called **modules**. Modules, which run inside a relational database, have direct access to database tables, and expose public functions called **reducers** that can be invoked over the network. Clients connect directly to the database to read data.
+
+```text
+ Client Application SpacetimeDB
+┌───────────────────────┐ ┌───────────────────────┐
+│ │ │ │
+│ ┌─────────────────┐ │ SQL Query │ ┌─────────────────┐ │
+│ │ Subscribed Data │<─────────────────────│ Database │ │
+│ └─────────────────┘ │ │ └─────────────────┘ │
+│ │ │ │ ^ │
+│ │ │ │ │ │
+│ v │ │ v │
+│ +─────────────────┐ │ call_reducer() │ ┌─────────────────┐ │
+│ │ Client Code │─────────────────────>│ Module Code │ │
+│ └─────────────────┘ │ │ └─────────────────┘ │
+│ │ │ │
+└───────────────────────┘ └───────────────────────┘
+```
-You can use the [C# SpacetimeDB library](https://github.com/clockworklabs/SpacetimeDBLibCSharp) to write modules in C# which interact with the SpacetimeDB database.
+C# modules are written with the the C# Module Library (this package). They are built using the [dotnet CLI tool](https://learn.microsoft.com/en-us/dotnet/core/tools/) and deployed using the [`spacetime` CLI tool](https://spacetimedb.com/install). C# modules can import any [NuGet package](https://www.nuget.org/packages) that supports being compiled to WebAssembly.
-It uses [Roslyn incremental generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) to add extra static methods to types, tables and reducers marked with special attributes and registers them with the database runtime.
+(Note: C# can also be used to write **clients** of SpacetimeDB databases, but this requires using a different library, the SpacetimeDB C# Client SDK. See the documentation on [clients] for more information.)
-## Example
+This reference assumes you are familiar with the basics of C#. If you aren't, check out the [C# language documentation](https://learn.microsoft.com/en-us/dotnet/csharp/). For a guided introduction to C# Modules, see the [C# Module Quickstart](https://spacetimedb.com/docs/modules/c-sharp/quickstart).
-Let's start with a heavily commented version of the default example from the landing page:
+# Overview
-```csharp
-// These imports bring into the scope common APIs you'll need to expose items from your module and to interact with the database runtime.
-using SpacetimeDB.Module;
-using static SpacetimeDB.Runtime;
+SpacetimeDB modules have two ways to interact with the outside world: tables and reducers.
+
+- [Tables](#tables) store data and optionally make it readable by [clients].
+
+- [Reducers](#reducers) are functions that modify data and can be invoked by [clients] over the network. They can read and write data in tables, and write to a private debug log.
-// Roslyn generators are statically generating extra code as-if they were part of the source tree, so,
-// in order to inject new methods, types they operate on as well as their parents have to be marked as `partial`.
-//
-// We start with the top-level `Module` class for the module itself.
+These are the only ways for a SpacetimeDB module to interact with the outside world. Calling functions from `System.IO` or `System.Net` inside a reducer will result in runtime errors.
+
+Declaring tables and reducers is straightforward:
+
+```csharp
static partial class Module
{
- // `[SpacetimeDB.Table]` registers a struct or a class as a SpacetimeDB table.
- //
- // It generates methods to insert, filter, update, and delete rows of the given type in the table.
- [SpacetimeDB.Table(Public = true)]
+ [SpacetimeDB.Table(Name = "player")]
+ public partial struct Player
+ {
+ public int Id;
+ public string Name;
+ }
+
+ [SpacetimeDB.Reducer]
+ public static void AddPerson(ReducerContext ctx, int Id, string Name) {
+ ctx.Db.player.Insert(new Player { Id = Id, Name = Name });
+ }
+}
+```
+
+
+Note that reducers don't return data directly; they can only modify the database. Clients connect directly to the database and use SQL to query [public](#public-and-private-tables) tables. Clients can also subscribe to a set of rows using SQL queries and receive streaming updates whenever any of those rows change.
+
+Tables and reducers in C# modules can use any type annotated with [`[SpacetimeDB.Type]`](#attribute-spacetimedbtype).
+
+
+
+# Setup
+
+To create a C# module, install the [`spacetime` CLI tool](https://spacetimedb.com/install) in your preferred shell. Navigate to your work directory and run the following command:
+
+```bash
+spacetime init --lang csharp my-project-directory
+```
+
+This creates a `dotnet` project in `my-project-directory` with the following `StdbModule.csproj`:
+
+```xml
+
+
+
+ net8.0
+ wasi-wasm
+ enable
+ enable
+
+
+
+
+
+
+
+```
+
+This is a standard `csproj`, with the exception of the line `wasi-wasm`.
+This line is important: it allows the project to be compiled to a WebAssembly module.
+
+The project's `Lib.cs` will contain the following skeleton:
+
+```csharp
+public static partial class Module
+{
+ [SpacetimeDB.Table]
public partial struct Person
{
- // `[SpacetimeDB.Column]` allows to specify column attributes / constraints such as
- // "this field should be unique" or "this field should get automatically assigned auto-incremented value".
- [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)]
+ [SpacetimeDB.AutoInc]
+ [SpacetimeDB.PrimaryKey]
public int Id;
public string Name;
public int Age;
}
- // `[SpacetimeDB.Reducer]` marks a static method as a SpacetimeDB reducer.
- //
- // Reducers are functions that can be invoked from the database runtime.
- // They can't return values, but can throw errors that will be caught and reported back to the runtime.
[SpacetimeDB.Reducer]
- public static void Add(string name, int age)
+ public static void Add(ReducerContext ctx, string name, int age)
{
- // We can skip (or explicitly set to zero) auto-incremented fields when creating new rows.
- var person = new Person { Name = name, Age = age };
-
- // `Insert()` method is auto-generated and will insert the given row into the table.
- person.Insert();
- // After insertion, the auto-incremented fields will be populated with their actual values.
- //
- // `Log()` function is provided by the runtime and will print the message to the database log.
- // It should be used instead of `Console.WriteLine()` or similar functions.
- Log($"Inserted {person.Name} under #{person.Id}");
+ var person = ctx.Db.Person.Insert(new Person { Name = name, Age = age });
+ Log.Info($"Inserted {person.Name} under #{person.Id}");
}
[SpacetimeDB.Reducer]
- public static void SayHello()
+ public static void SayHello(ReducerContext ctx)
{
- // Each table type gets a static Iter() method that can be used to iterate over the entire table.
- foreach (var person in Person.Iter())
+ foreach (var person in ctx.Db.Person.Iter())
{
- Log($"Hello, {person.Name}!");
+ Log.Info($"Hello, {person.Name}!");
}
- Log("Hello, World!");
+ Log.Info("Hello, World!");
}
}
```
-## API reference
+This skeleton declares a [table](#tables) and some [reducers](#reducers).
+
+You can also add some [lifecycle reducers](#lifecycle-reducers) to the `Module` class using the following code:
+
+```csharp
+[Reducer(ReducerKind.Init)]
+public static void Init(ReducerContext ctx)
+{
+ // Run when the module is first loaded.
+}
+
+[Reducer(ReducerKind.ClientConnected)]
+public static void ClientConnected(ReducerContext ctx)
+{
+ // Called when a client connects.
+}
+
+[Reducer(ReducerKind.ClientDisconnected)]
+public static void ClientDisconnected(ReducerContext ctx)
+{
+ // Called when a client connects.
+}
+```
+
+
+To compile the project, run the following command:
+
+```bash
+spacetime build
+```
+
+SpacetimeDB requires a WebAssembly-compatible `dotnet` toolchain. If the `spacetime` cli finds a compatible version of [`dotnet`](https://rustup.rs/) that it can run, it will automatically install the `wasi-experimental` workload and use it to build your application. This can also be done manually using the command:
-Now we'll get into details on all the APIs SpacetimeDB provides for writing modules in C#.
+```bash
+dotnet workload install wasi-experimental
+```
-### Logging
+If you are managing your dotnet installation in some other way, you will need to install the `wasi-experimental` workload yourself.
-First of all, logging as we're likely going to use it a lot for debugging and reporting errors.
+To build your application and upload it to the public SpacetimeDB network, run:
-`SpacetimeDB.Runtime` provides a `Log` function that will print the given message to the database log, along with the source location and a log level it was provided.
+```bash
+spacetime login
+```
+
+And then:
+
+```bash
+spacetime publish [MY_DATABASE_NAME]
+```
+
+For example:
+
+```bash
+spacetime publish silly_demo_app
+```
-Supported log levels are provided by the `LogLevel` enum:
+When you publish your module, a database named `silly_demo_app` will be created with the requested tables, and the module will be installed inside it.
+
+The output of `spacetime publish` will end with a line:
+```text
+Created new database with name: , identity:
+```
+
+This name is the human-readable name of the created database, and the hex string is its [`Identity`](#struct-identity). These distinguish the created database from the other databases running on the SpacetimeDB network. They are used when administering the application, for example using the [`spacetime logs `](#class-log) command. You should probably write the database name down in a text file so that you can remember it.
+
+After modifying your project, you can run:
+
+`spacetime publish `
+
+to update the module attached to your database. Note that SpacetimeDB tries to [automatically migrate](#automatic-migrations) your database schema whenever you run `spacetime publish`.
+
+You can also generate code for clients of your module using the `spacetime generate` command. See the [client SDK documentation] for more information.
+
+# How it works
+
+Under the hood, SpacetimeDB modules are WebAssembly modules that import a [specific WebAssembly ABI](https://spacetimedb.com/docs/webassembly-abi) and export a small number of special functions. This is automatically configured when you add the `SpacetimeDB.Runtime` package as a dependency of your application.
+
+The SpacetimeDB host is an application that hosts SpacetimeDB databases. [Its source code is available](https://github.com/clockworklabs/SpacetimeDB) under [the Business Source License with an Additional Use Grant](https://github.com/clockworklabs/SpacetimeDB/blob/master/LICENSE.txt). You can run your own host, or you can upload your module to the public SpacetimeDB network. The network will create a database for you and install your module in it to serve client requests.
+
+## In More Detail: Publishing a Module
+
+The `spacetime publish [DATABASE_IDENTITY]` command compiles a module and uploads it to a SpacetimeDB host. After this:
+- The host finds the database with the requested `DATABASE_IDENTITY`.
+ - (Or creates a fresh database and identity, if no identity was provided).
+- The host loads the new module and inspects its requested database schema. If there are changes to the schema, the host tries perform an [automatic migration](#automatic-migrations). If the migration fails, publishing fails.
+- The host terminates the old module attached to the database.
+- The host installs the new module into the database. It begins running the module's [lifecycle reducers](#lifecycle-reducers) and [scheduled reducers](#scheduled-reducers), starting with the `Init` reducer.
+- The host begins allowing clients to call the module's reducers.
+
+From the perspective of clients, this process is seamless. Open connections are maintained and subscriptions continue functioning. [Automatic migrations](#automatic-migrations) forbid most table changes except for adding new tables, so client code does not need to be recompiled.
+However:
+- Clients may witness a brief interruption in the execution of scheduled reducers (for example, game loops.)
+- New versions of a module may remove or change reducers that were previously present. Client code calling those reducers will receive runtime errors.
+
+
+# Tables
+
+Tables are declared using the [`[SpacetimeDB.Table]` attribute](#table-attribute).
+
+This macro is applied to a C# `partial class` or `partial struct` with named fields. (The `partial` modifier is required to allow code generation to add methods.) All of the fields of the table must be marked with [`[SpacetimeDB.Type]`](#type-attribute).
+
+The resulting type is used to store rows of the table. It's a normal class (or struct). Row values are not special -- operations on row types do not, by themselves, modify the table. Instead, a [`ReducerContext`](#class-reducercontext) is needed to get a handle to the table.
+
+```csharp
+public static partial class Module {
+
+ ///
+ /// A Person is a row of the table person.
+ ///
+ [SpacetimeDB.Table(Name = "person", Public)]
+ public partial struct Person {
+ [SpacetimeDB.PrimaryKey]
+ [SpacetimeDB.AutoInc]
+ ulong Id;
+ [SpacetimeDB.Index.BTree]
+ string Name;
+ }
+
+ // `Person` is a normal C# struct type.
+ // Operations on a `Person` do not, by themselves, do anything.
+ // The following function does not interact with the database at all.
+ public static void DoNothing() {
+ // Creating a `Person` DOES NOT modify the database.
+ var person = new Person { Id = 0, Name = "Joe Average" };
+ // Updating a `Person` DOES NOT modify the database.
+ person.Name = "Joanna Average";
+ // Deallocating a `Person` DOES NOT modify the database.
+ person = null;
+ }
+
+ // To interact with the database, you need a `ReducerContext`,
+ // which is provided as the first parameter of any reducer.
+ [SpacetimeDB.Reducer]
+ public static void DoSomething(ReducerContext ctx) {
+ // The following inserts a row into the table:
+ var examplePerson = ctx.Db.person.Insert(new Person { id = 0, name = "Joe Average" });
+
+ // `examplePerson` is a COPY of the row stored in the database.
+ // If we update it:
+ examplePerson.name = "Joanna Average".to_string();
+ // Our copy is now updated, but the database's copy is UNCHANGED.
+ // To push our change through, we can call `UniqueIndex.Update()`:
+ examplePerson = ctx.Db.person.Id.Update(examplePerson);
+ // Now the database and our copy are in sync again.
+
+ // We can also delete the row in the database using `UniqueIndex.Delete()`.
+ ctx.Db.person.Id.Delete(examplePerson.Id);
+ }
+}
+```
+
+(See [reducers](#reducers) for more information on declaring reducers.)
+
+This library generates a custom API for each table, depending on the table's name and structure.
+
+All tables support getting a handle implementing the [`ITableView`](#interface-itableview) interface from a [`ReducerContext`](#class-reducercontext), using:
+
+```text
+ctx.Db.{table_name}
+```
+
+For example,
```csharp
-public enum LogLevel
+ctx.Db.person
+```
+
+[Unique and primary key columns](#unique-and-primary-key-columns) and [indexes](#indexes) generate additional accessors, such as `ctx.Db.person.Id` and `ctx.Db.person.Name`.
+
+## Interface `ITableView`
+
+```csharp
+namespace SpacetimeDB.Internal;
+
+public interface ITableView
+ where Row : IStructuralReadWrite, new()
{
- Error,
- Warn,
- Info,
- Debug,
- Trace,
- Panic
+ /* ... */
}
```
+
+
+Implemented for every table handle generated by the [`Table`](#tables) attribute.
+For a table named `{name}`, a handle can be extracted from a [`ReducerContext`](#class-reducercontext) using `ctx.Db.{name}`. For example, `ctx.Db.person`.
+
+Contains methods that are present for every table handle, regardless of what unique constraints
+and indexes are present.
+
+The type `Row` is the type of rows in the table.
+
+| Name | Description |
+| --------------------------------------------- | ----------------------------- |
+| [Method `Insert`](#method-itableviewinsert) | Insert a row into the table |
+| [Method `Delete`](#method-itableviewdelete) | Delete a row from the table |
+| [Method `Iter`](#method-itableviewiter) | Iterate all rows of the table |
+| [Property `Count`](#property-itableviewcount) | Count all rows of the table |
+
+### Method `ITableView.Insert`
+
+```csharp
+Row Insert(Row row);
+```
+
+Inserts `row` into the table.
+
+The return value is the inserted row, with any auto-incrementing columns replaced with computed values.
+The `insert` method always returns the inserted row, even when the table contains no auto-incrementing columns.
+
+(The returned row is a copy of the row in the database.
+Modifying this copy does not directly modify the database.
+See [`UniqueIndex.Update()`](#method-uniqueindexupdate) if you want to update the row.)
+
+Throws an exception if inserting the row violates any constraints.
+
+Inserting a duplicate row in a table is a no-op,
+as SpacetimeDB is a set-semantic database.
-If omitted, the log level will default to `Info`, so these two forms are equivalent:
+### Method `ITableView.Delete`
```csharp
-Log("Hello, World!");
-Log("Hello, World!", LogLevel.Info);
+bool Delete(Row row);
```
-### Supported types
+Deletes a row equal to `row` from the table.
-#### Built-in types
+Returns `true` if the row was present and has been deleted,
+or `false` if the row was not present and therefore the tables have not changed.
-The following types are supported out of the box and can be stored in the database tables directly or as part of more complex types:
+Unlike [`Insert`](#method-itableviewinsert), there is no need to return the deleted row,
+as it must necessarily have been exactly equal to the `row` argument.
+No analogue to auto-increment placeholders exists for deletions.
-- `bool`
-- `byte`, `sbyte`
-- `short`, `ushort`
-- `int`, `uint`
-- `long`, `ulong`
-- `float`, `double`
-- `string`
-- [`Int128`](https://learn.microsoft.com/en-us/dotnet/api/system.int128), [`UInt128`](https://learn.microsoft.com/en-us/dotnet/api/system.uint128)
-- `T[]` - arrays of supported values.
-- [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1)
-- [`Dictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2)
+Throws an exception if deleting the row would violate any constraints.
-And a couple of special custom types:
+### Method `ITableView.Iter`
-- `SpacetimeDB.SATS.Unit` - semantically equivalent to an empty struct, sometimes useful in generic contexts where C# doesn't permit `void`.
-- `Identity` (`SpacetimeDB.Runtime.Identity`) - a unique identifier for each user; internally a byte blob but can be printed, hashed and compared for equality.
-- `Address` (`SpacetimeDB.Runtime.Address`) - an identifier which disamgibuates connections by the same `Identity`; internally a byte blob but can be printed, hashed and compared for equality.
+```csharp
+IEnumerable Iter();
+```
-#### Custom types
+Iterate over all rows of the table.
-`[SpacetimeDB.Type]` attribute can be used on any `struct`, `class` or an `enum` to mark it as a SpacetimeDB type. It will implement serialization and deserialization for values of this type so that they can be stored in the database.
+(This keeps track of changes made to the table since the start of this reducer invocation. For example, if rows have been [deleted](#method-itableviewdelete) since the start of this reducer invocation, those rows will not be returned by `Iter`. Similarly, [inserted](#method-itableviewinsert) rows WILL be returned.)
-Any `struct` or `class` marked with this attribute, as well as their respective parents, must be `partial`, as the code generator will add methods to them.
+For large tables, this can be a slow operation! Prefer [filtering](#method-indexfilter) by an [`Index`](#class-index) or [finding](#method-uniqueindexfind) a [`UniqueIndex`](#class-uniqueindex) if possible.
+
+### Property `ITableView.Count`
```csharp
-[SpacetimeDB.Type]
-public partial struct Point
-{
- public int x;
- public int y;
+ulong Count { get; }
+```
+
+Returns the number of rows of this table.
+
+This takes into account modifications by the current transaction,
+even though those modifications have not yet been committed or broadcast to clients.
+This applies generally to insertions, deletions, updates, and iteration as well.
+
+## Public and Private Tables
+
+By default, tables are considered **private**. This means that they are only readable by the database owner and by reducers. Reducers run inside the database, so clients cannot see private tables at all or even know of their existence.
+
+Using the `[SpacetimeDB.Table(Name = "table_name", Public)]` flag makes a table public. **Public** tables are readable by all clients. They can still only be modified by reducers.
+
+(Note that, when run by the module owner, the `spacetime sql ` command can also read private tables. This is for debugging convenience. Only the module owner can see these tables. This is determined by the `Identity` stored by the `spacetime login` command. Run `spacetime login show` to print your current logged-in `Identity`.)
+
+To learn how to subscribe to a public table, see the [client SDK documentation](https://spacetimedb.com/docs/sdks).
+
+## Unique and Primary Key Columns
+
+Columns of a table (that is, fields of a [`[Table]`](#tables) struct) can be annotated with `[Unique]` or `[PrimaryKey]`. Multiple columns can be `[Unique]`, but only one can be `[PrimaryKey]`. For example:
+
+```csharp
+[SpacetimeDB.Table(Name = "citizen")]
+public partial struct Citizen {
+ [SpacetimeDB.PrimaryKey]
+ ulong Id;
+
+ [SpacetimeDB.Unique]
+ string Ssn;
+
+ [SpacetimeDB.Unique]
+ string Email;
+
+ string name;
}
```
-`enum`s marked with this attribute must not use custom discriminants, as the runtime expects them to be always consecutive starting from zero. Unlike structs and classes, they don't use `partial` as C# doesn't allow to add methods to `enum`s.
+Every row in the table `Person` must have unique entries in the `id`, `ssn`, and `email` columns. Attempting to insert multiple `Person`s with the same `id`, `ssn`, or `email` will throw an exception.
+
+Any `[Unique]` or `[PrimaryKey]` column supports getting a [`UniqueIndex`](#class-uniqueindex) from a [`ReducerContext`](#class-reducercontext) using:
+
+```text
+ctx.Db.{table}.{unique_column}
+```
+
+For example,
```csharp
-[SpacetimeDB.Type]
-public enum Color
+ctx.Db.citizen.Ssn
+```
+
+Notice that updating a row is only possible if a row has a unique column -- there is no `update` method in the base [`ITableView`](#interface-itableview) interface. SpacetimeDB has no notion of rows having an "identity" aside from their unique / primary keys.
+
+The `[PrimaryKey]` annotation implies a `[Unique]` annotation, but avails additional methods in the [client]-side SDKs.
+
+It is not currently possible to mark a group of fields as collectively unique.
+
+Filtering on unique columns is only supported for a limited number of types.
+
+## Class `UniqueIndex`
+
+```csharp
+namespace SpacetimeDB.Internal;
+
+public abstract class UniqueIndex : IndexBase
+ where Handle : ITableView
+ where Row : IStructuralReadWrite, new()
+ where Column : IEquatable
{
- Red,
- Green,
- Blue,
+ /* ... */
+}
+```
+
+
+A unique index on a column. Available for `[Unique]` and `[PrimaryKey]` columns.
+(A custom class derived from `UniqueIndex` is generated for every such column.)
+
+`Row` is the type decorated with `[SpacetimeDB.Table]`, `Column` is the type of the column,
+and `Handle` is the type of the generated table handle.
+
+For a table *table* with a column *column*, use `ctx.Db.{table}.{column}`
+to get a `UniqueColumn` from a [`ReducerContext`](#class-reducercontext).
+
+Example:
+
+```csharp
+using SpacetimeDB;
+
+public static partial class Module {
+ [Table(Name = "user")]
+ public partial struct User {
+ [PrimaryKey]
+ uint Id;
+ [Unique]
+ string Username;
+ ulong DogCount;
+ }
+
+ [Reducer]
+ void Demo(ReducerContext ctx) {
+ var idIndex = ctx.Db.user.Id;
+ var exampleUser = idIndex.find(357).unwrap();
+ exampleUser.dog_count += 5;
+ idIndex.update(exampleUser);
+
+ var usernameIndex = ctx.Db.user.Username;
+ usernameIndex.delete("Evil Bob");
+ }
}
```
-#### Tagged enums
+| Name | Description |
+| -------------------------------------------- | -------------------------------------------- |
+| [Method `Find`](#method-uniqueindexfind) | Find a row by the value of a unique column |
+| [Method `Update`](#method-uniqueindexupdate) | Update a row with a unique column |
+| [Method `Delete`](#method-uniqueindexdelete) | Delete a row by the value of a unique column |
-SpacetimeDB has support for tagged enums which can be found in languages like Rust, but not C#.
+
-We provide a tagged enum support for C# modules via a special `record SpacetimeDB.TaggedEnum<(...types and names of the variants as a tuple...)>`.
+### Method `UniqueIndex.Find`
-When you inherit from the `SpacetimeDB.TaggedEnum` marker, it will generate variants as subclasses of the annotated type, so you can use regular C# pattern matching operators like `is` or `switch` to determine which variant a given tagged enum holds at any time.
+```csharp
+Row? Find(Column key);
+```
-For unit variants (those without any data payload) you can use a built-in `SpacetimeDB.Unit` as the variant type.
+Finds and returns the row where the value in the unique column matches the supplied `key`,
+or `null` if no such row is present in the database state.
-Example:
+### Method `UniqueIndex.Update`
```csharp
-// Define a tagged enum named `MyEnum` with three variants,
-// `MyEnum.String`, `MyEnum.Int` and `MyEnum.None`.
-[SpacetimeDB.Type]
-public partial record MyEnum : SpacetimeDB.TaggedEnum<(
- string String,
- int Int,
- SpacetimeDB.Unit None
-)>;
+Row Update(Row row);
+```
+
+Deletes the row where the value in the unique column matches that in the corresponding field of `row` and then inserts `row`.
+
+Returns the new row as actually inserted, with any auto-inc placeholders substituted for computed values.
+
+Throws if no row was previously present with the matching value in the unique column,
+or if either the delete or the insertion would violate a constraint.
+
+### Method `UniqueIndex.Delete`
+
+```csharp
+bool Delete(Column key);
+```
+
+Deletes the row where the value in the unique column matches the supplied `key`, if any such row is present in the database state.
+
+Returns `true` if a row with the specified `key` was previously present and has been deleted,
+or `false` if no such row was present.
+
+## Auto-inc columns
+
+Columns can be marked `[SpacetimeDB.AutoInc]`. This can only be used on integer types (`int`, `ulong`, etc.)
+
+When inserting into or updating a row in a table with an `[AutoInc]` column, if the annotated column is set to zero (`0`), the database will automatically overwrite that zero with an atomically increasing value.
+
+[`ITableView.Insert`] and [`UniqueIndex.Update()`](#method-uniqueindexupdate) returns rows with `[AutoInc]` columns set to the values that were actually written into the database.
-// Print an instance of `MyEnum`, using `switch`/`case` to determine the active variant.
-void PrintEnum(MyEnum e)
+```csharp
+public static partial class Module
{
- switch (e)
+ [SpacetimeDB.Table(Name = "example")]
+ public partial struct Example
{
- case MyEnum.String(var s):
- Console.WriteLine(s);
- break;
-
- case MyEnum.Int(var i):
- Console.WriteLine(i);
- break;
+ [SpacetimeDB.AutoInc]
+ public int Field;
+ }
- case MyEnum.None:
- Console.WriteLine("(none)");
- break;
+ [SpacetimeDB.Reducer]
+ public static void InsertAutoIncExample(ReducerContext ctx, int Id, string Name) {
+ for (var i = 0; i < 10; i++) {
+ // These will have distinct, unique values
+ // at rest in the database, since they
+ // are inserted with the sentinel value 0.
+ var actual = ctx.Db.example.Insert(new Example { Field = 0 });
+ Debug.Assert(actual.Field != 0);
+ }
}
}
+```
+
+`[AutoInc]` is often combined with `[Unique]` or `[PrimaryKey]` to automatically assign unique integer identifiers to rows.
+
+## Indexes
-// Test whether an instance of `MyEnum` holds some value (either a string or an int one).
-bool IsSome(MyEnum e) => e is not MyEnum.None;
+SpacetimeDB supports both single- and multi-column [B-Tree](https://en.wikipedia.org/wiki/B-tree) indexes.
-// Construct an instance of `MyEnum` with the `String` variant active.
-var myEnum = new MyEnum.String("Hello, world!");
-Console.WriteLine($"IsSome: {IsSome(myEnum)}");
-PrintEnum(myEnum);
+Indexes are declared using the syntax:
+
+```csharp
+[SpacetimeDB.Index.BTree(Name = "IndexName", Columns = [nameof(Column1), nameof(Column2), nameof(Column3)])]
+```
+
+For example:
+
+```csharp
+[SpacetimeDB.Table(Name = "paper")]
+[SpacetimeDB.Index.BTree(Name = "TitleAndDate", Columns = [nameof(Title), nameof(Date)])]
+[SpacetimeDB.Index.BTree(Name = "UrlAndCountry", Columns = [nameof(Url), nameof(Country)])]
+public partial struct AcademicPaper {
+ public string Title;
+ public string Url;
+ public string Date;
+ public string Venue;
+ public string Country;
+}
```
-### Tables
+Multiple indexes can be declared.
-`[SpacetimeDB.Table]` attribute can be used on any `struct` or `class` to mark it as a SpacetimeDB table. It will register a table in the database with the given name and fields as well as will generate C# methods to insert, filter, update, and delete rows of the given type.
-By default, tables are **private**. This means that they are only readable by the table owner, and by server module code.
-Adding `[SpacetimeDB.Table(Public = true))]` annotation makes a table public. **Public** tables are readable by all users, but can still only be modified by your server module code.
+Single-column indexes can also be declared using an annotation on a column:
+
+```csharp
+[SpacetimeDB.Table(Name = "academic_paper")]
+public partial struct AcademicPaper {
+ public string Title;
+ public string Url;
+ [SpacetimeDB.Index.BTree] // The index will be named "Date".
+ public string Date;
+ [SpacetimeDB.Index.BTree] // The index will be named "Venue".
+ public string Venue;
+ [SpacetimeDB.Index.BTree(Name = "ByCountry")] // The index will be named "ByCountry".
+ public string Country;
+}
+```
-_Coming soon: We plan to add much more robust access controls than just public or private. Stay tuned!_
-It implies `[SpacetimeDB.Type]`, so you must not specify both attributes on the same type.
+Any table supports getting an [`Index`](#class-index) using `ctx.Db.{table}.{index}`. For example, `ctx.Db.academic_paper.TitleAndDate` or `ctx.Db.academic_paper.Venue`.
+
+## Class `Index`
```csharp
-[SpacetimeDB.Table(Public = true)]
-public partial struct Person
+public abstract class IndexBase
+ where Row : IStructuralReadWrite, new()
{
- [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)]
- public int Id;
- public string Name;
- public int Age;
+ // ...
}
```
-The example above will generate the following extra methods:
+Each index generates a subclass of `IndexBase`, which is accessible via `ctx.Db.{table}.{index}`. For example, `ctx.Db.academic_paper.TitleAndDate`.
+
+Indexes can be applied to a variable number of columns, referred to as `Column1`, `Column2`, `Column3`... in the following examples.
+
+| Name | Description |
+| -------------------------------------- | ----------------------- |
+| Method [`Filter`](#method-indexfilter) | Filter rows in an index |
+| Method [`Delete`](#method-indexdelete) | Delete rows in an index |
+
+### Method `Index.Filter`
```csharp
-public partial struct Person
+public IEnumerable Filter(Column1 bound);
+public IEnumerable Filter(Bound bound);
+public IEnumerable Filter((Column1, Column2) bound);
+public IEnumerable Filter((Column1, Bound) bound);
+public IEnumerable Filter((Column1, Column2, Column3) bound);
+public IEnumerable Filter((Column1, Column2, Bound) bound);
+// ...
+```
+
+Returns an iterator over all rows in the database state where the indexed column(s) match the passed `bound`. Bound is a tuple of column values, possibly terminated by a `Bound`. A `Bound` is simply a tuple `(LastColumn Min, LastColumn Max)`. Any prefix of the indexed columns can be passed, for example:
+
+```csharp
+using SpacetimeDB;
+
+public static partial class Module
{
- // Inserts current instance as a new row into the table.
- public void Insert();
+ [SpacetimeDB.Table(Name = "zoo_animal")]
+ [SpacetimeDB.Index.BTree(Name = "SpeciesAgeName", Columns = [nameof(Species), nameof(Age), nameof(Name)])]
+ public partial struct ZooAnimal
+ {
+ public string Species;
+ public uint Age;
+ public string Name;
+ [SpacetimeDB.PrimaryKey]
+ public uint Id;
+ }
- // Returns an iterator over all rows in the table, e.g.:
- // `for (var person in Person.Iter()) { ... }`
- public static IEnumerable Iter();
+ [SpacetimeDB.Reducer]
+ public static void Example(ReducerContext ctx)
+ {
+ foreach (var baboon in ctx.Db.zoo_animal.SpeciesAgeName.Filter("baboon"))
+ {
+ // Work with the baboon.
+ }
+ foreach (var animal in ctx.Db.zoo_animal.SpeciesAgeName.Filter(("b", "e")))
+ {
+ // Work with the animal.
+ // The name of the species starts with a character between "b" and "e".
+ }
+ foreach (var babyBaboon in ctx.Db.zoo_animal.SpeciesAgeName.Filter(("baboon", 1)))
+ {
+ // Work with the baby baboon.
+ }
+ foreach (var youngBaboon in ctx.Db.zoo_animal.SpeciesAgeName.Filter(("baboon", (1, 5))))
+ {
+ // Work with the young baboon.
+ }
+ foreach (var babyBaboonNamedBob in ctx.Db.zoo_animal.SpeciesAgeName.Filter(("baboon", 1, "Bob")))
+ {
+ // Work with the baby baboon named "Bob".
+ }
+ foreach (var babyBaboon in ctx.Db.zoo_animal.SpeciesAgeName.Filter(("baboon", 1, ("a", "f"))))
+ {
+ // Work with the baby baboon, whose name starts with a letter between "a" and "f".
+ }
+ }
+}
+```
- // Returns an iterator over all rows in the table that match the given filter, e.g.:
- // `for (var person in Person.Query(p => p.Age >= 18)) { ... }`
- public static IEnumerable Query(Expression> filter);
+### Method `Index.Delete`
- // Generated for each column:
+```csharp
+public ulong Delete(Column1 bound);
+public ulong Delete(Bound bound);
+public ulong Delete((Column1, Column2) bound);
+public ulong Delete((Column1, Bound) bound);
+public ulong Delete((Column1, Column2, Column3) bound);
+public ulong Delete((Column1, Column2, Bound) bound);
+// ...
+```
- // Returns an iterator over all rows in the table that have the given value in the `Name` column.
- public static IEnumerable FilterByName(string name);
- public static IEnumerable FilterByAge(int age);
+Delete all rows in the database state where the indexed column(s) match the passed `bound`. Returns the count of rows deleted. Note that there may be multiple rows deleted even if only a single column value is passed, since the index is not guaranteed to be unique.
- // Generated for each unique column:
+# Reducers
- // Finds a row in the table with the given value in the `Id` column and returns it, or `null` if no such row exists.
- public static Person? FindById(int id);
+Reducers are declared using the `[SpacetimeDB.Reducer]` attribute.
- // Deletes a row in the table with the given value in the `Id` column and returns `true` if the row was found and deleted, or `false` if no such row exists.
- public static bool DeleteById(int id);
+`[SpacetimeDB.Reducer]` is always applied to static C# functions. The first parameter of a reducer must be a [`ReducerContext`]. The remaining parameters must be types marked with [`SpacetimeDB.Type`]. Reducers should return `void`.
- // Updates a row in the table with the given value in the `Id` column and returns `true` if the row was found and updated, or `false` if no such row exists.
- public static bool UpdateById(int oldId, Person newValue);
+```csharp
+public static partial class Module {
+ [SpacetimeDB.Reducer]
+ public static void GivePlayerItem(
+ ReducerContext context,
+ ulong PlayerId,
+ ulong ItemId
+ )
+ {
+ // ...
+ }
}
```
-You can create multiple tables backed by items of the same type by applying it with different names. For example, to store active and archived posts separately and with different privacy rules, you can declare two tables like this:
+Every reducer runs inside a [database transaction](https://en.wikipedia.org/wiki/Database_transaction). This means that reducers will not observe the effects of other reducers modifying the database while they run. If a reducer fails, all of its changes to the database will automatically be rolled back. Reducers can fail by throwing an exception.
+
+## Class `ReducerContext`
```csharp
-[SpacetimeDB.Table(Name = "Post", Public = true)]
-[SpacetimeDB.Table(Name = "ArchivedPost", Public = false)]
-public partial struct Post {
- public string Title;
- public string Body;
+public sealed record ReducerContext : DbContext, Internal.IReducerContext
+{
+ // ...
}
```
-#### Column attributes
+Reducers have access to a special [`ReducerContext`] parameter. This parameter allows reading and writing the database attached to a module. It also provides some additional functionality, like generating random numbers and scheduling future operations.
-Attribute `[SpacetimeDB.Column]` can be used on any field of a `SpacetimeDB.Table`-marked `struct` or `class` to customize column attributes as seen above.
+[`ReducerContext`] provides access to the database tables via [the `.Db` property](#property-reducercontextdb). The [`[Table]`](#tables) attribute generated code that adds table accessors to this property.
-The supported column attributes are:
+| Name | Description |
+| --------------------------------------------------------------- | ------------------------------------------------------------------------------- |
+| Property [`Db`](#property-reducercontextdb) | The current state of the database |
+| Property [`Sender`](#property-reducercontextsender) | The [`Identity`](#struct-identity) of the caller of the reducer |
+| Property [`ConnectionId`](#property-reducercontextconnectionid) | The [`ConnectionId`](#struct-connectionid) of the caller of the reducer, if any |
+| Property [`Rng`](#property-reducercontextrng) | A [`System.Random`] instance. |
+| Property [`Timestamp`](#property-reducercontexttimestamp) | The [`Timestamp`](#struct-timestamp) of the reducer invocation |
+| Property [`Identity`](#property-reducercontextidentity) | The [`Identity`](#struct-identity) of the module |
-- `ColumnAttrs.AutoInc` - this column should be auto-incremented.
-- `ColumnAttrs.Unique` - this column should be unique.
-- `ColumnAttrs.PrimaryKey` - this column should be a primary key, it implies `ColumnAttrs.Unique` but also allows clients to subscribe to updates via `OnUpdate` which will use this field to match the old and the new version of the row with each other.
+### Property `ReducerContext.Db`
-These attributes are bitflags and can be combined together, but you can also use some predefined shortcut aliases:
+```csharp
+DbView Db;
+```
-- `ColumnAttrs.Identity` - same as `ColumnAttrs.Unique | ColumnAttrs.AutoInc`.
-- `ColumnAttrs.PrimaryKeyAuto` - same as `ColumnAttrs.PrimaryKey | ColumnAttrs.AutoInc`.
+Allows accessing the local database attached to a module.
-### Reducers
+The `[Table]` attribute generates a field of this property.
-Attribute `[SpacetimeDB.Reducer]` can be used on any `static void` method to register it as a SpacetimeDB reducer. The method must accept only supported types as arguments. If it throws an exception, those will be caught and reported back to the database runtime.
+For a table named *table*, use `ctx.Db.{table}` to get a [table view](#interface-itableview).
+For example, `ctx.Db.users`.
+
+You can also use `ctx.Db.{table}.{index}` to get an [index](#class-index) or [unique index](#class-uniqueindex).
+
+### Property `ReducerContext.Sender`
```csharp
-[SpacetimeDB.Reducer]
-public static void Add(string name, int age)
-{
- var person = new Person { Name = name, Age = age };
- person.Insert();
- Log($"Inserted {person.Name} under #{person.Id}");
-}
+Identity Sender;
```
-If a reducer has an argument with a type `ReducerContext` (`SpacetimeDB.Runtime.ReducerContext`), it will be provided with event details such as the sender identity (`SpacetimeDB.Runtime.Identity`), sender address (`SpacetimeDB.Runtime.Address?`) and the time (`DateTimeOffset`) of the invocation:
+The [`Identity`](#struct-identity) of the client that invoked the reducer.
+
+### Property `ReducerContext.ConnectionId`
```csharp
-[SpacetimeDB.Reducer]
-public static void PrintInfo(ReducerContext e)
-{
- Log($"Sender identity: {e.Sender}");
- Log($"Sender address: {e.Address}");
- Log($"Time: {e.Time}");
-}
+ConnectionId? ConnectionId;
+```
+
+The [`ConnectionId`](#struct-connectionid) of the client that invoked the reducer.
+
+`null` if no `ConnectionId` was supplied to the `/database/call` HTTP endpoint,
+or via the CLI's `spacetime call` subcommand.
+
+### Property `ReducerContext.Rng`
+
+```csharp
+Random Rng;
+```
+
+A [`System.Random`] that can be used to generate random numbers.
+
+### Property `ReducerContext.Timestamp`
+
+```csharp
+Timestamp Timestamp;
+```
+
+The time at which the reducer was invoked.
+
+### Property `ReducerContext.Identity`
+
+```csharp
+Identity Identity;
```
-### Scheduler Tables
+The [`Identity`](#struct-identity) of the module.
+
+This can be used to [check whether a scheduled reducer is being called by a user](#restricting-scheduled-reducers).
+
+Note: this is not the identity of the caller, that's [`ReducerContext.Sender`](#property-reducercontextsender).
+
+
+## Lifecycle Reducers
+
+A small group of reducers are called at set points in the module lifecycle. These are used to initialize
+the database and respond to client connections. You can have one of each per module.
+
+These reducers cannot be called manually and may not have any parameters except for `ReducerContext`.
+
+### The `Init` reducer
-Tables can be used to schedule a reducer calls either at a specific timestamp or at regular intervals.
+This reducer is marked with `[SpacetimeDB.Reducer(ReducerKind.Init)]`. It is run the first time a module is published and any time the database is cleared.
+
+If an error occurs when initializing, the module will not be published.
+
+This reducer can be used to configure any static data tables used by your module. It can also be used to start running [scheduled reducers](#scheduled-reducers).
+
+### The `ClientConnected` reducer
+
+This reducer is marked with `[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]`. It is run when a client connects to the SpacetimeDB module. Their identity can be found in the sender value of the `ReducerContext`.
+
+If an error occurs in the reducer, the client will be disconnected.
+
+### The `ClientDisconnected` reducer
+
+This reducer is marked with `[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]`. It is run when a client disconnects from the SpacetimeDB module. Their identity can be found in the sender value of the `ReducerContext`.
+
+If an error occurs in the disconnect reducer, the client is still recorded as disconnected.
+
+
+## Scheduled Reducers
+
+Reducers can schedule other reducers to run asynchronously. This allows calling the reducers at a particular time, or at repeating intervals. This can be used to implement timers, game loops, and maintenance tasks.
+
+The scheduling information for a reducer is stored in a table.
+This table has two mandatory fields:
+- An `[AutoInc] [PrimaryKey] ulong` field that identifies scheduled reducer calls.
+- A [`ScheduleAt`](#record-scheduleat) field that says when to call the reducer.
+
+Managing timers with a scheduled table is as simple as inserting or deleting rows from the table.
+This makes scheduling transactional in SpacetimeDB. If a reducer A first schedules B but then errors for some other reason, B will not be scheduled to run.
+
+A [`ScheduleAt`](#record-scheduleat) can be created from a [`Timestamp`](#struct-timestamp), in which case the reducer will be scheduled once, or from a [`TimeDuration`](#struct-timeduration), in which case the reducer will be scheduled in a loop.
+
+Example:
```csharp
-public static partial class Timers
+using SpacetimeDB;
+
+public static partial class Module
{
- // The `Scheduled` attribute links this table to a reducer.
- [SpacetimeDB.Table(Scheduled = nameof(SendScheduledMessage))]
- public partial struct SendMessageTimer
+ // First, we declare the table with scheduling information.
+
+ [Table(Name = "send_message_schedule", Scheduled = nameof(SendMessage), ScheduledAt = nameof(ScheduledAt))]
+ public partial struct SendMessageSchedule
{
- public string Text;
+
+ // Mandatory fields:
+
+ [PrimaryKey]
+ [AutoInc]
+ public ulong Id;
+
+ public ScheduleAt ScheduledAt;
+
+ // Custom fields:
+
+ public string Message;
}
+ // Then, we declare the scheduled reducer.
+ // The first argument of the reducer should be, as always, a `ReducerContext`.
+ // The second argument should be a row of the scheduling information table.
- // Define the reducer that will be invoked by the scheduler table.
- // The first parameter is always `ReducerContext`, and the second parameter is an instance of the linked table struct.
- [SpacetimeDB.Reducer]
- public static void SendScheduledMessage(ReducerContext ctx, SendMessageTimer arg)
+ [Reducer]
+ public static void SendMessage(ReducerContext ctx, SendMessageSchedule schedule)
{
+ Log.Info($"Sending message {schedule.Message}");
// ...
}
+ // Finally, we want to actually start scheduling reducers.
+ // It's convenient to do this inside the `init` reducer.
- // Scheduling reducers inside `init` reducer.
- [SpacetimeDB.Reducer(ReducerKind.Init)]
+ [Reducer(ReducerKind.Init)]
public static void Init(ReducerContext ctx)
{
+ var currentTime = ctx.Timestamp;
+ var tenSeconds = new TimeDuration { Microseconds = +10_000_000 };
+ var futureTimestamp = currentTime + tenSeconds;
- // Schedule a one-time reducer call by inserting a row.
- new SendMessageTimer
+ ctx.Db.send_message_schedule.Insert(new()
{
- Text = "bot sending a message",
- ScheduledAt = ctx.Time.AddSeconds(10),
- ScheduledId = 1,
- }.Insert();
-
+ Id = 0, // Have [AutoInc] assign an Id.
+ ScheduledAt = new ScheduleAt.Time(futureTimestamp),
+ Message = "I'm a bot sending a message one time!"
+ });
- // Schedule a recurring reducer.
- new SendMessageTimer
+ ctx.Db.send_message_schedule.Insert(new()
{
- Text = "bot sending a message",
- ScheduledAt = new TimeStamp(10),
- ScheduledId = 2,
- }.Insert();
+ Id = 0, // Have [AutoInc] assign an Id.
+ ScheduledAt = new ScheduleAt.Interval(tenSeconds),
+ Message = "I'm a bot sending a message every ten seconds!"
+ });
}
}
```
-Annotating a struct with `Scheduled` automatically adds fields to support scheduling, It can be expanded as:
+Scheduled reducers are called on a best-effort basis and may be slightly delayed in their execution
+when a database is under heavy load.
+
+### Restricting scheduled reducers
+
+Scheduled reducers are normal reducers, and may still be called by clients.
+If a scheduled reducer should only be called by the scheduler, consider beginning it with a check that the caller `Identity` is the module:
```csharp
-public static partial class Timers
+[Reducer]
+public static void SendMessage(ReducerContext ctx, SendMessageSchedule schedule)
{
- [SpacetimeDB.Table]
- public partial struct SendMessageTimer
+ if (ctx.Sender != ctx.Identity)
{
- public string Text; // fields of original struct
+ throw new Exception("Reducer SendMessage may not be invoked by clients, only via scheduling.");
+ }
+ // ...
+}
+```
+
+# Automatic migrations
+
+When you `spacetime publish` a module that has already been published using `spacetime publish `,
+SpacetimeDB attempts to automatically migrate your existing database to the new schema. (The "schema" is just the collection
+of tables and reducers you've declared in your code, together with the types they depend on.) This form of migration is limited and only supports a few kinds of changes.
+On the plus side, automatic migrations usually don't break clients. The situations that may break clients are documented below.
+
+The following changes are always allowed and never breaking:
+
+- ✅ **Adding tables**. Non-updated clients will not be able to see the new tables.
+- ✅ **Adding indexes**.
+- ✅ **Adding or removing `[AutoInc]` annotations.**
+- ✅ **Changing tables from private to public**.
+- ✅ **Adding reducers**.
+- ✅ **Removing `[Unique]` annotations.**
+
+The following changes are allowed, but may break clients:
+
+- ⚠️ **Changing or removing reducers**. Clients that attempt to call the old version of a changed reducer will receive runtime errors.
+- ⚠️ **Changing tables from public to private**. Clients that are subscribed to a newly-private table will receive runtime errors.
+- ⚠️ **Removing `[PrimaryKey]` annotations**. Non-updated clients will still use the old `[PrimaryKey]` as a unique key in their local cache, which can result in non-deterministic behavior when updates are received.
+- ⚠️ **Removing indexes**. This is only breaking in some situtations.
+ The specific problem is subscription queries involving semijoins, such as:
+ ```sql
+ SELECT Employee.*
+ FROM Employee JOIN Dept
+ ON Employee.DeptName = Dept.DeptName
+ )
+ ```
+ For performance reasons, SpacetimeDB will only allow this kind of subscription query if there are indexes on `Employee.DeptName` and `Dept.DeptName`. Removing either of these indexes will invalidate this subscription query, resulting in client-side runtime errors.
+
+The following changes are forbidden without a manual migration:
- [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)]
- public ulong ScheduledId; // unique identifier to be used internally
+- ❌ **Removing tables**.
+- ❌ **Changing the columns of a table**. This includes changing the order of columns of a table.
+- ❌ **Changing whether a table is used for [scheduling](#scheduled-reducers).**
+- ❌ **Adding `[Unique]` or `[PrimaryKey]` constraints.** This could result in existing tables being in an invalid state.
- public SpacetimeDB.ScheduleAt ScheduleAt; // Scheduling details (Time or Inteval)
+Currently, manual migration support is limited. The `spacetime publish --clear-database ` command can be used to **COMPLETELY DELETE** and reinitialize your database, but naturally it should be used with EXTREME CAUTION.
+
+# Other infrastructure
+
+## Class `Log`
+
+```csharp
+namespace SpacetimeDB
+{
+ public static class Log
+ {
+ public static void Debug(string message);
+ public static void Error(string message);
+ public static void Exception(string message);
+ public static void Exception(Exception exception);
+ public static void Info(string message);
+ public static void Trace(string message);
+ public static void Warn(string message);
}
}
-
-// `ScheduledAt` definition
-public abstract partial record ScheduleAt: SpacetimeDB.TaggedEnum<(DateTimeOffset Time, TimeSpan Interval)>
```
-#### Special reducers
+Methods for writing to a private debug log. Log messages will include file and line numbers.
-These are four special kinds of reducers that can be used to respond to module lifecycle events. They're stored in the `SpacetimeDB.Module.ReducerKind` class and can be used as an argument to the `[SpacetimeDB.Reducer]` attribute:
+Log outputs of a running module can be inspected using the `spacetime logs` command:
-- `ReducerKind.Init` - this reducer will be invoked when the module is first published.
-- `ReducerKind.Update` - this reducer will be invoked when the module is updated.
-- `ReducerKind.Connect` - this reducer will be invoked when a client connects to the database.
-- `ReducerKind.Disconnect` - this reducer will be invoked when a client disconnects from the database.
+```text
+spacetime logs
+```
+
+These are only visible to the database owner, not to clients or other developers.
+
+Note that `Log.Error` and `Log.Exception` only write to the log, they do not throw exceptions themselves.
Example:
-````csharp
-[SpacetimeDB.Reducer(ReducerKind.Init)]
-public static void Init()
+```csharp
+using SpacetimeDB;
+
+public static partial class Module {
+ [Table(Name = "user")]
+ public partial struct User {
+ [PrimaryKey]
+ uint Id;
+ [Unique]
+ string Username;
+ ulong DogCount;
+ }
+
+ [Reducer]
+ public static void LogDogs(ReducerContext ctx) {
+ Log.Info("Examining users.");
+
+ var totalDogCount = 0;
+
+ foreach (var user in ctx.Db.user.Iter()) {
+ Log.Info($" User: Id = {user.Id}, Username = {user.Username}, DogCount = {user.DogCount}");
+
+ totalDogCount += user.DogCount;
+ }
+
+ if (totalDogCount < 300) {
+ Log.Warn("Insufficient dogs.");
+ }
+
+ if (totalDogCount < 100) {
+ Log.Error("Dog population is critically low!");
+ }
+ }
+}
+```
+
+## Attribute `[SpacetimeDB.Type]`
+
+This attribute makes types self-describing, allowing them to automatically register their structure
+with SpacetimeDB. Any C# type annotated with `[SpacetimeDB.Type]` can be used as a table column or reducer argument.
+
+Types marked `[SpacetimeDB.Table]` are automatically marked `[SpacetimeDB.Type]`.
+
+`[SpacetimeDB.Type]` can be combined with [`SpacetimeDB.TaggedEnum`] to use tagged enums in tables or reducers.
+
+```csharp
+using SpacetimeDB;
+
+public static partial class Module {
+
+ [Type]
+ public partial struct Coord {
+ public int X;
+ public int Y;
+ }
+
+ [Type]
+ public partial struct TankData {
+ public int Ammo;
+ public int LeftTreadHealth;
+ public int RightTreadHealth;
+ }
+
+ [Type]
+ public partial struct TransportData {
+ public int TroopCount;
+ }
+
+ // A type that could be either the data for a Tank or the data for a Transport.
+ // See SpacetimeDB.TaggedEnum docs.
+ [Type]
+ public partial record VehicleData : TaggedEnum<(TankData Tank, TransportData Transport)> {}
+
+ [Table(Name = "vehicle")]
+ public partial struct Vehicle {
+ [PrimaryKey]
+ [AutoInc]
+ public uint Id;
+ public Coord Coord;
+ public VehicleData Data;
+ }
+
+ [SpacetimeDB.Reducer]
+ public static void InsertVehicle(ReducerContext ctx, Coord Coord, VehicleData Data) {
+ ctx.Db.vehicle.Insert(new Vehicle { Id = 0, Coord = Coord, Data = Data });
+ }
+}
+```
+
+The fields of the struct/enum must also be marked with `[SpacetimeDB.Type]`.
+
+Some types from the standard library are also considered to be marked with `[SpacetimeDB.Type]`, including:
+- `byte`
+- `sbyte`
+- `ushort`
+- `short`
+- `uint`
+- `int`
+- `ulong`
+- `long`
+- `SpacetimeDB.U128`
+- `SpacetimeDB.I128`
+- `SpacetimeDB.U256`
+- `SpacetimeDB.I256`
+- `List` where `T` is a `[SpacetimeDB.Type]`
+
+## Struct `Identity`
+
+```csharp
+namespace SpacetimeDB;
+
+public readonly record struct Identity
{
- Log("...and we're live!");
+ public static Identity FromHexString(string hex);
+ public string ToString();
}
+```
+
+An `Identity` for something interacting with the database.
+
+This is a record struct, so it can be printed, compared with `==`, and used as a `Dictionary` key.
+
+`ToString()` returns a hex encoding of the Identity, suitable for printing.
-[SpacetimeDB.Reducer(ReducerKind.Update)]
-public static void Update()
+
+
+## Struct `ConnectionId`
+
+```csharp
+namespace SpacetimeDB;
+
+public readonly record struct ConnectionId
{
- Log("Update get!");
+ public static ConnectionId? FromHexString(string hex);
+ public string ToString();
}
+```
+
+A unique identifier for a client connection to a SpacetimeDB database.
+
+This is a record struct, so it can be printed, compared with `==`, and used as a `Dictionary` key.
+
+`ToString()` returns a hex encoding of the `ConnectionId`, suitable for printing.
-[SpacetimeDB.Reducer(ReducerKind.Connect)]
-public static void OnConnect(DbEventArgs ctx)
+## Struct `Timestamp`
+
+```csharp
+namespace SpacetimeDB;
+
+public record struct Timestamp(long MicrosecondsSinceUnixEpoch)
+ : IStructuralReadWrite,
+ IComparable
{
- Log($"{ctx.Sender} has connected from {ctx.Address}!");
+ // ...
+}
+```
+
+A point in time, measured in microseconds since the Unix epoch.
+This can be converted to/from a standard library [`DateTimeOffset`]. It is provided for consistency of behavior between SpacetimeDB's supported module and SDK languages.
+
+| Name | Description |
+| ------------------------------------- | ----------------------------------------------------- |
+| Property `MicrosecondsSinceUnixEpoch` | Microseconds since the [unix epoch]. |
+| Conversion to/from `DateTimeOffset` | Convert to/from a standard library [`DateTimeOffset`] |
+| Static property `UNIX_EPOCH` | The [unix epoch] as a `Timestamp` |
+| Method `TimeDurationSince` | Measure the time elapsed since another `Timestamp` |
+| Operator `+` | Add a [`TimeDuration`] to a `Timestamp` |
+| Method `CompareTo` | Compare to another `Timestamp` |
+
+### Property `Timestamp.MicrosecondsSinceUnixEpoch`
+
+```csharp
+long MicrosecondsSinceUnixEpoch;
+```
+
+The number of microseconds since the [unix epoch].
+
+A positive value means a time after the Unix epoch, and a negative value means a time before.
+
+### Conversion to/from `DateTimeOffset`
+
+```csharp
+public static implicit operator DateTimeOffset(Timestamp t);
+public static implicit operator Timestamp(DateTimeOffset offset);
+```
+`Timestamp` may be converted to/from a [`DateTimeOffset`], but the conversion can lose precision.
+This type has less precision than DateTimeOffset (units of microseconds rather than units of 100ns).
+
+### Static property `Timestamp.UNIX_EPOCH`
+```csharp
+public static readonly Timestamp UNIX_EPOCH = new Timestamp { MicrosecondsSinceUnixEpoch = 0 };
+```
+
+The [unix epoch] as a `Timestamp`.
+
+### Method `Timestamp.TimeDurationSince`
+```csharp
+public readonly TimeDuration TimeDurationSince(Timestamp earlier) =>
+```
+
+Create a new [`TimeDuration`] that is the difference between two `Timestamps`.
+
+### Operator `Timestamp.+`
+```csharp
+public static Timestamp operator +(Timestamp point, TimeDuration interval);
+```
+
+Create a new `Timestamp` that occurs `interval` after `point`.
+
+### Method `Timestamp.CompareTo`
+```csharp
+public int CompareTo(Timestamp that)
+```
+
+Compare two `Timestamp`s.
+
+## Struct `TimeDuration`
+```csharp
+namespace SpacetimeDB;
+
+public record struct TimeDuration(long Microseconds) : IStructuralReadWrite {
+ // ...
}
+```
+
+A duration that represents an interval between two [`Timestamp`]s.
+
+This type may be converted to/from a [`TimeSpan`]. It is provided for consistency of behavior between SpacetimeDB's supported module and SDK languages.
+
+| Name | Description |
+| ------------------------------------------------------------- | ------------------------------------------------- |
+| Property [`Microseconds`](#property-timedurationmicroseconds) | Microseconds between the [`Timestamp`]s. |
+| [Conversion to/from `TimeSpan`](#conversion-tofrom-timespan) | Convert to/from a standard library [`TimeSpan`] |
+| Static property [`ZERO`](#static-property-timedurationzero) | The duration between any [`Timestamp`] and itself |
+
+### Property `TimeDuration.Microseconds`
+```csharp
+long Microseconds;
+```
+
+The number of microseconds between two [`Timestamp`]s.
+
+### Conversion to/from `TimeSpan`
+```csharp
+public static implicit operator TimeSpan(TimeDuration d) =>
+ new(d.Microseconds * Util.TicksPerMicrosecond);
+
+public static implicit operator TimeDuration(TimeSpan timeSpan) =>
+ new(timeSpan.Ticks / Util.TicksPerMicrosecond);
+```
+
+`TimeDuration` may be converted to/from a [`TimeSpan`], but the conversion can lose precision.
+This type has less precision than [`TimeSpan`] (units of microseconds rather than units of 100ns).
+
+### Static property `TimeDuration.ZERO`
+```csharp
+public static readonly TimeDuration ZERO = new TimeDuration { Microseconds = 0 };
+```
-[SpacetimeDB.Reducer(ReducerKind.Disconnect)]
-public static void OnDisconnect(DbEventArgs ctx)
+The duration between any `Timestamp` and itself.
+
+## Record `TaggedEnum`
+```csharp
+namespace SpacetimeDB;
+
+public abstract record TaggedEnum : IEquatable> where Variants : struct, ITuple
+```
+
+A [tagged enum](https://en.wikipedia.org/wiki/Tagged_union) is a type that can hold a value from any one of several types. `TaggedEnum` uses code generation to accomplish this.
+
+For example, to declare a type that can be either a `string` or an `int`, write:
+
+```csharp
+[SpacetimeDB.Type]
+public partial record ProductId : SpacetimeDB.TaggedEnum<(string Text, uint Number)> { }
+```
+
+Here there are two **variants**: one is named `Text` and holds a `string`, the other is named `Number` and holds a `uint`.
+
+To create a value of this type, use `new {Type}.{Variant}({data})`. For example:
+
+```csharp
+ProductId a = new ProductId.Text("apple");
+ProductId b = new ProductId.Number(57);
+ProductId c = new ProductId.Number(59);
+```
+
+To use a value of this type, you need to check which variant it stores.
+This is done with [C# pattern matching syntax](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching). For example:
+
+```csharp
+public static void Print(ProductId id)
{
- Log($"{ctx.Sender} has disconnected.");
-}```
-````
+ if (id is ProductId.Text(var s))
+ {
+ Log.Info($"Textual product ID: '{s}'");
+ }
+ else if (id is ProductId.Number(var i))
+ {
+ Log.Info($"Numeric Product ID: {i}");
+ }
+}
+```
+
+A `TaggedEnum` can have up to 255 variants, and the variants can be any type marked with [`[SpacetimeDB.Type]`].
+
+```csharp
+[SpacetimeDB.Type]
+public partial record ManyChoices : SpacetimeDB.TaggedEnum<(
+ string String,
+ int Int,
+ List IntList,
+ Banana Banana,
+ List> BananaMatrix
+)> { }
+
+[SpacetimeDB.Type]
+public partial struct Banana {
+ public int Sweetness;
+ public int Rot;
+}
+```
+
+`TaggedEnums` are an excellent alternative to nullable fields when groups of fields are always set together. Consider a data type like:
+
+```csharp
+[SpacetimeDB.Type]
+public partial struct ShapeData {
+ public int? CircleRadius;
+ public int? RectWidth;
+ public int? RectHeight;
+}
+```
+
+Often this is supposed to be a circle XOR a rectangle -- that is, not both at the same time. If this is the case, then we don't want to set `circleRadius` at the same time as `rectWidth` or `rectHeight`. Also, if `rectWidth` is set, we expect `rectHeight` to be set.
+However, C# doesn't know about this, so code using this type will be littered with extra null checks.
+
+If we instead write:
+
+```csharp
+[SpacetimeDB.Type]
+public partial struct CircleData {
+ public int Radius;
+}
+
+[SpacetimeDB.Type]
+public partial struct RectData {
+ public int Width;
+ public int Height;
+}
+
+[SpacetimeDB.Type]
+public partial record ShapeData : SpacetimeDB.TaggedEnum<(CircleData Circle, RectData Rect)> { }
+```
+
+Then code using a `ShapeData` will only have to do one check -- do I have a circle or a rectangle?
+And in each case, the data will be guaranteed to have exactly the fields needed.
+
+## Record `ScheduleAt`
+```csharp
+namespace SpacetimeDB;
+
+public partial record ScheduleAt : TaggedEnum<(TimeDuration Interval, Timestamp Time)>
+```
+
+When a [scheduled reducer](#scheduled-reducers) should execute, either at a specific point in time, or at regular intervals for repeating schedules.
+
+Stored in reducer-scheduling tables as a column.
+
+[demo]: /#demo
+[client]: https://spacetimedb.com/docs/#client
+[clients]: https://spacetimedb.com/docs/#client
+[client SDK documentation]: https://spacetimedb.com/docs/#client
+[host]: https://spacetimedb.com/docs/#host
+[`DateTimeOffset`]: https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset?view=net-9.0
+[`TimeSpan`]: https://learn.microsoft.com/en-us/dotnet/api/system.timespan?view=net-9.0
+[unix epoch]: https://en.wikipedia.org/wiki/Unix_time
+[`System.Random`]: https://learn.microsoft.com/en-us/dotnet/api/system.random?view=net-9.0
\ No newline at end of file
diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md
index 571351c1..86bcf16f 100644
--- a/docs/modules/c-sharp/quickstart.md
+++ b/docs/modules/c-sharp/quickstart.md
@@ -10,8 +10,6 @@ Each table is defined as a C# `class` annotated with `[SpacetimeDB.Table]`, wher
By default, tables are **private**. This means that they are only readable by the table owner, and by server module code.
The `[SpacetimeDB.Table(Public = true))]` annotation makes a table public. **Public** tables are readable by all users, but can still only be modified by your server module code.
-_Coming soon: We plan to add much more robust access controls than just public or private tables. Stay tuned!_
-
A reducer is a function which traverses and updates the database. Each reducer call runs in its own transaction, and its updates to the database are only committed if the reducer returns successfully. In C#, reducers are defined as functions annotated with `[SpacetimeDB.Reducer]`. If an exception is thrown, the reducer call fails, the database is not updated, and a failed message is reported to the client.
## Install SpacetimeDB
@@ -57,21 +55,18 @@ spacetime init --lang csharp server
2. Open `server/Lib.cs`, a trivial module.
3. Clear it out, so we can write a new module that's still pretty simple: a bare-bones chat server.
+To start, we'll need to add `SpacetimeDB` to our using statements. This will give us access to everything we need to author our SpacetimeDB server module.
+
To the top of `server/Lib.cs`, add some imports we'll be using:
```csharp
-using System.Runtime.CompilerServices;
-using SpacetimeDB.Module;
-using static SpacetimeDB.Runtime;
+using SpacetimeDB;
```
-- `SpacetimeDB.Module` contains the special attributes we'll use to define tables and reducers in our module.
-- `SpacetimeDB.Runtime` contains the raw API bindings SpacetimeDB uses to communicate with the database.
-
We also need to create our static module class which all of the module code will live in. In `server/Lib.cs`, add:
```csharp
-static partial class Module
+public static partial class Module
{
}
```
@@ -85,10 +80,10 @@ For each `User`, we'll store their `Identity`, an optional name they can set to
In `server/Lib.cs`, add the definition of the table `User` to the `Module` class:
```csharp
-[SpacetimeDB.Table(Public = true)]
+[Table(Name = "user", Public = true)]
public partial class User
{
- [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]
+ [PrimaryKey]
public Identity Identity;
public string? Name;
public bool Online;
@@ -100,11 +95,11 @@ For each `Message`, we'll store the `Identity` of the user who sent it, the `Tim
In `server/Lib.cs`, add the definition of the table `Message` to the `Module` class:
```csharp
-[SpacetimeDB.Table(Public = true)]
+[Table(Name = "message", Public = true)]
public partial class Message
{
public Identity Sender;
- public long Sent;
+ public Timestamp Sent;
public string Text = "";
}
```
@@ -113,23 +108,23 @@ public partial class Message
We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `SetName` which clients can invoke to set their `User.Name`. It will validate the caller's chosen name, using a function `ValidateName` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail.
-Each reducer may accept as its first argument a `ReducerContext`, which includes the `Identity` and `Address` of the client that called the reducer, and the `Timestamp` when it was invoked. For now, we only need the `Identity`, `ctx.Sender`.
+Each reducer must accept as its first argument a `ReducerContext`, which includes contextual data such as the `Sender` which contains the Identity of the client that called the reducer, and the `Timestamp` when it was invoked. For now, we only need the `Sender`.
It's also possible to call `SetName` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though.
In `server/Lib.cs`, add to the `Module` class:
```csharp
-[SpacetimeDB.Reducer]
+[Reducer]
public static void SetName(ReducerContext ctx, string name)
{
name = ValidateName(name);
- var user = User.FindByIdentity(ctx.Sender);
+ var user = ctx.Db.user.Identity.Find(ctx.Sender);
if (user is not null)
{
user.Name = name;
- User.UpdateByIdentity(ctx.Sender, user);
+ ctx.Db.user.Identity.Update(user);
}
}
```
@@ -146,7 +141,7 @@ In `server/Lib.cs`, add to the `Module` class:
```csharp
/// Takes a name and checks if it's acceptable as a user's name.
-public static string ValidateName(string name)
+private static string ValidateName(string name)
{
if (string.IsNullOrEmpty(name))
{
@@ -163,17 +158,19 @@ We define a reducer `SendMessage`, which clients will call to send messages. It
In `server/Lib.cs`, add to the `Module` class:
```csharp
-[SpacetimeDB.Reducer]
+[Reducer]
public static void SendMessage(ReducerContext ctx, string text)
{
text = ValidateMessage(text);
- Log(text);
- new Message
- {
- Sender = ctx.Sender,
- Text = text,
- Sent = ctx.Time.ToUnixTimeMilliseconds(),
- }.Insert();
+ Log.Info(text);
+ ctx.Db.message.Insert(
+ new Message
+ {
+ Sender = ctx.Sender,
+ Text = text,
+ Sent = ctx.Timestamp,
+ }
+ );
}
```
@@ -183,7 +180,7 @@ In `server/Lib.cs`, add to the `Module` class:
```csharp
/// Takes a message's text and checks if it's acceptable to send.
-public static string ValidateMessage(string text)
+private static string ValidateMessage(string text)
{
if (string.IsNullOrEmpty(text))
{
@@ -202,58 +199,60 @@ You could extend the validation in `ValidateMessage` in similar ways to `Validat
In C# modules, you can register for `Connect` and `Disconnect` events by using a special `ReducerKind`. We'll use the `Connect` event to create a `User` record for the client if it doesn't yet exist, and to set its online status.
-We'll use `User.FindByIdentity` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`.
+We'll use `reducerContext.Db.User.Identity.Find` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `reducerContext.Db.User.Identity.Update` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `User.Identity.Find` returns a nullable `User`, because the unique constraint from the `[PrimaryKey]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `User.Identity.Update`.
In `server/Lib.cs`, add the definition of the connect reducer to the `Module` class:
```csharp
-[SpacetimeDB.Reducer(ReducerKind.Connect)]
-public static void OnConnect(ReducerContext ReducerContext)
+[Reducer(ReducerKind.ClientConnected)]
+public static void ClientConnected(ReducerContext ctx)
{
- Log($"Connect {ReducerContext.Sender}");
- var user = User.FindByIdentity(ReducerContext.Sender);
+ Log.Info($"Connect {ctx.Sender}");
+ var user = ctx.Db.user.Identity.Find(ctx.Sender);
if (user is not null)
{
// If this is a returning user, i.e., we already have a `User` with this `Identity`,
// set `Online: true`, but leave `Name` and `Identity` unchanged.
user.Online = true;
- User.UpdateByIdentity(ReducerContext.Sender, user);
+ ctx.Db.user.Identity.Update(user);
}
else
{
// If this is a new user, create a `User` object for the `Identity`,
// which is online, but hasn't set a name.
- new User
- {
- Name = null,
- Identity = ReducerContext.Sender,
- Online = true,
- }.Insert();
+ ctx.Db.user.Insert(
+ new User
+ {
+ Name = null,
+ Identity = ctx.Sender,
+ Online = true,
+ }
+ );
}
}
```
-Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.Disconnect`. We'll use it to un-set the `Online` status of the `User` for the disconnected client.
+Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.ClientDisconnected`. We'll use it to un-set the `Online` status of the `User` for the disconnected client.
Add the following code after the `OnConnect` handler:
```csharp
-[SpacetimeDB.Reducer(ReducerKind.Disconnect)]
-public static void OnDisconnect(ReducerContext ReducerContext)
+[Reducer(ReducerKind.ClientDisconnected)]
+public static void ClientDisconnected(ReducerContext ctx)
{
- var user = User.FindByIdentity(ReducerContext.Sender);
+ var user = ctx.Db.user.Identity.Find(ctx.Sender);
if (user is not null)
{
// This user should exist, so set `Online: false`.
user.Online = false;
- User.UpdateByIdentity(ReducerContext.Sender, user);
+ ctx.Db.user.Identity.Update(user);
}
else
{
// User does not exist, log warning
- Log("Warning: No user found for disconnected client.");
+ Log.Warn("Warning: No user found for disconnected client.");
}
}
```
@@ -264,30 +263,28 @@ If you haven't already started the SpacetimeDB server, run the `spacetime start`
## Publish the module
-And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more pleasant. Come up with a unique name, and fill it in where we've written ``.
+And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more pleasant. In this example, we'll be using `quickstart-chat`. Feel free to come up with a unique name, and in the CLI commands, replace where we've written `quickstart-chat` with the name you chose.
From the `quickstart-chat` directory, run:
```bash
-spacetime publish --project-path server
+spacetime publish --project-path server quickstart-chat
```
-```bash
-npm i wasm-opt -g
-```
+Note: If the WebAssembly optimizer `wasm-opt` is installed, `spacetime publish` will automatically optimize the Web Assembly output of the published module. Instruction for installing the `wasm-opt` binary can be found in [Rust's wasm-opt documentation](https://docs.rs/wasm-opt/latest/wasm_opt/).
## Call Reducers
You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format.
```bash
-spacetime call SendMessage "Hello, World!"
+spacetime call quickstart-chat SendMessage "Hello, World!"
```
Once we've called our `SendMessage` reducer, we can check to make sure it ran by running the `logs` command.
```bash
-spacetime logs
+spacetime logs quickstart-chat
```
You should now see the output that your module printed in the database.
@@ -301,7 +298,7 @@ info: Hello, World!
SpacetimeDB supports a subset of the SQL syntax so that you can easily query the data of your database. We can run a query using the `sql` command.
```bash
-spacetime sql "SELECT * FROM Message"
+spacetime sql quickstart-chat "SELECT * FROM Message"
```
```bash
@@ -312,6 +309,8 @@ spacetime sql "SELECT * FROM Message"
## What's next?
-You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](/docs/sdks/rust/quickstart), [C#](/docs/sdks/c-sharp/quickstart), or [TypeScript](/docs/sdks/typescript/quickstart).
+You've just set up your first database in SpacetimeDB! You can find the full code for this client [in the C# server module example](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/tree/master/examples~/quickstart-chat/server).
+
+The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](/docs/sdks/rust/quickstart), [C#](/docs/sdks/c-sharp/quickstart), or [TypeScript](/docs/sdks/typescript/quickstart).
-If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1) or check out our example game, [BitcraftMini](/docs/unity/part-3).
+If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1).
diff --git a/docs/modules/index.md b/docs/modules/index.md
index d7d13685..78d60d9c 100644
--- a/docs/modules/index.md
+++ b/docs/modules/index.md
@@ -8,7 +8,7 @@ In the following sections, we'll cover the basics of server modules and how to c
### Rust
-As of SpacetimeDB 0.6, Rust is the only fully supported language for server modules. Rust is a great option for server modules because it is fast, safe, and has a small runtime.
+Rust is the only fully supported language for server modules. Rust is a great option for server modules because it is fast, safe, and has a small runtime.
- [Rust Module Reference](/docs/modules/rust)
- [Rust Module Quickstart Guide](/docs/modules/rust/quickstart)
@@ -19,12 +19,3 @@ We have C# support available in experimental status. C# can be a good choice for
- [C# Module Reference](/docs/modules/c-sharp)
- [C# Module Quickstart Guide](/docs/modules/c-sharp/quickstart)
-
-### Coming Soon
-
-We have plans to support additional languages in the future.
-
-- Python
-- Typescript
-- C++
-- Lua
diff --git a/docs/modules/rust/index.md b/docs/modules/rust/index.md
index dba75ab2..a8681954 100644
--- a/docs/modules/rust/index.md
+++ b/docs/modules/rust/index.md
@@ -1,525 +1,4 @@
-# SpacetimeDB Rust Modules
+# Rust Module SDK Reference
-Rust clients of SpacetimeDB use the [Rust SpacetimeDB module library][module library] to write modules which interact with the SpacetimeDB database.
+The Rust Module SDK docs are [hosted on docs.rs](https://docs.rs/spacetimedb/latest/spacetimedb/).
-First, the `spacetimedb` library provides a number of macros for creating tables and Rust `struct`s corresponding to rows in those tables.
-
-Then the client API allows interacting with the database inside special functions called reducers.
-
-This guide assumes you are familiar with some basics of Rust. At the very least, you should be familiar with the idea of using attribute macros. An extremely common example is `derive` macros.
-
-Derive macros look at the type they are attached to and generate some related code. In this example, `#[derive(Debug)]` generates the formatting code needed to print out a `Location` for debugging purposes.
-
-```rust
-#[derive(Debug)]
-struct Location {
- x: u32,
- y: u32,
-}
-```
-
-## SpacetimeDB Macro basics
-
-Let's start with a highly commented example, straight from the [demo]. This Rust package defines a SpacetimeDB module, with types we can operate on and functions we can run.
-
-```rust
-// In this small example, we have two Rust imports:
-// |spacetimedb::spacetimedb| is the most important attribute we'll be using.
-// |spacetimedb::println| is like regular old |println|, but outputting to the module's logs.
-use spacetimedb::{spacetimedb, println};
-
-// This macro lets us interact with a SpacetimeDB table of Person rows.
-// We can insert and delete into, and query, this table by the collection
-// of functions generated by the macro.
-#[table(name = person, public)]
-pub struct Person {
- name: String,
-}
-
-// This is the other key macro we will be using. A reducer is a
-// stored procedure that lives in the database, and which can
-// be invoked remotely.
-#[reducer]
-pub fn add(ctx: &ReducerContext, name: String) {
- // |Person| is a totally ordinary Rust struct. We can construct
- // one from the given name as we typically would.
- let person = Person { name };
-
- // Here's our first generated function! Given a |Person| object,
- // we can insert it into the table:
- ctx.db.person().insert(person);
-}
-
-// Here's another reducer. Notice that this one doesn't take any arguments, while
-// |add| did take one. Reducers can take any number of arguments, as long as
-// SpacetimeDB recognizes their types. Reducers also have to be top level
-// functions, not methods.
-#[reducer]
-pub fn say_hello(ctx: &ReducerContext) {
- // Here's the next of our generated functions: |iter()|. This
- // iterates over all the columns in the |Person| table in SpacetimeDB.
- for person in ctx.db.person().iter() {
- // Reducers run in a very constrained and sandboxed environment,
- // and in particular, can't do most I/O from the Rust standard library.
- // We provide an alternative |spacetimedb::println| which is just like
- // the std version, excepted it is redirected out to the module's logs.
- println!("Hello, {}!", person.name);
- }
- println!("Hello, World!");
-}
-
-// Reducers can't return values, but can return errors. To do so,
-// the reducer must have a return type of `Result<(), T>`, for any `T` that
-// implements `Debug`. Such errors returned from reducers will be formatted and
-// printed out to logs.
-#[reducer]
-pub fn add_person(ctx: &ReducerContext, name: String) -> Result<(), String> {
- if name.is_empty() {
- return Err("Name cannot be empty");
- }
-
- ctx.db.person().insert(Person { name })
-}
-```
-
-## Macro API
-
-Now we'll get into details on all the macro APIs SpacetimeDB provides, starting with all the variants of the `spacetimedb` attribute.
-
-### Defining tables
-
-The `#[table(name = table_name)]` macro is applied to a Rust struct with named fields.
-By default, tables are considered **private**. This means that they are only readable by the table owner, and by server module code.
-The `#[table(name = table_name, public)]` macro makes a table public. **Public** tables are readable by all users, but can still only be modified by your server module code.
-
-_Coming soon: We plan to add much more robust access controls than just public or private. Stay tuned!_
-
-```rust
-#[table(name = my_table, public)]
-struct MyTable {
- field1: String,
- field2: u32,
-}
-```
-
-This attribute is applied to Rust structs in order to create corresponding tables in SpacetimeDB. Fields of the Rust struct correspond to columns of the database table.
-
-The fields of the struct have to be types that SpacetimeDB knows how to encode into the database. This is captured in Rust by the `SpacetimeType` trait.
-
-This is automatically defined for built in numeric types:
-
-- `bool`
-- `u8`, `u16`, `u32`, `u64`, `u128`
-- `i8`, `i16`, `i32`, `i64`, `i128`
-- `f32`, `f64`
-
-And common data structures:
-
-- `String` and `&str`, utf-8 string data
-- `()`, the unit type
-- `Option where T: SpacetimeType`
-- `Vec where T: SpacetimeType`
-
-All `#[table(..)]` types are `SpacetimeType`s, and accordingly, all of their fields have to be.
-
-```rust
-#[table(name = another_table, public)]
-struct AnotherTable {
- // Fine, some builtin types.
- id: u64,
- name: Option,
-
- // Fine, another table type.
- table: Table,
-
- // Fine, another type we explicitly make serializable.
- serial: Serial,
-}
-```
-
-If you want to have a field that is not one of the above primitive types, and not a table of its own, you can derive the `SpacetimeType` attribute on it.
-
-We can derive `SpacetimeType` on `struct`s and `enum`s with members that are themselves `SpacetimeType`s.
-
-```rust
-#[derive(SpacetimeType)]
-enum Serial {
- Builtin(f64),
- Compound {
- s: String,
- bs: Vec,
- }
-}
-```
-
-Once the table is created via the macro, other attributes described below can control more aspects of the table. For instance, a particular column can be indexed, or take on values of an automatically incremented counter. These are described in detail below.
-
-```rust
-#[table(name = person, public)]
-struct Person {
- #[unique]
- id: u64,
-
- name: String,
- address: String,
-}
-```
-
-You can create multiple tables backed by items of the same type by applying it with different names. For example, to store active and archived posts separately and with different privacy rules, you can declare two tables like this:
-
-```rust
-#[table(name = post, public)]
-#[table(name = archived_post)]
-struct Post {
- title: String,
- body: String,
-}
-```
-
-### Defining reducers
-
-`#[reducer]` is always applied to top level Rust functions. They can take arguments of types known to SpacetimeDB (just like fields of structs must be known to SpacetimeDB), and either return nothing, or return a `Result<(), E: Debug>`.
-
-```rust
-#[reducer]
-fn give_player_item(ctx: &ReducerContext, player_id: u64, item_id: u64) -> Result<(), GameErr> {
- // Notice how the exact name of the filter function derives from
- // the name of the field of the struct.
- let mut item = ctx.db.item().item_id().find(id).ok_or(GameErr::InvalidId)?;
- item.owner = Some(player_id);
- ctx.db.item().item_id().update(item);
- Ok(())
-}
-
-#[table(name = item, public)]
-struct Item {
- #[primary_key]
- item_id: u64,
- owner: Option,
-}
-```
-
-Note that reducers can call non-reducer functions, including standard library functions.
-
-There are several macros which modify the semantics of a column, which are applied to the members of the table struct. `#[primary_key]`, `#[unique]` and `#[autoinc]` are covered below, describing how those attributes affect the semantics of inserting, filtering, and so on.
-
-#[SpacetimeType]
-
-#[sats]
-
-### Defining Scheduler Tables
-
-Tables can be used to schedule a reducer calls either at a specific timestamp or at regular intervals.
-
-```rust
-// The `scheduled` attribute links this table to a reducer.
-#[table(name = send_message_timer, scheduled(send_message)]
-struct SendMessageTimer {
- text: String,
-}
-```
-
-The `scheduled` attribute adds a couple of default fields and expands as follows:
-
-```rust
-#[table(name = send_message_timer, scheduled(send_message)]
- struct SendMessageTimer {
- text: String, // original field
- #[primary_key]
- #[autoinc]
- scheduled_id: u64, // identifier for internal purpose
- scheduled_at: ScheduleAt, //schedule details
-}
-
-pub enum ScheduleAt {
- /// A specific time at which the reducer is scheduled.
- /// Value is a UNIX timestamp in microseconds.
- Time(u64),
- /// A regular interval at which the repeated reducer is scheduled.
- /// Value is a duration in microseconds.
- Interval(u64),
-}
-```
-
-Managing timers with a scheduled table is as simple as inserting or deleting rows from the table.
-
-```rust
-#[reducer]
-// Reducers linked to the scheduler table should have their first argument as `&ReducerContext`
-// and the second as an instance of the table struct it is linked to.
-fn send_message(ctx: &ReducerContext, arg: SendMessageTimer) -> Result<(), String> {
- // ...
-}
-
-// Scheduling reducers inside `init` reducer
-#[reducer(init)]
-fn init(ctx: &ReducerContext) {
- // Scheduling a reducer for a specific Timestamp
- ctx.db.send_message_timer().insert(SendMessageTimer {
- scheduled_id: 1,
- text:"bot sending a message".to_string(),
- //`spacetimedb::Timestamp` implements `From` trait to `ScheduleAt::Time`.
- scheduled_at: ctx.timestamp.plus(Duration::from_secs(10)).into()
- });
-
- // Scheduling a reducer to be called at fixed interval of 100 milliseconds.
- ctx.db.send_message_timer().insert(SendMessageTimer {
- scheduled_id: 0,
- text:"bot sending a message".to_string(),
- //`std::time::Duration` implements `From` trait to `ScheduleAt::Duration`.
- scheduled_at: duration!(100ms).into(),
- });
-}
-```
-
-## Client API
-
-Besides the macros for creating tables and reducers, there's two other parts of the Rust SpacetimeDB library. One is a collection of macros for logging, and the other is all the automatically generated functions for operating on those tables.
-
-### `println!` and friends
-
-Because reducers run in a WASM sandbox, they don't have access to general purpose I/O from the Rust standard library. There's no filesystem or network access, and no input or output. This means no access to things like `std::println!`, which prints to standard output.
-
-SpacetimeDB modules have access to logging output. These are exposed as macros, just like their `std` equivalents. The names, and all the Rust formatting machinery, work the same; just the location of the output is different.
-
-Logs for a module can be viewed with the `spacetime logs` command from the CLI.
-
-```rust
-use spacetimedb::{
- println,
- print,
- eprintln,
- eprint,
- dbg,
-};
-
-#[reducer]
-fn output(ctx: &ReducerContext, i: i32) {
- // These will be logged at log::Level::Info.
- println!("an int with a trailing newline: {i}");
- print!("some more text...\n");
-
- // These log at log::Level::Error.
- eprint!("Oops...");
- eprintln!(", we hit an error");
-
- // Just like std::dbg!, this prints its argument and returns the value,
- // as a drop-in way to print expressions. So this will print out |i|
- // before passing the value of |i| along to the calling function.
- //
- // The output is logged log::Level::Debug.
- ctx.db.outputted_number().insert(dbg!(i));
-}
-```
-
-### Generated functions on a SpacetimeDB table
-
-We'll work off these structs to see what functions SpacetimeDB generates:
-
-This table has a plain old column.
-
-```rust
-#[table(name = ordinary, public)]
-struct Ordinary {
- ordinary_field: u64,
-}
-```
-
-This table has a unique column. Every row in the `Unique` table must have distinct values of the `unique_field` column. Attempting to insert a row with a duplicate value will fail.
-
-```rust
-#[table(name = unique, public)]
-struct Unique {
- // A unique column:
- #[unique]
- unique_field: u64,
-}
-```
-
-This table has an automatically incrementing column. SpacetimeDB automatically provides an incrementing sequence of values for this field, and sets the field to that value when you insert the row.
-
-Only integer types can be `#[unique]`: `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64` and `i128`.
-
-```rust
-#[table(name = autoinc, public)]
-struct Autoinc {
- #[autoinc]
- autoinc_field: u64,
-}
-```
-
-These attributes can be combined, to create an automatically assigned ID usable for filtering.
-
-```rust
-#[table(name = identity, public)]
-struct Identity {
- #[autoinc]
- #[unique]
- id_field: u64,
-}
-```
-
-### Insertion
-
-We'll talk about insertion first, as there a couple of special semantics to know about.
-
-When we define |Ordinary| as a SpacetimeDB table, we get the ability to insert into it with the generated `ctx.db.ordinary().insert(..)` method.
-
-Inserting takes a single argument, the row to insert. When there are no unique fields in the row, the return value is the inserted row.
-
-```rust
-#[reducer]
-fn insert_ordinary(ctx: &ReducerContext, value: u64) {
- let ordinary = Ordinary { ordinary_field: value };
- let result = ctx.db.ordinary().insert(ordinary);
- assert_eq!(ordinary.ordinary_field, result.ordinary_field);
-}
-```
-
-When there is a unique column constraint on the table, insertion can fail if a uniqueness constraint is violated.
-
-If we insert two rows which have the same value of a unique column, the second will fail.
-
-```rust
-#[reducer]
-fn insert_unique(ctx: &ReducerContext, value: u64) {
- let result = ctx.db.unique().insert(Unique { unique_field: value });
- assert!(result.is_ok());
-
- let result = ctx.db.unique().insert(Unique { unique_field: value });
- assert!(result.is_err());
-}
-```
-
-When inserting a table with an `#[autoinc]` column, the database will automatically overwrite whatever we give it with an atomically increasing value.
-
-The returned row has the `autoinc` column set to the value that was actually written into the database.
-
-```rust
-#[reducer]
-fn insert_autoinc(ctx: &ReducerContext) {
- for i in 1..=10 {
- // These will have values of 1, 2, ..., 10
- // at rest in the database, regardless of
- // what value is actually present in the
- // insert call.
- let actual = ctx.db.autoinc().insert(Autoinc { autoinc_field: 23 })
- assert_eq!(actual.autoinc_field, i);
- }
-}
-
-#[reducer]
-fn insert_id(ctx: &ReducerContext) {
- for _ in 0..10 {
- // These also will have values of 1, 2, ..., 10.
- // There's no collision and silent failure to insert,
- // because the value of the field is ignored and overwritten
- // with the automatically incremented value.
- ctx.db.identity().insert(Identity { id_field: 23 })
- }
-}
-```
-
-### Iterating
-
-Given a table, we can iterate over all the rows in it.
-
-```rust
-#[table(name = person, public)]
-struct Person {
- #[unique]
- id: u64,
-
- #[index(btree)]
- age: u32,
- name: String,
- address: String,
-}
-```
-
-// Every table structure has a generated iter function, like:
-
-```rust
-ctx.db.my_table().iter()
-```
-
-`iter()` returns a regular old Rust iterator, giving us a sequence of `Person`. The database sends us over rows, one at a time, for each time through the loop. This means we get them by value, and own the contents of `String` fields and so on.
-
-```
-#[reducer]
-fn iteration(ctx: &ReducerContext) {
- let mut addresses = HashSet::new();
-
- for person in ctx.db.person().iter() {
- addresses.insert(person.address);
- }
-
- for address in addresses.iter() {
- println!("{address}");
- }
-}
-```
-
-### Filtering
-
-Often, we don't need to look at the entire table, and instead are looking for rows with specific values in certain columns.
-
-Our `Person` table has a unique id column, so we can filter for a row matching that ID. Since it is unique, we will find either 0 or 1 matching rows in the database. This gets represented naturally as an `Option` in Rust. SpacetimeDB automatically creates and uses indexes for filtering on unique columns, so it is very efficient.
-
-The name of the filter method just corresponds to the column name.
-
-```rust
-#[reducer]
-fn filtering(ctx: &ReducerContext, id: u64) {
- match ctx.db.person().id().find(id) {
- Some(person) => println!("Found {person}"),
- None => println!("No person with id {id}"),
- }
-}
-```
-
-Our `Person` table also has an index on its `age` column. Unlike IDs, ages aren't unique. Filtering for every person who is 21, then, gives us an `Iterator- ` rather than an `Option`.
-
-```rust
-#[reducer]
-fn filtering_non_unique(ctx: &ReducerContext) {
- for person in ctx.db.person().age().filter(21u32) {
- println!("{} has turned 21", person.name);
- }
-}
-```
-
-> NOTE: An unfortunate interaction between Rust's trait solver and integer literal defaulting rules means that you must specify the types of integer literals passed to `filter` and `find` methods via the suffix syntax, like `21u32`. If you don't, you'll see a compiler error like:
-> ```
-> error[E0271]: type mismatch resolving `::Column == u32`
-> --> modules/rust-wasm-test/src/lib.rs:356:48
-> |
-> 356 | for person in ctx.db.person().age().filter(21) {
-> | ------ ^^ expected `u32`, found `i32`
-> | |
-> | required by a bound introduced by this call
-> |
-> = note: required for `i32` to implement `BTreeIndexBounds<(u32,), SingleBound>`
-> note: required by a bound in `BTreeIndex::::filter`
-> |
-> 410 | pub fn filter(&self, b: B) -> impl Iterator
-
-> | ------ required by a bound in this associated function
-> 411 | where
-> 412 | B: BTreeIndexBounds,
-> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BTreeIndex::::filter`
-> ```
-
-### Deleting
-
-Like filtering, we can delete by an indexed or unique column instead of the entire row.
-
-```rust
-#[reducer]
-fn delete_id(ctx: &ReducerContext, id: u64) {
- ctx.db.person().id().delete(id)
-}
-```
-
-[macro library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/bindings-macro
-[module library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/lib
-[demo]: /#demo
diff --git a/docs/modules/rust/quickstart.md b/docs/modules/rust/quickstart.md
index 9fcfe30d..04b7d206 100644
--- a/docs/modules/rust/quickstart.md
+++ b/docs/modules/rust/quickstart.md
@@ -11,8 +11,6 @@ Each table is defined as a Rust struct annotated with `#[table(name = table_name
By default, tables are **private**. This means that they are only readable by the table owner, and by server module code.
The `#[table(name = table_name, public)]` macro makes a table public. **Public** tables are readable by all users but can still only be modified by your server module code.
-_Coming soon: We plan to add much more robust access controls than just public or private. Stay tuned!_
-
A reducer is a function that traverses and updates the database. Each reducer call runs in its own transaction, and its updates to the database are only committed if the reducer returns successfully. In Rust, reducers are defined as functions annotated with `#[reducer]`, and may return a `Result<()>`, with an `Err` return aborting the transaction.
## Install SpacetimeDB
@@ -100,7 +98,7 @@ pub struct Message {
We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `set_name` which clients can invoke to set their `User.name`. It will validate the caller's chosen name, using a function `validate_name` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail.
-Each reducer may accept as its first argument a `ReducerContext`, which includes the `Identity` and `Address` of the client that called the reducer, and the `Timestamp` when it was invoked. It also allows us access to the `db`, which is used to read and manipulate rows in our tables. For now, we only need the `db`, `Identity`, and `ctx.sender`.
+Each reducer must accept as its first argument a `ReducerContext`, which includes the `Identity` and `ConnectionId` of the client that called the reducer, and the `Timestamp` when it was invoked. It also allows us access to the `db`, which is used to read and manipulate rows in our tables. For now, we only need the `db`, `Identity`, and `ctx.sender`.
It's also possible to call `set_name` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though.
@@ -227,12 +225,12 @@ pub fn identity_disconnected(ctx: &ReducerContext) {
## Publish the module
-And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more user-friendly. Come up with a unique name that contains only URL-safe characters (letters, numbers, hyphens and underscores), and fill it in where we've written ``.
+And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique `Identity`. Clients can connect either by name or by `Identity`, but names are much more user-friendly. If you'd like, come up with a unique name that contains only URL-safe characters (letters, numbers, hyphens and underscores), and fill it in where we've written `quickstart-chat`.
From the `quickstart-chat` directory, run:
```bash
-spacetime publish --project-path server
+spacetime publish --project-path server quickstart-chat
```
## Call Reducers
@@ -240,13 +238,13 @@ spacetime publish --project-path server
You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format.
```bash
-spacetime call send_message 'Hello, World!'
+spacetime call quickstart-chat send_message 'Hello, World!'
```
Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command.
```bash
-spacetime logs
+spacetime logs quickstart-chat
```
You should now see the output that your module printed in the database.
@@ -263,7 +261,7 @@ You should now see the output that your module printed in the database.
SpacetimeDB supports a subset of the SQL syntax so that you can easily query the data of your database. We can run a query using the `sql` command.
```bash
-spacetime sql "SELECT * FROM message"
+spacetime sql quickstart-chat "SELECT * FROM message"
```
```bash
@@ -278,4 +276,4 @@ You can find the full code for this module [in the SpacetimeDB module examples](
You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quickstart guide for your client language of choice: [Rust](/docs/sdks/rust/quickstart), [C#](/docs/sdks/c-sharp/quickstart), or [TypeScript](/docs/sdks/typescript/quickstart).
-If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1) or check out our example game, [BitcraftMini](/docs/unity/part-3).
+If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1).
diff --git a/docs/nav.js b/docs/nav.js
index bdf49517..aed58053 100644
--- a/docs/nav.js
+++ b/docs/nav.js
@@ -10,15 +10,16 @@ const nav = {
page('Overview', 'index', 'index.md'), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'?
page('Getting Started', 'getting-started', 'getting-started.md'),
section('Deploying'),
- page('Testnet', 'deploying/testnet', 'deploying/testnet.md'),
- section('Migration Guides'),
- page('v0.12', 'migration/v0.12', 'migration/v0.12.md'),
+ page('Maincloud', 'deploying/maincloud', 'deploying/maincloud.md'),
section('Unity Tutorial - Basic Multiplayer'),
page('Overview', 'unity', 'unity/index.md'),
page('1 - Setup', 'unity/part-1', 'unity/part-1.md'),
page('2 - Connecting to SpacetimeDB', 'unity/part-2', 'unity/part-2.md'),
page('3 - Gameplay', 'unity/part-3', 'unity/part-3.md'),
page('4 - Moving and Colliding', 'unity/part-4', 'unity/part-4.md'),
+ section('CLI Reference'),
+ page('CLI Reference', 'cli-reference', 'cli-reference.md'),
+ page('SpacetimeDB Standalone Configuration', 'cli-reference/standalone-config', 'cli-reference/standalone-config.md'),
section('Server Module Languages'),
page('Overview', 'modules', 'modules/index.md'),
page('Rust Quickstart', 'modules/rust/quickstart', 'modules/rust/quickstart.md'),
@@ -27,26 +28,28 @@ const nav = {
page('C# Reference', 'modules/c-sharp', 'modules/c-sharp/index.md'),
section('Client SDK Languages'),
page('Overview', 'sdks', 'sdks/index.md'),
- page('Typescript Quickstart', 'sdks/typescript/quickstart', 'sdks/typescript/quickstart.md'),
- page('Typescript Reference', 'sdks/typescript', 'sdks/typescript/index.md'),
- page('Rust Quickstart', 'sdks/rust/quickstart', 'sdks/rust/quickstart.md'),
- page('Rust Reference', 'sdks/rust', 'sdks/rust/index.md'),
page('C# Quickstart', 'sdks/c-sharp/quickstart', 'sdks/c-sharp/quickstart.md'),
page('C# Reference', 'sdks/c-sharp', 'sdks/c-sharp/index.md'),
- section('WebAssembly ABI'),
- page('Module ABI Reference', 'webassembly-abi', 'webassembly-abi/index.md'),
+ page('Rust Quickstart', 'sdks/rust/quickstart', 'sdks/rust/quickstart.md'),
+ page('Rust Reference', 'sdks/rust', 'sdks/rust/index.md'),
+ page('TypeScript Quickstart', 'sdks/typescript/quickstart', 'sdks/typescript/quickstart.md'),
+ page('TypeScript Reference', 'sdks/typescript', 'sdks/typescript/index.md'),
+ section('SQL'),
+ page('SQL Reference', 'sql', 'sql/index.md'),
+ section('Subscriptions'),
+ page('Subscription Reference', 'subscriptions', 'subscriptions/index.md'),
+ section('How To'),
+ page('Incremental Migrations', 'how-to/incremental-migrations', 'how-to/incremental-migrations.md'),
section('HTTP API'),
page('HTTP', 'http', 'http/index.md'),
page('`/identity`', 'http/identity', 'http/identity.md'),
page('`/database`', 'http/database', 'http/database.md'),
- page('`/energy`', 'http/energy', 'http/energy.md'),
- section('WebSocket API Reference'),
- page('WebSocket', 'ws', 'ws/index.md'),
- section('Data Format'),
- page('SATN', 'satn', 'satn.md'),
- page('BSATN', 'bsatn', 'bsatn.md'),
- section('SQL'),
- page('SQL Reference', 'sql', 'sql/index.md'),
+ section('Internals'),
+ page('Module ABI Reference', 'webassembly-abi', 'webassembly-abi/index.md'),
+ page('SATS-JSON Data Format', 'sats-json', 'sats-json.md'),
+ page('BSATN Data Format', 'bsatn', 'bsatn.md'),
+ section('Appendix'),
+ page('Appendix', 'appendix', 'appendix.md'),
],
};
export default nav;
diff --git a/docs/satn.md b/docs/sats-json.md
similarity index 86%
rename from docs/satn.md
rename to docs/sats-json.md
index 6fb0ee9f..38f08756 100644
--- a/docs/satn.md
+++ b/docs/sats-json.md
@@ -1,6 +1,6 @@
-# SATN JSON Format
+# SATS-JSON Format
-The Spacetime Algebraic Type Notation JSON format defines how Spacetime `AlgebraicType`s and `AlgebraicValue`s are encoded as JSON. Algebraic types and values are JSON-encoded for transport via the [HTTP Databases API](/docs/http/database) and the [WebSocket text protocol](/docs/ws#text-protocol).
+The Spacetime Algebraic Type System JSON format defines how Spacetime `AlgebraicType`s and `AlgebraicValue`s are encoded as JSON. Algebraic types and values are JSON-encoded for transport via the [HTTP Databases API](/docs/http/database) and the WebSocket text protocol. Note that SATS-JSON is not self-describing, and so a SATS value represented in JSON requires knowing the value's schema to meaningfully understand it - for example, it's not possible to tell whether a JSON object with a single field is a `ProductValue` with one element or a `SumValue`.
## Values
@@ -32,6 +32,8 @@ The tag is an index into the [`SumType.variants`](#sumtype) array of the value's
}
```
+The tag may also be the name of one of the variants.
+
### `ProductValue`
An instance of a [`ProductType`](#producttype). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#producttype) array of the value's [`ProductType`](#producttype).
@@ -40,6 +42,10 @@ An instance of a [`ProductType`](#producttype). `ProductValue`s are encoded as J
array
```
+`ProductValue`s may also be encoded as a JSON object with the keys as the field
+names of the `ProductValue` and the values as the corresponding
+`AlgebraicValue`s.
+
### `BuiltinValue`
An instance of a [`BuiltinType`](#builtintype). `BuiltinValue`s are encoded as JSON values of corresponding types.
@@ -69,7 +75,7 @@ All SATS types are JSON-encoded by converting them to an `AlgebraicValue`, then
| --------------------------------------- | ------------------------------------------------------------------------------------ |
| [`AlgebraicType`](#algebraictype) | Any SATS type. |
| [`SumType`](#sumtype) | Sum types, i.e. tagged unions. |
-| [`ProductType`](#producttype) | Product types, i.e. structures. |
+| [`ProductType`](#producttype) | Product types, i.e. structures. |
| [`BuiltinType`](#builtintype) | Built-in and primitive types, including booleans, numbers, strings, arrays and maps. |
| [`AlgebraicTypeRef`](#algebraictyperef) | An indirect reference to a type, used to implement recursive types. |
@@ -160,4 +166,4 @@ SATS array and map types are homogeneous, meaning that each array has a single e
### `AlgebraicTypeRef`
-`AlgebraicTypeRef`s are JSON-encoded as non-negative integers. These are indices into a typespace, like the one returned by the [`/database/schema/:name_or_address GET` HTTP endpoint](/docs/http/database#databaseschemaname_or_address-get).
+`AlgebraicTypeRef`s are JSON-encoded as non-negative integers. These are indices into a typespace, like the one returned by the [`GET /v1/database/:name_or_identity/schema` HTTP endpoint](/docs/http/database#get-v1databasename_or_identityschema).
diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md
index e9c5f23a..16fd2068 100644
--- a/docs/sdks/c-sharp/index.md
+++ b/docs/sdks/c-sharp/index.md
@@ -1,56 +1,22 @@
# The SpacetimeDB C# client SDK
-The SpacetimeDB client C# for Rust contains all the tools you need to build native clients for SpacetimeDB modules using C#.
-
-## Table of Contents
-
-- [The SpacetimeDB C# client SDK](#the-spacetimedb-c-client-sdk)
- - [Table of Contents](#table-of-contents)
- - [Install the SDK](#install-the-sdk)
- - [Using the `dotnet` CLI tool](#using-the-dotnet-cli-tool)
- - [Using Unity](#using-unity)
- - [Generate module bindings](#generate-module-bindings)
- - [Initialization](#initialization)
- - [Property `SpacetimeDBClient.instance`](#property-spacetimedbclientinstance)
- - [Class `NetworkManager`](#class-networkmanager)
- - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect)
- - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived)
- - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect)
- - [Subscribe to queries](#subscribe-to-queries)
- - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe)
- - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied)
- - [Method \[`SpacetimeDBClient.OneOffQuery`\]](#method-spacetimedbclientoneoffquery)
- - [View rows of subscribed tables](#view-rows-of-subscribed-tables)
- - [Class `{TABLE}`](#class-table)
- - [Static Method `{TABLE}.Iter`](#static-method-tableiter)
- - [Static Method `{TABLE}.FilterBy{COLUMN}`](#static-method-tablefilterbycolumn)
- - [Static Method `{TABLE}.FindBy{COLUMN}`](#static-method-tablefindbycolumn)
- - [Static Method `{TABLE}.Count`](#static-method-tablecount)
- - [Static Event `{TABLE}.OnInsert`](#static-event-tableoninsert)
- - [Static Event `{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete)
- - [Static Event `{TABLE}.OnDelete`](#static-event-tableondelete)
- - [Static Event `{TABLE}.OnUpdate`](#static-event-tableonupdate)
- - [Observe and invoke reducers](#observe-and-invoke-reducers)
- - [Class `Reducer`](#class-reducer)
- - [Static Method `Reducer.{REDUCER}`](#static-method-reducerreducer)
- - [Static Event `Reducer.On{REDUCER}`](#static-event-reduceronreducer)
- - [Class `ReducerEvent`](#class-reducerevent)
- - [Enum `Status`](#enum-status)
- - [Variant `Status.Committed`](#variant-statuscommitted)
- - [Variant `Status.Failed`](#variant-statusfailed)
- - [Variant `Status.OutOfEnergy`](#variant-statusoutofenergy)
- - [Identity management](#identity-management)
- - [Class `AuthToken`](#class-authtoken)
- - [Static Method `AuthToken.Init`](#static-method-authtokeninit)
- - [Static Property `AuthToken.Token`](#static-property-authtokentoken)
- - [Static Method `AuthToken.SaveToken`](#static-method-authtokensavetoken)
- - [Class `Identity`](#class-identity)
- - [Customizing logging](#customizing-logging)
- - [Interface `ISpacetimeDBLogger`](#interface-ispacetimedblogger)
- - [Class `ConsoleLogger`](#class-consolelogger)
- - [Class `UnityDebugLogger`](#class-unitydebuglogger)
-
-## Install the SDK
+The SpacetimeDB client for C# contains all the tools you need to build native clients for SpacetimeDB modules using C#.
+
+| Name | Description |
+|---------------------------------------------------------|---------------------------------------------------------------------------|
+| [Project setup](#project-setup) | Configure a C# project to use the SpacetimeDB C# client SDK. |
+| [Generate module bindings](#generate-module-bindings) | Use the SpacetimeDB CLI to generate module-specific types and interfaces. |
+| [`DbConnection` type](#type-dbconnection) | A connection to a remote database. |
+| [`IDbContext` interface](#interface-idbcontext) | Methods for interacting with the remote database. |
+| [`EventContext` type](#type-eventcontext) | Implements [`IDbContext`](##interface-idbcontext) for [row callbacks](#callback-oninsert). |
+| [`ReducerEventContext` type](#type-reducereventcontext) | Implements [`IDbContext`](##interface-idbcontext) for [reducer callbacks](#observe-and-invoke-reducers). |
+| [`SubscriptionEventContext` type](#type-subscriptioneventcontext) | Implements [`IDbContext`](##interface-idbcontext) for [subscription callbacks](#subscribe-to-queries). |
+| [`ErrorContext` type](#type-errorcontext) | Implements [`IDbContext`](##interface-idbcontext) for error-related callbacks. |
+| [Access the client cache](#access-the-client-cache) | Access to your local view of the database. |
+| [Observe and invoke reducers](#observe-and-invoke-reducers) | Send requests to the database to run reducers, and register callbacks to run when notified of reducers. |
+| [Identify a client](#identify-a-client) | Types for identifying users and client connections. |
+
+## Project setup
### Using the `dotnet` CLI tool
@@ -81,853 +47,878 @@ spacetime generate --lang cs --out-dir module_bindings --project-path PATH-TO-MO
Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module.
-## Initialization
+## Type `DbConnection`
-### Property `SpacetimeDBClient.instance`
+A connection to a remote database is represented by the `DbConnection` class. This class is generated per module and contains information about the types, tables, and reducers defined by your module.
-```cs
-namespace SpacetimeDB {
+| Name | Description |
+|------------------------------------------------------------------------|-------------------------------------------------------------------------------|
+| [Connect to a module](#connect-to-a-module) | Construct a `DbConnection` instance. |
+| [Advance the connection](#advance-the-connection-and-process-messages) | Poll the `DbConnection` or run it in the background. |
+| [Access tables and reducers](#access-tables-and-reducers) | Access the client cache, request reducer invocations, and register callbacks. |
-public class SpacetimeDBClient {
- public static SpacetimeDBClient instance;
-}
+## Connect to a module
+```csharp
+class DbConnection
+{
+ public static DbConnectionBuilder Builder();
}
```
-This is the global instance of a SpacetimeDB client in a particular .NET/Unity process. Much of the SDK is accessible through this instance.
+Construct a `DbConnection` by calling `DbConnection.Builder()`, chaining configuration methods, and finally calling `.Build()`. At a minimum, you must specify `WithUri` to provide the URI of the SpacetimeDB instance, and `WithModuleName` to specify the module's name or identity.
-### Class `NetworkManager`
+| Name | Description |
+|---------------------------------------------------------|--------------------------------------------------------------------------------------------|
+| [WithUri method](#method-withuri) | Set the URI of the SpacetimeDB instance hosting the remote database. |
+| [WithModuleName method](#method-withmodulename) | Set the name or identity of the remote module. |
+| [OnConnect callback](#callback-onconnect) | Register a callback to run when the connection is successfully established. |
+| [OnConnectError callback](#callback-onconnecterror) | Register a callback to run if the connection is rejected or the host is unreachable. |
+| [OnDisconnect callback](#callback-ondisconnect) | Register a callback to run when the connection ends. |
+| [WithToken method](#method-withtoken) | Supply a token to authenticate with the remote database. |
+| [Build method](#method-build) | Finalize configuration and open the connection. |
-The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component.
+### Method `WithUri`
-This component will handle updating and closing the [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect), you still need to handle that yourself. See the [Unity Tutorial](/docs/unity) for more information.
+```csharp
+class DbConnectionBuilder
+{
+ public DbConnectionBuilder WithUri(Uri uri);
+}
+```
-### Method `SpacetimeDBClient.Connect`
+Configure the URI of the SpacetimeDB instance or cluster which hosts the remote module.
-```cs
-namespace SpacetimeDB {
+### Method `WithModuleName`
-class SpacetimeDBClient {
- public void Connect(
- string? token,
- string host,
- string addressOrName,
- bool sslEnabled = true
- );
+```csharp
+class DbConnectionBuilder
+{
+ public DbConnectionBuilder WithModuleName(string nameOrIdentity);
}
+```
+
+Configure the SpacetimeDB domain name or `Identity` of the remote module which identifies it within the SpacetimeDB instance or cluster.
+### Callback `OnConnect`
+
+```csharp
+class DbConnectionBuilder
+{
+ public DbConnectionBuilder OnConnect(Action callback);
}
```
-
+Chain a call to `.OnConnect(callback)` to your builder to register a callback to run when your new `DbConnection` successfully initiates its connection to the remote module. The callback accepts three arguments: a reference to the `DbConnection`, the `Identity` by which SpacetimeDB identifies this connection, and a private access token which can be saved and later passed to [`WithToken`](#method-withtoken) to authenticate the same user in future connections.
-Connect to a database named `addressOrName` accessible over the internet at the URI `host`.
+### Callback `OnConnectError`
-| Argument | Type | Meaning |
-| --------------- | --------- | -------------------------------------------------------------------------- |
-| `token` | `string?` | Identity token to use, if one is available. |
-| `host` | `string` | URI of the SpacetimeDB instance running the module. |
-| `addressOrName` | `string` | Address or name of the module. |
-| `sslEnabled` | `bool` | Whether or not to use SSL when connecting to SpacetimeDB. Default: `true`. |
+```csharp
+class DbConnectionBuilder
+{
+ public DbConnectionBuilder OnConnectError(Action callback);
+}
+```
-If a `token` is supplied, it will be passed to the new connection to identify and authenticate the user. Otherwise, a new token and [`Identity`](#class-identity) will be generated by the server and returned in [`onConnect`](#event-spacetimedbclientonconnect).
+Chain a call to `.OnConnectError(callback)` to your builder to register a callback to run when your connection fails.
-```cs
-using SpacetimeDB;
-using SpacetimeDB.Types;
+A known bug in the SpacetimeDB Rust client SDK currently causes this callback never to be invoked. [`OnDisconnect`](#callback-ondisconnect) callbacks are invoked instead.
-const string DBNAME = "chat";
+### Callback `OnDisconnect`
+
+```csharp
+class DbConnectionBuilder
+{
+ public DbConnectionBuilder OnDisconnect(Action callback);
+}
+```
-// Connect to a local DB with a fresh identity
-SpacetimeDBClient.instance.Connect(null, "localhost:3000", DBNAME, false);
+Chain a call to `.OnDisconnect(callback)` to your builder to register a callback to run when your `DbConnection` disconnects from the remote module, either as a result of a call to [`Disconnect`](#method-disconnect) or due to an error.
-// Connect to cloud with a fresh identity
-SpacetimeDBClient.instance.Connect(null, "dev.spacetimedb.net", DBNAME, true);
+### Method `WithToken`
-// Connect to cloud using a saved identity from the filesystem, or get a new one and save it
-AuthToken.Init();
-Identity localIdentity;
-SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true);
-SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) {
- AuthToken.SaveToken(authToken);
- localIdentity = identity;
+```csharp
+class DbConnectionBuilder
+{
+ public DbConnectionBuilder WithToken(string token = null);
}
```
-(You should probably also store the returned `Identity` somewhere; see the [`onIdentityReceived`](#event-spacetimedbclientonidentityreceived) event.)
+Chain a call to `.WithToken(token)` to your builder to provide an OpenID Connect compliant JSON Web Token to authenticate with, or to explicitly select an anonymous connection. If this method is not called or `None` is passed, SpacetimeDB will generate a new `Identity` and sign a new private access token for the connection.
-### Event `SpacetimeDBClient.onIdentityReceived`
+### Method `Build`
-```cs
-namespace SpacetimeDB {
-
-class SpacetimeDBClient {
- public event Action onIdentityReceived;
+```csharp
+class DbConnectionBuilder
+{
+ public DbConnection Build();
}
+```
+
+After configuring the connection and registering callbacks, attempt to open the connection.
+
+## Advance the connection and process messages
+
+In the interest of supporting a wide variety of client applications with different execution strategies, the SpacetimeDB SDK allows you to choose when the `DbConnection` spends compute time and processes messages. If you do not arrange for the connection to advance by calling one of these methods, the `DbConnection` will never advance, and no callbacks will ever be invoked.
+| Name | Description |
+|---------------------------------------------|-------------------------------------------------------|
+| [`FrameTick` method](#method-frametick) | Process messages on the main thread without blocking. |
+
+#### Method `FrameTick`
+
+```csharp
+class DbConnection {
+ public void FrameTick();
}
```
-Called when we receive an auth token, [`Identity`](#class-identity) and `Address` from the server. The [`Identity`](#class-identity) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The `Address` is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity).
+`FrameTick` will advance the connection until no work remains or until it is disconnected, then return rather than blocking. Games might arrange for this message to be called every frame.
-To store the auth token to the filesystem, use the static method [`AuthToken.SaveToken`](#static-method-authtokensavetoken). You may also want to store the returned [`Identity`](#class-identity) in a local variable.
+It is not advised to run `FrameTick` on a background thread, since it modifies [`dbConnection.Db`](#property-db). If main thread code is also accessing the `Db`, it may observe data races when `FrameTick` runs on another thread.
-If an existing auth token is used to connect to the database, the same auth token and the identity it came with will be returned verbatim in `onIdentityReceived`.
+(Note that the SDK already does most of the work for parsing messages on a background thread. `FrameTick()` does the minimal amount of work needed to apply updates to the `Db`.)
-```cs
-// Connect to cloud using a saved identity from the filesystem, or get a new one and save it
-AuthToken.Init();
-Identity localIdentity;
-SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true);
-SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) {
- AuthToken.SaveToken(authToken);
- localIdentity = identity;
+## Access tables and reducers
+
+### Property `Db`
+
+```csharp
+class DbConnection
+{
+ public RemoteTables Db;
+ /* other members */
}
```
-### Event `SpacetimeDBClient.onConnect`
+The `Db` property of the `DbConnection` provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache).
-```cs
-namespace SpacetimeDB {
+### Property `Reducers`
-class SpacetimeDBClient {
- public event Action onConnect;
+```csharp
+class DbConnection
+{
+ public RemoteReducers Reducers;
+ /* other members */
}
+```
+The `Reducers` field of the `DbConnection` provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers).
+
+## Interface `IDbContext`
+
+```csharp
+interface IDbContext
+{
+ /* methods */
}
```
-Allows registering delegates to be invoked upon authentication with the database.
+[`DbConnection`](#type-dbconnection), [`EventContext`](#type-eventcontext), [`ReducerEventContext`](#type-reducereventcontext), [`SubscriptionEventContext`](#type-subscriptioneventcontext) and [`ErrorContext`](#type-errorcontext) all implement `IDbContext`. `IDbContext` has methods for inspecting and configuring your connection to the remote database.
-Once this occurs, the SDK is prepared for calls to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe).
+The `IDbContext` interface is implemented by connections and contexts to *every* module - hence why it takes [`DbView`](#method-db) and [`RemoteReducers`](#method-reducers) as type parameters.
-## Subscribe to queries
+| Name | Description |
+|---------------------------------------------------------------|--------------------------------------------------------------------------|
+| [`IRemoteDbContext` interface](#interface-iremotedbcontext) | Module-specific `IDbContext`. |
+| [`Db` method](#method-db) | Provides access to the subscribed view of the remote database's tables. |
+| [`Reducers` method](#method-reducers) | Provides access to reducers exposed by the remote module. |
+| [`Disconnect` method](#method-disconnect) | End the connection. |
+| [Subscribe to queries](#subscribe-to-queries) | Register SQL queries to receive updates about matching rows. |
+| [Read connection metadata](#read-connection-metadata) | Access the connection's `Identity` and `ConnectionId` |
-### Method `SpacetimeDBClient.Subscribe`
+### Interface `IRemoteDbContext`
-```cs
-namespace SpacetimeDB {
+Each module's `module_bindings` exports an interface `IRemoteDbContext` which inherits from `IDbContext`, with the type parameters `DbView` and `RemoteReducers` bound to the types defined for that module. This can be more convenient when creating functions that can be called from any callback for a specific module, but which access the database or invoke reducers, and so must know the type of the `DbView` or `Reducers`.
-class SpacetimeDBClient {
- public void Subscribe(List queries);
-}
+### Method `Db`
+```csharp
+interface IRemoteDbContext
+{
+ public DbView Db { get; }
}
```
-| Argument | Type | Meaning |
-| --------- | -------------- | ---------------------------- |
-| `queries` | `List` | SQL queries to subscribe to. |
+`Db` will have methods to access each table defined by the module.
-Subscribe to a set of queries, to be notified when rows which match those queries are altered.
+#### Example
-`Subscribe` will return an error if called before establishing a connection with the [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect) function. In that case, the queries are not registered.
-
-The `Subscribe` method does not return data directly. `spacetime generate` will generate classes [`SpacetimeDB.Types.{TABLE}`](#class-table) for each table in your module. These classes are used to reecive information from the database. See the section [View Rows of Subscribed Tables](#view-rows-of-subscribed-tables) for more information.
+```csharp
+var conn = ConnectToDB();
-A new call to `Subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache, and [`{TABLE}.OnDelete`](#static-event-tableoninsert) callbacks will be invoked for them.
+// Get a handle to the User table
+var tableHandle = conn.Db.User;
+```
-```cs
-using SpacetimeDB;
-using SpacetimeDB.Types;
+### Method `Reducers`
-void Main()
+```csharp
+interface IRemoteDbContext
{
- AuthToken.Init();
+ public RemoteReducers Reducers { get; }
+}
+```
- SpacetimeDBClient.instance.onConnect += OnConnect;
+`Reducers` will have methods to invoke each reducer defined by the module,
+plus methods for adding and removing callbacks on each of those reducers.
- // Our module contains a table named "Loot"
- Loot.OnInsert += Loot_OnInsert;
+#### Example
- SpacetimeDBClient.instance.Connect(/* ... */);
-}
+```csharp
+var conn = ConnectToDB();
-void OnConnect()
-{
- SpacetimeDBClient.instance.Subscribe(new List {
- "SELECT * FROM Loot"
- });
-}
+// Register a callback to be run every time the SendMessage reducer is invoked
+conn.Reducers.OnSendMessage += Reducer_OnSendMessageEvent;
+```
+
+### Method `Disconnect`
-void Loot_OnInsert(
- Loot loot,
- ReducerEvent? event
-) {
- Console.Log($"Loaded loot {loot.itemType} at coordinates {loot.position}");
+```csharp
+interface IRemoteDbContext
+{
+ public void Disconnect();
}
```
-### Event `SpacetimeDBClient.onSubscriptionApplied`
+Gracefully close the `DbConnection`. Throws an error if the connection is already closed.
-```cs
-namespace SpacetimeDB {
+### Subscribe to queries
-class SpacetimeDBClient {
- public event Action onSubscriptionApplied;
-}
+| Name | Description |
+|---------------------------------------------------------|-------------------------------------------------------------|
+| [`SubscriptionBuilder` type](#type-subscriptionbuilder) | Builder-pattern constructor to register subscribed queries. |
+| [`SubscriptionHandle` type](#type-subscriptionhandle) | Manage an active subscripion. |
-}
-```
+#### Type `SubscriptionBuilder`
-Register a delegate to be invoked when a subscription is registered with the database.
+| Name | Description |
+|----------------------------------------------------------------------------------|-----------------------------------------------------------------|
+| [`ctx.SubscriptionBuilder()` constructor](#constructor-ctxsubscriptionbuilder) | Begin configuring a new subscription. |
+| [`OnApplied` callback](#callback-onapplied) | Register a callback to run when matching rows become available. |
+| [`OnError` callback](#callback-onerror) | Register a callback to run if the subscription fails. |
+| [`Subscribe` method](#method-subscribe) | Finish configuration and subscribe to one or more SQL queries. |
+| [`SubscribeToAllTables` method](#method-subscribetoalltables) | Convenience method to subscribe to the entire database. |
-```cs
-using SpacetimeDB;
+##### Constructor `ctx.SubscriptionBuilder()`
-void OnSubscriptionApplied()
+```csharp
+interface IRemoteDbContext
{
- Console.WriteLine("Now listening on queries.");
+ public SubscriptionBuilder SubscriptionBuilder();
}
+```
+
+Subscribe to queries by calling `ctx.SubscriptionBuilder()` and chaining configuration methods, then calling `.Subscribe(queries)`.
-void Main()
+##### Callback `OnApplied`
+
+```csharp
+class SubscriptionBuilder
{
- // ...initialize...
- SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied;
+ public SubscriptionBuilder OnApplied(Action callback);
}
```
-### Method [`SpacetimeDBClient.OneOffQuery`]
+Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache.
-You may not want to subscribe to a query, but instead want to run a query once and receive the results immediately via a `Task` result:
+##### Callback `OnError`
```csharp
-// Query all Messages from the sender "bob"
-SpacetimeDBClient.instance.OneOffQuery("WHERE sender = \"bob\"");
+class SubscriptionBuilder
+{
+ public SubscriptionBuilder OnError(Action callback);
+}
```
-## View rows of subscribed tables
+Register a callback to run if the subscription is rejected or unexpectedly terminated by the server. This is most frequently caused by passing an invalid query to [`Subscribe`](#method-subscribe).
-The SDK maintains a local view of the database called the "client cache". This cache contains whatever rows are selected via a call to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe). These rows are represented in the SpacetimeDB .Net SDK as instances of [`SpacetimeDB.Types.{TABLE}`](#class-table).
-ONLY the rows selected in a [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe) call will be available in the client cache. All operations in the client sdk operate on these rows exclusively, and have no information about the state of the rest of the database.
+##### Method `Subscribe`
-In particular, SpacetimeDB does not support foreign key constraints. This means that if you are using a column as a foreign key, SpacetimeDB will not automatically bring in all of the rows that key might reference. You will need to manually subscribe to all tables you need information from.
-
-To optimize network performance, prefer selecting as few rows as possible in your [`Subscribe`](#method-spacetimedbclientsubscribe) query. Processes that need to view the entire state of the database are better run inside the database -- that is, inside modules.
+```csharp
+class SubscriptionBuilder
+{
+ public SubscriptionHandle Subscribe(string[] querySqls);
+}
+```
-### Class `{TABLE}`
+Subscribe to a set of queries. `queries` should be an array of SQL query strings.
-For each table defined by a module, `spacetime generate` will generate a class [`SpacetimeDB.Types.{TABLE}`](#class-table) whose name is that table's name converted to `PascalCase`. The generated class contains a property for each of the table's columns, whose names are the column names converted to `camelCase`. It also contains various static events and methods.
+See [the SpacetimeDB SQL Reference](/docs/sql#subscriptions) for information on the queries SpacetimeDB supports as subscriptions.
-Static Methods:
+##### Method `SubscribeToAllTables`
-- [`{TABLE}.Iter()`](#static-method-tableiter) iterates all subscribed rows in the client cache.
-- [`{TABLE}.FilterBy{COLUMN}(value)`](#static-method-tablefilterbycolumn) filters subscribed rows in the client cache by a column value.
-- [`{TABLE}.FindBy{COLUMN}(value)`](#static-method-tablefindbycolumn) finds a subscribed row in the client cache by a unique column value.
-- [`{TABLE}.Count()`](#static-method-tablecount) counts the number of subscribed rows in the client cache.
+```csharp
+class SubscriptionBuilder
+{
+ public void SubscribeToAllTables();
+}
+```
-Static Events:
+Subscribe to all rows from all public tables. This method is provided as a convenience for simple clients. The subscription initiated by `SubscribeToAllTables` cannot be canceled after it is initiated. You should [`subscribe` to specific queries](#method-subscribe) if you need fine-grained control over the lifecycle of your subscriptions.
-- [`{TABLE}.OnInsert`](#static-event-tableoninsert) is called when a row is inserted into the client cache.
-- [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete) is called when a row is about to be removed from the client cache.
-- If the table has a primary key attribute, [`{TABLE}.OnUpdate`](#static-event-tableonupdate) is called when a row is updated.
-- [`{TABLE}.OnDelete`](#static-event-tableondelete) is called while a row is being removed from the client cache. You should almost always use [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete) instead.
+#### Type `SubscriptionHandle`
-Note that it is not possible to directly insert into the database from the client SDK! All insertion validation should be performed inside serverside modules for security reasons. You can instead [invoke reducers](#observe-and-invoke-reducers), which run code inside the database that can insert rows for you.
+A `SubscriptionHandle` represents a subscribed query or a group of subscribed queries.
-#### Static Method `{TABLE}.Iter`
+The `SubscriptionHandle` does not contain or provide access to the subscribed rows. Subscribed rows of all subscriptions by a connection are contained within that connection's [`ctx.Db`](#property-db). See [Access the client cache](#access-the-client-cache).
-```cs
-namespace SpacetimeDB.Types {
+| Name | Description |
+|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
+| [`IsEnded` property](#property-isended) | Determine whether the subscription has ended. |
+| [`IsActive` property](#property-isactive) | Determine whether the subscription is active and its matching rows are present in the client cache. |
+| [`Unsubscribe` method](#method-unsubscribe) | Discard a subscription. |
+| [`UnsubscribeThen` method](#method-unsubscribethen) | Discard a subscription, and register a callback to run when its matching rows are removed from the client cache. |
-class TABLE {
- public static IEnumerable
Iter();
-}
+##### Property `IsEnded`
+```csharp
+class SubscriptionHandle
+{
+ public bool IsEnded;
}
```
-Iterate over all the subscribed rows in the table. This method is only available after [`SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied) has occurred.
+True if this subscription has been terminated due to an unsubscribe call or an error.
-When iterating over rows and filtering for those containing a particular column, [`{TABLE}.FilterBy{COLUMN}`](#static-method-tablefilterbycolumn) and [`{TABLE}.FindBy{COLUMN}`](#static-method-tablefindbycolumn) will be more efficient, so prefer those when possible.
+##### Property `IsActive`
-```cs
-using SpacetimeDB;
-using SpacetimeDB.Types;
-
-SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => {
- SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" });
-};
-SpacetimeDBClient.instance.onSubscriptionApplied += () => {
- // Will print a line for each `User` row in the database.
- foreach (var user in User.Iter()) {
- Console.WriteLine($"User: {user.Name}");
- }
-};
-SpacetimeDBClient.instance.connect(/* ... */);
+```csharp
+class SubscriptionHandle
+{
+ public bool IsActive;
+}
```
-#### Static Method `{TABLE}.FilterBy{COLUMN}`
-
-```cs
-namespace SpacetimeDB.Types {
+True if this subscription has been applied and has not yet been unsubscribed.
-class TABLE {
- public static IEnumerable FilterBySender(COLUMNTYPE value);
-}
+##### Method `Unsubscribe`
+```csharp
+class SubscriptionHandle
+{
+ public void Unsubscribe();
}
```
-For each column of a table, `spacetime generate` generates a static method on the [table class](#class-table) to filter subscribed rows where that column matches a requested value.
+Terminate this subscription, causing matching rows to be removed from the client cache. Any rows removed from the client cache this way will have [`OnDelete` callbacks](#callback-ondelete) run for them.
-These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `PascalCase`. The method's return type is an `IEnumerable` over the [table class](#class-table).
+Unsubscribing is an asynchronous operation. Matching rows are not removed from the client cache immediately. Use [`UnsubscribeThen`](#method-unsubscribethen) to run a callback once the unsubscribe operation is completed.
-#### Static Method `{TABLE}.FindBy{COLUMN}`
+Returns an error if the subscription has already ended, either due to a previous call to `Unsubscribe` or [`UnsubscribeThen`](#method-unsubscribethen), or due to an error.
-```cs
-namespace SpacetimeDB.Types {
-
-class TABLE {
- // If the column has a #[unique] or #[primarykey] constraint
- public static TABLE? FindBySender(COLUMNTYPE value);
-}
+##### Method `UnsubscribeThen`
+```csharp
+class SubscriptionHandle
+{
+ public void UnsubscribeThen(Action? onEnded);
}
```
-For each unique column of a table (those annotated `#[unique]` or `#[primarykey]`), `spacetime generate` generates a static method on the [table class](#class-table) to seek a subscribed row where that column matches a requested value.
+Terminate this subscription, and run the `onEnded` callback when the subscription is ended and its matching rows are removed from the client cache. Any rows removed from the client cache this way will have [`OnDelete` callbacks](#callback-ondelete) run for them.
-These methods are named `findBy{COLUMN}`, where `{COLUMN}` is the column name converted to `PascalCase`. Those methods return a single instance of the [table class](#class-table) if a row is found, or `null` if no row matches the query.
+Returns an error if the subscription has already ended, either due to a previous call to [`Unsubscribe`](#method-unsubscribe) or `UnsubscribeThen`, or due to an error.
-#### Static Method `{TABLE}.Count`
+### Read connection metadata
-```cs
-namespace SpacetimeDB.Types {
-
-class TABLE {
- public static int Count();
-}
+#### Property `Identity`
+```csharp
+interface IDbContext
+{
+ public Identity? Identity { get; }
}
```
-Return the number of subscribed rows in the table, or 0 if there is no active connection.
+Get the `Identity` with which SpacetimeDB identifies the connection. This method returns null if the connection was initiated anonymously and the newly-generated `Identity` has not yet been received, i.e. if called before the [`OnConnect` callback](#callback-onconnect) is invoked.
-```cs
-using SpacetimeDB;
-using SpacetimeDB.Types;
+#### Property `ConnectionId`
-SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => {
- SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" });
-};
-SpacetimeDBClient.instance.onSubscriptionApplied += () => {
- Console.WriteLine($"There are {User.Count()} users in the database.");
-};
-SpacetimeDBClient.instance.connect(/* ... */);
+```csharp
+interface IDbContext
+{
+ public ConnectionId ConnectionId { get; }
+}
```
-#### Static Event `{TABLE}.OnInsert`
+Get the [`ConnectionId`](#type-connectionid) with which SpacetimeDB identifies the connection.
-```cs
-namespace SpacetimeDB.Types {
-
-class TABLE {
- public delegate void InsertEventHandler(
- TABLE insertedValue,
- ReducerEvent? dbEvent
- );
- public static event InsertEventHandler OnInsert;
-}
+#### Property `IsActive`
+```csharp
+interface IDbContext
+{
+ public bool IsActive { get; }
}
```
-Register a delegate for when a subscribed row is newly inserted into the database.
+`true` if the connection has not yet disconnected. Note that a connection `IsActive` when it is constructed, before its [`OnConnect` callback](#callback-onconnect) is invoked.
-The delegate takes two arguments:
+## Type `EventContext`
-- A [`{TABLE}`](#class-table) instance with the data of the inserted row
-- A [`ReducerEvent?`], which contains the data of the reducer that inserted the row, or `null` if the row is being inserted while initializing a subscription.
+An `EventContext` is an [`IDbContext`](#interface-idbcontext) augmented with an [`Event`](#record-event) property. `EventContext`s are passed as the first argument to row callbacks [`OnInsert`](#callback-oninsert), [`OnDelete`](#callback-ondelete) and [`OnUpdate`](#callback-onupdate).
-```cs
-using SpacetimeDB;
-using SpacetimeDB.Types;
+| Name | Description |
+|-------------------------------------------|---------------------------------------------------------------|
+| [`Event` property](#property-event) | Enum describing the cause of the current row callback. |
+| [`Db` property](#property-db) | Provides access to the client cache. |
+| [`Reducers` property](#property-reducers) | Allows requesting reducers run on the remote database. |
+| [`Event` record](#record-event) | Possible events which can cause a row callback to be invoked. |
-/* initialize, subscribe to table User... */
+### Property `Event`
-User.OnInsert += (User user, ReducerEvent? reducerEvent) => {
- if (reducerEvent == null) {
- Console.WriteLine($"New user '{user.Name}' received during subscription update.");
- } else {
- Console.WriteLine($"New user '{user.Name}' inserted by reducer {reducerEvent.Reducer}.");
- }
-};
+```csharp
+class EventContext {
+ public readonly Event Event;
+ /* other fields */
+}
```
-#### Static Event `{TABLE}.OnBeforeDelete`
+The [`Event`](#record-event) contained in the `EventContext` describes what happened to cause the current row callback to be invoked.
-```cs
-namespace SpacetimeDB.Types {
-
-class TABLE {
- public delegate void DeleteEventHandler(
- TABLE deletedValue,
- ReducerEvent dbEvent
- );
- public static event DeleteEventHandler OnBeforeDelete;
-}
+### Property `Db`
+```csharp
+class EventContext {
+ public RemoteTables Db;
+ /* other fields */
}
```
-Register a delegate for when a subscribed row is about to be deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked for each of those rows before any of them is deleted.
+The `Db` property of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache).
+
+### Field `Reducers`
-The delegate takes two arguments:
+```csharp
+class EventContext {
+ public RemoteReducers Reducers;
+ /* other fields */
+}
+```
-- A [`{TABLE}`](#class-table) instance with the data of the deleted row
-- A [`ReducerEvent`](#class-reducerevent), which contains the data of the reducer that deleted the row.
+The `Reducers` property of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers).
-This event should almost always be used instead of [`OnDelete`](#static-event-tableondelete). This is because often, many rows will be deleted at once, and `OnDelete` can be invoked in an arbitrary order on these rows. This means that data related to a row may already be missing when `OnDelete` is called. `OnBeforeDelete` does not have this problem.
+### Record `Event`
-```cs
-using SpacetimeDB;
-using SpacetimeDB.Types;
+| Name | Description |
+|-------------------------------------------------------------|--------------------------------------------------------------------------|
+| [`Reducer` variant](#variant-reducer) | A reducer ran in the remote database. |
+| [`SubscribeApplied` variant](#variant-subscribeapplied) | A new subscription was applied to the client cache. |
+| [`UnsubscribeApplied` variant](#variant-unsubscribeapplied) | A previous subscription was removed from the client cache after a call to [`Unsubscribe`](#method-unsubscribe). |
+| [`SubscribeError` variant](#variant-subscribeerror) | A previous subscription was removed from the client cache due to an error. |
+| [`UnknownTransaction` variant](#variant-unknowntransaction) | A transaction ran in the remote database, but was not attributed to a known reducer. |
+| [`ReducerEvent` record](#record-reducerevent) | Metadata about a reducer run. Contained in a [`Reducer` event](#variant-reducer) and [`ReducerEventContext`](#type-reducereventcontext). |
+| [`Status` record](#record-status) | Completion status of a reducer run. |
+| [`Reducer` record](#record-reducer) | Module-specific generated record with a variant for each reducer defined by the module. |
-/* initialize, subscribe to table User... */
+#### Variant `Reducer`
-User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => {
- Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}.");
-};
+```csharp
+record Event
+{
+ public record Reducer(ReducerEvent ReducerEvent) : Event;
+}
```
-#### Static Event `{TABLE}.OnDelete`
+Event when we are notified that a reducer ran in the remote module. The [`ReducerEvent`](#record-reducerevent) contains metadata about the reducer run, including its arguments and termination [`Status`](#record-status).
-```cs
-namespace SpacetimeDB.Types {
+This event is passed to row callbacks resulting from modifications by the reducer.
-class TABLE {
- public delegate void DeleteEventHandler(
- TABLE deletedValue,
- SpacetimeDB.ReducerEvent dbEvent
- );
- public static event DeleteEventHandler OnDelete;
-}
+#### Variant `SubscribeApplied`
+```csharp
+record Event
+{
+ public record SubscribeApplied : Event;
}
```
-Register a delegate for when a subscribed row is being deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked on those rows in arbitrary order, and data for some rows may already be missing when it is invoked. For this reason, prefer the event [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete).
-
-The delegate takes two arguments:
+Event when our subscription is applied and its rows are inserted into the client cache.
-- A [`{TABLE}`](#class-table) instance with the data of the deleted row
-- A [`ReducerEvent`](#class-reducerevent), which contains the data of the reducer that deleted the row.
+This event is passed to [row `OnInsert` callbacks](#callback-oninsert) resulting from the new subscription.
-```cs
-using SpacetimeDB;
-using SpacetimeDB.Types;
+#### Variant `UnsubscribeApplied`
-/* initialize, subscribe to table User... */
-
-User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => {
- Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}.");
-};
+```csharp
+record Event
+{
+ public record UnsubscribeApplied : Event;
+}
```
-#### Static Event `{TABLE}.OnUpdate`
+Event when our subscription is removed after a call to [`SubscriptionHandle.Unsubscribe`](#method-unsubscribe) or [`SubscriptionHandle.UnsubscribeTthen`](#method-unsubscribethen) and its matching rows are deleted from the client cache.
-```cs
-namespace SpacetimeDB.Types {
+This event is passed to [row `OnDelete` callbacks](#callback-ondelete) resulting from the subscription ending.
-class TABLE {
- public delegate void UpdateEventHandler(
- TABLE oldValue,
- TABLE newValue,
- ReducerEvent dbEvent
- );
- public static event UpdateEventHandler OnUpdate;
-}
+#### Variant `SubscribeError`
+```csharp
+record Event
+{
+ public record SubscribeError(Exception Exception) : Event;
}
```
-Register a delegate for when a subscribed row is being updated. This event is only available if the row has a column with the `#[primary_key]` attribute.
+Event when a subscription ends unexpectedly due to an error.
-The delegate takes three arguments:
+This event is passed to [row `OnDelete` callbacks](#callback-ondelete) resulting from the subscription ending.
-- A [`{TABLE}`](#class-table) instance with the old data of the updated row
-- A [`{TABLE}`](#class-table) instance with the new data of the updated row
-- A [`ReducerEvent`](#class-reducerevent), which contains the data of the reducer that updated the row.
+#### Variant `UnknownTransaction`
-```cs
-using SpacetimeDB;
-using SpacetimeDB.Types;
-
-/* initialize, subscribe to table User... */
-
-User.OnUpdate += (User oldUser, User newUser, ReducerEvent reducerEvent) => {
- Debug.Assert(oldUser.UserId == newUser.UserId, "Primary key never changes in an update");
-
- Console.WriteLine($"User with ID {oldUser.UserId} had name changed "+
- $"from '{oldUser.Name}' to '{newUser.Name}' by reducer {reducerEvent.Reducer}.");
-};
+```csharp
+record Event
+{
+ public record UnknownTransaction : Event;
+}
```
-## Observe and invoke reducers
+Event when we are notified of a transaction in the remote module which we cannot associate with a known reducer. This may be an ad-hoc SQL query or a reducer for which we do not have bindings.
-"Reducer" is SpacetimeDB's name for the stored procedures that run in modules inside the database. You can invoke reducers from a connected client SDK, and also receive information about which reducers are running.
+This event is passed to [row callbacks](#callback-oninsert) resulting from modifications by the transaction.
-`spacetime generate` generates a class [`SpacetimeDB.Types.Reducer`](#class-reducer) that contains methods and events for each reducer defined in a module. To invoke a reducer, use the method [`Reducer.{REDUCER}`](#static-method-reducerreducer) generated for it. To receive a callback each time a reducer is invoked, use the static event [`Reducer.On{REDUCER}`](#static-event-reduceronreducer).
+### Record `ReducerEvent`
-### Class `Reducer`
+```csharp
+record ReducerEvent(
+ Timestamp Timestamp,
+ Status Status,
+ Identity CallerIdentity,
+ ConnectionId? CallerConnectionId,
+ U128? EnergyConsumed,
+ R Reducer
+)
+```
-```cs
-namespace SpacetimeDB.Types {
+A `ReducerEvent` contains metadata about a reducer run.
-class Reducer {}
+### Record `Status`
-}
+```csharp
+record Status : TaggedEnum<(
+ Unit Committed,
+ string Failed,
+ Unit OutOfEnergy
+)>;
```
-This class contains a static method and event for each reducer defined in a module.
+
-#### Static Method `Reducer.{REDUCER}`
+| Name | Description |
+|-----------------------------------------------|-----------------------------------------------------|
+| [`Committed` variant](#variant-committed) | The reducer ran successfully. |
+| [`Failed` variant](#variant-failed) | The reducer errored. |
+| [`OutOfEnergy` variant](#variant-outofenergy) | The reducer was aborted due to insufficient energy. |
-```cs
-namespace SpacetimeDB.Types {
-class Reducer {
+#### Variant `Committed`
-/* void {REDUCER_NAME}(...ARGS...) */
+The reducer returned successfully and its changes were committed into the database state. An [`Event.Reducer`](#variant-reducer) passed to a row callback must have this status in its [`ReducerEvent`](#record-reducerevent).
-}
-}
-```
+#### Variant `Failed`
-For each reducer defined by a module, `spacetime generate` generates a static method which sends a request to the database to invoke that reducer. The generated function's name is the reducer's name converted to `PascalCase`.
+The reducer returned an error, panicked, or threw an exception. The record payload is the stringified error message. Formatting of the error message is unstable and subject to change, so clients should use it only as a human-readable diagnostic, and in particular should not attempt to parse the message.
-Reducers don't run immediately! They run as soon as the request reaches the database. Don't assume data inserted by a reducer will be available immediately after you call this method.
+#### Variant `OutOfEnergy`
-For reducers which accept a `ReducerContext` as their first argument, the `ReducerContext` is not included in the generated function's argument list.
+The reducer was aborted due to insufficient energy balance of the module owner.
-For example, if we define a reducer in Rust as follows:
+### Record `Reducer`
-```rust
-#[spacetimedb(reducer)]
-pub fn set_name(
- ctx: ReducerContext,
- user_id: u64,
- name: String
-) -> Result<(), Error>;
-```
+The module bindings contains an record `Reducer` with a variant for each reducer defined by the module. Each variant has a payload containing the arguments to the reducer.
-The following C# static method will be generated:
+## Type `ReducerEventContext`
-```cs
-namespace SpacetimeDB.Types {
-class Reducer {
+A `ReducerEventContext` is an [`IDbContext`](#interface-idbcontext) augmented with an [`Event`](#record-reducerevent) property. `ReducerEventContext`s are passed as the first argument to [reducer callbacks](#observe-and-invoke-reducers).
-public static void SendMessage(UInt64 userId, string name);
+| Name | Description |
+|-------------------------------------------|---------------------------------------------------------------------|
+| [`Event` property](#property-event) | [`ReducerEvent`](#record-reducerevent) containing reducer metadata. |
+| [`Db` property](#property-db) | Provides access to the client cache. |
+| [`Reducers` property](#property-reducers) | Allows requesting reducers run on the remote database. |
-}
+### Property `Event`
+
+```csharp
+class ReducerEventContext {
+ public readonly ReducerEvent Event;
+ /* other fields */
}
```
-#### Static Event `Reducer.On{REDUCER}`
+The [`ReducerEvent`](#record-reducerevent) contained in the `ReducerEventContext` has metadata about the reducer which ran.
-```cs
-namespace SpacetimeDB.Types {
-class Reducer {
+### Property `Db`
-public delegate void /*{REDUCER}*/Handler(ReducerEvent reducerEvent, /* {ARGS...} */);
-
-public static event /*{REDUCER}*/Handler On/*{REDUCER}*/Event;
-
-}
+```csharp
+class ReducerEventContext {
+ public RemoteTables Db;
+ /* other fields */
}
```
-For each reducer defined by a module, `spacetime generate` generates an event to run each time the reducer is invoked. The generated functions are named `on{REDUCER}Event`, where `{REDUCER}` is the reducer's name converted to `PascalCase`.
-
-The first argument to the event handler is an instance of [`SpacetimeDB.Types.ReducerEvent`](#class-reducerevent) describing the invocation -- its timestamp, arguments, and whether it succeeded or failed. The remaining arguments are the arguments passed to the reducer. Reducers cannot have return values, so no return value information is included.
+The `Db` property of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache).
-For example, if we define a reducer in Rust as follows:
+### Property `Reducers`
-```rust
-#[spacetimedb(reducer)]
-pub fn set_name(
- ctx: ReducerContext,
- user_id: u64,
- name: String
-) -> Result<(), Error>;
+```csharp
+class ReducerEventContext {
+ public RemoteReducers Reducers;
+ /* other fields */
+}
```
-The following C# static method will be generated:
+The `Reducers` property of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers).
-```cs
-namespace SpacetimeDB.Types {
-class Reducer {
+## Type `SubscriptionEventContext`
-public delegate void SetNameHandler(
- ReducerEvent reducerEvent,
- UInt64 userId,
- string name
-);
-public static event SetNameHandler OnSetNameEvent;
+A `SubscriptionEventContext` is an [`IDbContext`](#interface-idbcontext). Unlike the other context types, `SubscriptionEventContext` doesn't have an `Event` property. `SubscriptionEventContext`s are passed to subscription [`OnApplied`](#callback-onapplied) and [`UnsubscribeThen`](#method-unsubscribethen) callbacks.
-}
+| Name | Description |
+|-------------------------------------------|------------------------------------------------------------|
+| [`Db` property](#property-db) | Provides access to the client cache. |
+| [`Reducers` property](#property-reducers) | Allows requesting reducers run on the remote database. |
+
+### Property `Db`
+
+```csharp
+class SubscriptionEventContext {
+ public RemoteTables Db;
+ /* other fields */
}
```
-Which can be used as follows:
+The `Db` property of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache).
-```cs
-/* initialize, wait for onSubscriptionApplied... */
+### Property `Reducers`
-Reducer.SetNameHandler += (
- ReducerEvent reducerEvent,
- UInt64 userId,
- string name
-) => {
- if (reducerEvent.Status == ClientApi.Event.Types.Status.Committed) {
- Console.WriteLine($"User with id {userId} set name to {name}");
- } else if (reducerEvent.Status == ClientApi.Event.Types.Status.Failed) {
- Console.WriteLine(
- $"User with id {userId} failed to set name to {name}:"
- + reducerEvent.ErrMessage
- );
- } else if (reducerEvent.Status == ClientApi.Event.Types.Status.OutOfEnergy) {
- Console.WriteLine(
- $"User with id {userId} failed to set name to {name}:"
- + "Invoker ran out of energy"
- );
- }
-};
-Reducer.SetName(USER_ID, NAME);
+```csharp
+class SubscriptionEventContext {
+ public RemoteReducers Reducers;
+ /* other fields */
+}
```
-### Class `ReducerEvent`
+The `Reducers` property of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers).
-`spacetime generate` defines an class `ReducerEvent` containing an enum `ReducerType` with a variant for each reducer defined by a module. The variant's name will be the reducer's name converted to `PascalCase`.
+## Type `ErrorContext`
-For example, the example project shown in the Rust Module quickstart will generate the following (abridged) code.
+An `ErrorContext` is an [`IDbContext`](#interface-idbcontext) augmented with an `Event` property. `ErrorContext`s are to connections' [`OnDisconnect`](#callback-ondisconnect) and [`OnConnectError`](#callback-onconnecterror) callbacks, and to subscriptions' [`OnError`](#callback-onerror) callbacks.
-```cs
-namespace SpacetimeDB.Types {
+| Name | Description |
+|-------------------------------------------|--------------------------------------------------------|
+| [`Event` property](#property-event) | The error which caused the current error callback. |
+| [`Db` property](#property-db) | Provides access to the client cache. |
+| [`Reducers` property](#property-reducers) | Allows requesting reducers run on the remote database. |
-public enum ReducerType
-{
- /* A member for each reducer in the module, with names converted to PascalCase */
- None,
- SendMessage,
- SetName,
-}
-public partial class SendMessageArgsStruct
-{
- /* A member for each argument of the reducer SendMessage, with names converted to PascalCase. */
- public string Text;
-}
-public partial class SetNameArgsStruct
-{
- /* A member for each argument of the reducer SetName, with names converted to PascalCase. */
- public string Name;
-}
-public partial class ReducerEvent : ReducerEventBase {
- // Which reducer was invoked
- public ReducerType Reducer { get; }
- // If event.Reducer == ReducerType.SendMessage, the arguments
- // sent to the SendMessage reducer. Otherwise, accesses will
- // throw a runtime error.
- public SendMessageArgsStruct SendMessageArgs { get; }
- // If event.Reducer == ReducerType.SetName, the arguments
- // passed to the SetName reducer. Otherwise, accesses will
- // throw a runtime error.
- public SetNameArgsStruct SetNameArgs { get; }
- /* Additional information, present on any ReducerEvent */
- // The name of the reducer.
- public string ReducerName { get; }
- // The timestamp of the reducer invocation inside the database.
- public ulong Timestamp { get; }
- // The identity of the client that invoked the reducer.
- public SpacetimeDB.Identity Identity { get; }
- // Whether the reducer succeeded, failed, or ran out of energy.
- public ClientApi.Event.Types.Status Status { get; }
- // If event.Status == Status.Failed, the error message returned from inside the module.
- public string ErrMessage { get; }
-}
+### Property `Event`
+```csharp
+class SubscriptionEventContext {
+ public readonly Exception Event;
+ /* other fields */
}
```
-#### Enum `Status`
-
-```cs
-namespace ClientApi {
-public sealed partial class Event {
-public static partial class Types {
-
-public enum Status {
- Committed = 0,
- Failed = 1,
- OutOfEnergy = 2,
-}
+### Property `Db`
-}
-}
+```csharp
+class ErrorContext {
+ public RemoteTables Db;
+ /* other fields */
}
```
-An enum whose variants represent possible reducer completion statuses of a reducer invocation.
+The `Db` property of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache).
-##### Variant `Status.Committed`
+### Property `Reducers`
-The reducer finished successfully, and its row changes were committed to the database.
-
-##### Variant `Status.Failed`
+```csharp
+class ErrorContext {
+ public RemoteReducers Reducers;
+ /* other fields */
+}
+```
-The reducer failed, either by panicking or returning a `Err`.
+The `Reducers` property of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers).
-##### Variant `Status.OutOfEnergy`
+## Access the client cache
-The reducer was canceled because the module owner had insufficient energy to allow it to run to completion.
+All [`IDbContext`](#interface-idbcontext) implementors, including [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext), have `.Db` properties, which in turn have methods for accessing tables in the client cache.
-## Identity management
+Each table defined by a module has an accessor method, whose name is the table name converted to `snake_case`, on this `.Db` property. The table accessor methods return table handles which inherit from [`RemoteTableHandle`](#type-remotetablehandle) and have methods for searching by index.
-### Class `AuthToken`
+| Name | Description |
+|-------------------------------------------------------------------|---------------------------------------------------------------------------------|
+| [`RemoteTableHandle`](#type-remotetablehandle) | Provides access to subscribed rows of a specific table within the client cache. |
+| [Unique constraint index access](#unique-constraint-index-access) | Seek a subscribed row by the value in its unique or primary key column. |
+| [BTree index access](#btree-index-access) | Seek subscribed rows by the value in its indexed column. |
-The AuthToken helper class handles creating and saving SpacetimeDB identity tokens in the filesystem.
+### Type `RemoteTableHandle`
-#### Static Method `AuthToken.Init`
+Implemented by all table handles.
-```cs
-namespace SpacetimeDB {
+| Name | Description |
+|-----------------------------------------------|------------------------------------------------------------------------------|
+| [`Row` type parameter](#type-row) | The type of rows in the table. |
+| [`Count` property](#property-count) | The number of subscribed rows in the table. |
+| [`Iter` method](#method-iter) | Iterate over all subscribed rows in the table. |
+| [`OnInsert` callback](#callback-oninsert) | Register a callback to run whenever a row is inserted into the client cache. |
+| [`OnDelete` callback](#callback-ondelete) | Register a callback to run whenever a row is deleted from the client cache. |
+| [`OnUpdate` callback](#callback-onupdate) | Register a callback to run whenever a subscribed row is replaced with a new version. |
-class AuthToken {
- public static void Init(
- string configFolder = ".spacetime_csharp_sdk",
- string configFile = "settings.ini",
- string? configRoot = null
- );
-}
+#### Type `Row`
+```csharp
+class RemoteTableHandle
+{
+ /* members */
}
```
-Creates a file `$"{configRoot}/{configFolder}/{configFile}"` to store tokens.
-If no arguments are passed, the default is `"%HOME%/.spacetime_csharp_sdk/settings.ini"`.
+The type of rows in the table.
-| Argument | Type | Meaning |
-| -------------- | -------- | ---------------------------------------------------------------------------------- |
-| `configFolder` | `string` | The folder to store the config file in. Default is `"spacetime_csharp_sdk"`. |
-| `configFile` | `string` | The name of the config file. Default is `"settings.ini"`. |
-| `configRoot` | `string` | The root folder to store the config file in. Default is the user's home directory. |
+#### Property `Count`
-#### Static Property `AuthToken.Token`
+```csharp
+class RemoteTableHandle
+{
+ public int Count;
+}
+```
-```cs
-namespace SpacetimeDB {
+The number of rows of this table resident in the client cache, i.e. the total number which match any subscribed query.
-class AuthToken {
- public static string? Token { get; }
-}
+#### Method `Iter`
+```csharp
+class RemoteTableHandle
+{
+ public IEnumerable Iter();
}
```
-The auth token stored on the filesystem, if one exists.
-
-#### Static Method `AuthToken.SaveToken`
-
-```cs
-namespace SpacetimeDB {
+An iterator over all the subscribed rows in the client cache, i.e. those which match any subscribed query.
-class AuthToken {
- public static void SaveToken(string token);
-}
+#### Callback `OnInsert`
+```csharp
+class RemoteTableHandle
+{
+ public delegate void RowEventHandler(EventContext context, Row row);
+ public event RowEventHandler? OnInsert;
}
```
-Save a token to the filesystem.
+The `OnInsert` callback runs whenever a new row is inserted into the client cache, either when applying a subscription or being notified of a transaction. The passed [`EventContext`](#type-eventcontext) contains an [`Event`](#record-event) which can identify the change which caused the insertion, and also allows the callback to interact with the connection, inspect the client cache and invoke reducers. Newly registered or canceled callbacks do not take effect until the following event.
+
+See [the quickstart](/docs/sdks/c-sharp/quickstart#register-callbacks) for examples of regstering and unregistering row callbacks.
-### Class `Identity`
+#### Callback `OnDelete`
-```cs
-namespace SpacetimeDB
+```csharp
+class RemoteTableHandle
{
- public struct Identity : IEquatable
- {
- public byte[] Bytes { get; }
- public static Identity From(byte[] bytes);
- public bool Equals(Identity other);
- public static bool operator ==(Identity a, Identity b);
- public static bool operator !=(Identity a, Identity b);
- }
+ public delegate void RowEventHandler(EventContext context, Row row);
+ public event RowEventHandler? OnDelete;
}
```
-A unique public identifier for a user of a database.
+The `OnDelete` callback runs whenever a previously-resident row is deleted from the client cache. Newly registered or canceled callbacks do not take effect until the following event.
-
+See [the quickstart](/docs/sdks/c-sharp/quickstart#register-callbacks) for examples of regstering and unregistering row callbacks.
-Columns of type `Identity` inside a module will be represented in the C# SDK as properties of type `byte[]`. `Identity` is essentially just a wrapper around `byte[]`, and you can use the `Bytes` property to get a `byte[]` that can be used to filter tables and so on.
+#### Callback `OnUpdate`
-```cs
-namespace SpacetimeDB
+```csharp
+class RemoteTableHandle
{
- public struct Address : IEquatable
- {
- public byte[] Bytes { get; }
- public static Address? From(byte[] bytes);
- public bool Equals(Address other);
- public static bool operator ==(Address a, Address b);
- public static bool operator !=(Address a, Address b);
- }
+ public delegate void RowEventHandler(EventContext context, Row row);
+ public event RowEventHandler? OnUpdate;
}
```
-An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity).
-
-## Customizing logging
+The `OnUpdate` callback runs whenever an already-resident row in the client cache is updated, i.e. replaced with a new row that has the same primary key. The table must have a primary key for callbacks to be triggered. Newly registered or canceled callbacks do not take effect until the following event.
-The SpacetimeDB C# SDK performs internal logging.
+See [the quickstart](/docs/sdks/c-sharp/quickstart#register-callbacks) for examples of regstering and unregistering row callbacks.
-A default logger is set up automatically for you - a [`ConsoleLogger`](#class-consolelogger) for C# projects and [`UnityDebugLogger`](#class-unitydebuglogger) for Unity projects.
+### Unique constraint index access
-If you want to redirect SDK logs elsewhere, you can inherit from the [`ISpacetimeDBLogger`](#interface-ispacetimedblogger) and assign an instance of your class to the `SpacetimeDB.Logger.Current` static property.
+For each unique constraint on a table, its table handle has a property which is a unique index handle and whose name is the unique column name. This unique index handle has a method `.Find(Column value)`. If a `Row` with `value` in the unique column is resident in the client cache, `.Find` returns it. Otherwise it returns null.
-### Interface `ISpacetimeDBLogger`
-```cs
-namespace SpacetimeDB
-{
+#### Example
-public interface ISpacetimeDBLogger
+Given the following module-side `User` definition:
+```csharp
+[Table(Name = "User", Public = true)]
+public partial class User
{
- void Log(string message);
- void LogError(string message);
- void LogWarning(string message);
- void LogException(Exception e);
+ [Unique] // Or [PrimaryKey]
+ public Identity Identity;
+ ..
}
+```
-}
+a client would lookup a user as follows:
+```csharp
+User? FindUser(RemoteTables tables, Identity id) => tables.User.Identity.Find(id);
```
-This interface provides methods that are invoked when the SpacetimeDB C# SDK needs to log at various log levels. You can create custom implementations if needed to integrate with existing logging solutions.
+### BTree index access
-### Class `ConsoleLogger`
+For each btree index defined on a remote table, its corresponding table handle has a property which is a btree index handle and whose name is the name of the index. This index handle has a method `IEnumerable Filter(Column value)` which will return `Row`s with `value` in the indexed `Column`, if there are any in the cache.
-```cs
-namespace SpacetimeDB {
+#### Example
-public class ConsoleLogger : ISpacetimeDBLogger {}
+Given the following module-side `Player` definition:
+```csharp
+[Table(Name = "Player", Public = true)]
+public partial class Player
+{
+ [PrimaryKey]
+ public Identity id;
+ [Index.BTree(Name = "Level")]
+ public uint level;
+ ..
}
```
-An `ISpacetimeDBLogger` implementation for regular .NET applications, using `Console.Write` when logs are received.
+a client would count the number of `Player`s at a certain level as follows:
+```csharp
+int CountPlayersAtLevel(RemoteTables tables, uint level) => tables.Player.Level.Filter(level).Count();
+```
-### Class `UnityDebugLogger`
+## Observe and invoke reducers
-```cs
-namespace SpacetimeDB {
+All [`IDbContext`](#interface-idbcontext) implementors, including [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext), have a `.Reducers` property, which in turn has methods for invoking reducers defined by the module and registering callbacks on it.
-public class UnityDebugLogger : ISpacetimeDBLogger {}
+Each reducer defined by the module has three methods on the `.Reducers`:
-}
-```
+- An invoke method, whose name is the reducer's name converted to snake case, like `set_name`. This requests that the module run the reducer.
+- A callback registation method, whose name is prefixed with `on_`, like `on_set_name`. This registers a callback to run whenever we are notified that the reducer ran, including successfully committed runs and runs we requested which failed. This method returns a callback id, which can be passed to the callback remove method.
+- A callback remove method, whose name is prefixed with `remove_on_`, like `remove_on_set_name`. This cancels a callback previously registered via the callback registration method.
+
+## Identify a client
+
+### Type `Identity`
+
+A unique public identifier for a client connected to a database.
+See the [module docs](/docs/modules/c-sharp#struct-identity) for more details.
+
+### Type `ConnectionId`
+
+An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity).
+See the [module docs](/docs/modules/c-sharp#struct-connectionid) for more details.
+
+### Type `Timestamp`
+
+A point in time, measured in microseconds since the Unix epoch.
+See the [module docs](/docs/modules/c-sharp#struct-timestamp) for more details.
+
+### Type `TaggedEnum`
-An `ISpacetimeDBLogger` implementation for Unity, using the Unity `Debug.Log` api.
+A [tagged union](https://en.wikipedia.org/wiki/Tagged_union) type.
+See the [module docs](/docs/modules/c-sharp#record-taggedenum) for more details.
diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md
index db06d9a4..aba4b77c 100644
--- a/docs/sdks/c-sharp/quickstart.md
+++ b/docs/sdks/c-sharp/quickstart.md
@@ -28,6 +28,10 @@ Add the `SpacetimeDB.ClientSDK` [NuGet package](https://www.nuget.org/packages/s
dotnet add package SpacetimeDB.ClientSDK
```
+## Clear `client/Program.cs`
+
+Clear out any data from `client/Program.cs` so we can write our chat client.
+
## Generate your module types
The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module.
@@ -39,15 +43,22 @@ mkdir -p client/module_bindings
spacetime generate --lang csharp --out-dir client/module_bindings --project-path server
```
-Take a look inside `client/module_bindings`. The CLI should have generated five files:
+Take a look inside `client/module_bindings`. The CLI should have generated three folders and nine files:
```
module_bindings
-├── Message.cs
-├── ReducerEvent.cs
-├── SendMessageReducer.cs
-├── SetNameReducer.cs
-└── User.cs
+├── Reducers
+│ ├── ClientConnected.g.cs
+│ ├── ClientDisconnected.g.cs
+│ ├── SendMessage.g.cs
+│ └── SetName.g.cs
+├── Tables
+│ ├── Message.g.cs
+│ └── User.g.cs
+├── Types
+│ ├── Message.g.cs
+│ └── User.g.cs
+└── SpacetimeDBClient.g.cs
```
## Add imports to Program.cs
@@ -60,17 +71,16 @@ using SpacetimeDB.Types;
using System.Collections.Concurrent;
```
-We will also need to create some global variables that will be explained when we use them later. Add the following to the top of `Program.cs`:
+We will also need to create some global variables. We'll cover the `Identity` later in the `Save credentials` section. Later we'll also be setting up a second thread for handling user input. In the `Process thread` section we'll use this in the `ConcurrentQueue` to store the commands for that thread.
+
+To `Program.cs`, add:
```csharp
// our local client SpacetimeDB identity
Identity? local_identity = null;
-// declare a thread safe queue to store commands in format (command, args)
-ConcurrentQueue<(string,string)> input_queue = new ConcurrentQueue<(string, string)>();
-
-// declare a threadsafe cancel token to cancel the process loop
-CancellationTokenSource cancel_token = new CancellationTokenSource();
+// declare a thread safe queue to store commands
+var input_queue = new ConcurrentQueue<(string Command, string Args)>();
```
## Define Main function
@@ -78,58 +88,152 @@ CancellationTokenSource cancel_token = new CancellationTokenSource();
We'll work outside-in, first defining our `Main` function at a high level, then implementing each behavior it needs. We need `Main` to do several things:
1. Initialize the `AuthToken` module, which loads and stores our authentication token to/from local storage.
-2. Create the `SpacetimeDBClient` instance.
-3. Register callbacks on any events we want to handle. These will print to standard output messages received from the database and updates about users' names and online statuses.
+2. Connect to the database.
+3. Register a number of callbacks to run in response to various database events.
4. Start our processing thread which connects to the SpacetimeDB module, updates the SpacetimeDB client and processes commands that come in from the input loop running in the main thread.
5. Start the input loop, which reads commands from standard input and sends them to the processing thread.
6. When the input loop exits, stop the processing thread and wait for it to exit.
+To `Program.cs`, add:
+
```csharp
void Main()
{
+ // Initialize the `AuthToken` module
AuthToken.Init(".spacetime_csharp_quickstart");
+ // Builds and connects to the database
+ DbConnection? conn = null;
+ conn = ConnectToDB();
+ // Registers to run in response to database events.
+ RegisterCallbacks(conn);
+ // Declare a threadsafe cancel token to cancel the process loop
+ var cancellationTokenSource = new CancellationTokenSource();
+ // Spawn a thread to call process updates and process commands
+ var thread = new Thread(() => ProcessThread(conn, cancellationTokenSource.Token));
+ thread.Start();
+ // Handles CLI input
+ InputLoop();
+ // This signals the ProcessThread to stop
+ cancellationTokenSource.Cancel();
+ thread.Join();
+}
+```
- RegisterCallbacks();
+## Connect to database
- // spawn a thread to call process updates and process commands
- var thread = new Thread(ProcessThread);
- thread.Start();
+Before we connect, we'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`.
- InputLoop();
+A connection to a SpacetimeDB database is represented by a `DbConnection`. We configure `DbConnection`s using the builder pattern, by calling `DbConnection.Builder()`, chaining method calls to set various connection parameters and register callbacks, then we cap it off with a call to `.Build()` to begin the connection.
- // this signals the ProcessThread to stop
- cancel_token.Cancel();
- thread.Join();
+In our case, we'll supply the following options:
+
+1. A `WithUri` call, to specify the URI of the SpacetimeDB host where our module is running.
+2. A `WithModuleName` call, to specify the name or `Identity` of our database. Make sure to pass the same name here as you supplied to `spacetime publish`.
+3. A `WithToken` call, to supply a token to authenticate with.
+4. An `OnConnect` callback, to run when the remote database acknowledges and accepts our connection.
+5. An `OnConnectError` callback, to run if the remote database is unreachable or it rejects our connection.
+6. An `OnDisconnect` callback, to run when our connection ends.
+
+To `Program.cs`, add:
+
+```csharp
+/// The URI of the SpacetimeDB instance hosting our chat module.
+const string HOST = "http://localhost:3000";
+
+/// The module name we chose when we published our module.
+const string DBNAME = "quickstart-chat";
+
+/// Load credentials from a file and connect to the database.
+DbConnection ConnectToDB()
+{
+ DbConnection? conn = null;
+ conn = DbConnection.Builder()
+ .WithUri(HOST)
+ .WithModuleName(DBNAME)
+ .WithToken(AuthToken.Token)
+ .OnConnect(OnConnected)
+ .OnConnectError(OnConnectError)
+ .OnDisconnect(OnDisconnected)
+ .Build();
+ return conn;
}
```
-## Register callbacks
+### Save credentials
+
+SpacetimeDB will accept any [OpenID Connect](https://openid.net/developers/how-connect-works/) compliant [JSON Web Token](https://jwt.io/) and use it to compute an `Identity` for the user. More complex applications will generally authenticate their user somehow, generate or retrieve a token, and attach it to their connection via `WithToken`. In our case, though, we'll connect anonymously the first time, let SpacetimeDB generate a fresh `Identity` and corresponding JWT for us, and save that token locally to re-use the next time we connect.
+
+Once we are connected, we'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app.
+
+To `Program.cs`, add:
+
+```csharp
+/// Our `OnConnected` callback: save our credentials to a file.
+void OnConnected(DbConnection conn, Identity identity, string authToken)
+{
+ local_identity = identity;
+ AuthToken.SaveToken(authToken);
+}
+```
+
+### Connect Error callback
+
+Should we get an error during connection, we'll be given an `Exception` which contains the details about the exception. To keep things simple, we'll just write the exception to the console.
+
+To `Program.cs`, add:
-We need to handle several sorts of events:
+```csharp
+/// Our `OnConnectError` callback: print the error, then exit the process.
+void OnConnectError(Exception e)
+{
+ Console.Write($"Error while connecting: {e}");
+}
+```
-1. `onConnect`: When we connect, we will call `Subscribe` to tell the module what tables we care about.
-2. `onIdentityReceived`: When we receive our credentials, we'll use the `AuthToken` module to save our token so that the next time we connect, we can re-authenticate as the same user.
-3. `onSubscriptionApplied`: When we get the onSubscriptionApplied callback, that means our local client cache has been fully populated. At this time we'll print the user menu.
-4. `User.OnInsert`: When a new user joins, we'll print a message introducing them.
-5. `User.OnUpdate`: When a user is updated, we'll print their new name, or declare their new online status.
-6. `Message.OnInsert`: When we receive a new message, we'll print it.
-7. `Reducer.OnSetNameEvent`: If the server rejects our attempt to set our name, we'll print an error.
-8. `Reducer.OnSendMessageEvent`: If the server rejects a message we send, we'll print an error.
+### Disconnect callback
+
+When disconnecting, the callback contains the connection details and if an error occurs, it will also contain an `Exception`. If we get an error, we'll write the error to the console, if not, we'll just write that we disconnected.
+
+To `Program.cs`, add:
```csharp
-void RegisterCallbacks()
+/// Our `OnDisconnect` callback: print a note, then exit the process.
+void OnDisconnected(DbConnection conn, Exception? e)
{
- SpacetimeDBClient.instance.onConnect += OnConnect;
- SpacetimeDBClient.instance.onIdentityReceived += OnIdentityReceived;
- SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied;
+ if (e != null)
+ {
+ Console.Write($"Disconnected abnormally: {e}");
+ }
+ else
+ {
+ Console.Write($"Disconnected normally.");
+ }
+}
+```
+
+## Register callbacks
+
+Now we need to handle several sorts of events with Tables and Reducers:
- User.OnInsert += User_OnInsert;
- User.OnUpdate += User_OnUpdate;
+1. `User.OnInsert`: When a new user joins, we'll print a message introducing them.
+2. `User.OnUpdate`: When a user is updated, we'll print their new name, or declare their new online status.
+3. `Message.OnInsert`: When we receive a new message, we'll print it.
+4. `Reducer.OnSetName`: If the server rejects our attempt to set our name, we'll print an error.
+5. `Reducer.OnSendMessage`: If the server rejects a message we send, we'll print an error.
- Message.OnInsert += Message_OnInsert;
+To `Program.cs`, add:
- Reducer.OnSetNameEvent += Reducer_OnSetNameEvent;
- Reducer.OnSendMessageEvent += Reducer_OnSendMessageEvent;
+```csharp
+/// Register all the callbacks our app will use to respond to database events.
+void RegisterCallbacks(DbConnection conn)
+{
+ conn.Db.User.OnInsert += User_OnInsert;
+ conn.Db.User.OnUpdate += User_OnUpdate;
+
+ conn.Db.Message.OnInsert += Message_OnInsert;
+
+ conn.Reducers.OnSetName += Reducer_OnSetNameEvent;
+ conn.Reducers.OnSendMessage += Reducer_OnSendMessageEvent;
}
```
@@ -144,14 +248,18 @@ These callbacks can fire in two contexts:
This second case means that, even though the module only ever inserts online users, the client's `User.OnInsert` callbacks may be invoked with users who are offline. We'll only notify about online users.
-`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `ReducerEvent`. This will be `null` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument.
+`OnInsert` and `OnDelete` callbacks take two arguments: an `EventContext` and the altered row. The `EventContext.Event` is an enum which describes the event that caused the row to be inserted or deleted. All SpacetimeDB callbacks accept a context argument, which you can use in place of your top-level `DbConnection`.
Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `UserNameOrIdentity` to handle this.
+To `Program.cs`, add:
+
```csharp
-string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()!.Substring(0, 8);
+/// If the user has no set name, use the first 8 characters from their identity.
+string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()[..8];
-void User_OnInsert(User insertedValue, ReducerEvent? dbEvent)
+/// Our `User.OnInsert` callback: if the user is online, print a notification.
+void User_OnInsert(EventContext ctx, User insertedValue)
{
if (insertedValue.Online)
{
@@ -162,9 +270,9 @@ void User_OnInsert(User insertedValue, ReducerEvent? dbEvent)
### Notify about updated users
-Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User::update_by_identity` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column.
+Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User.Identity.Update` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column.
-`OnUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`.
+`OnUpdate` callbacks take three arguments: the old row, the new row, and a `EventContext`.
In our module, users can be updated for three reasons:
@@ -174,24 +282,27 @@ In our module, users can be updated for three reasons:
We'll print an appropriate message in each of these cases.
+To `Program.cs`, add:
+
```csharp
-void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent)
+/// Our `User.OnUpdate` callback:
+/// print a notification about name and status changes.
+void User_OnUpdate(EventContext ctx, User oldValue, User newValue)
{
if (oldValue.Name != newValue.Name)
{
Console.WriteLine($"{UserNameOrIdentity(oldValue)} renamed to {newValue.Name}");
}
-
- if (oldValue.Online == newValue.Online)
- return;
-
- if (newValue.Online)
+ if (oldValue.Online != newValue.Online)
{
- Console.WriteLine($"{UserNameOrIdentity(newValue)} connected.");
- }
- else
- {
- Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected.");
+ if (newValue.Online)
+ {
+ Console.WriteLine($"{UserNameOrIdentity(newValue)} connected.");
+ }
+ else
+ {
+ Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected.");
+ }
}
}
```
@@ -200,29 +311,35 @@ void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent)
When we receive a new message, we'll print it to standard output, along with the name of the user who sent it. Keep in mind that we only want to do this for new messages, i.e. those inserted by a `SendMessage` reducer invocation. We have to handle the backlog we receive when our subscription is initialized separately, to ensure they're printed in the correct order. To that effect, our `OnInsert` callback will check if its `ReducerEvent` argument is not `null`, and only print in that case.
-To find the `User` based on the message's `Sender` identity, we'll use `User::FindByIdentity`, which behaves like the same function on the server.
+To find the `User` based on the message's `Sender` identity, we'll use `User.Identity.Find`, which behaves like the same function on the server.
We'll print the user's name or identity in the same way as we did when notifying about `User` table events, but here we have to handle the case where we don't find a matching `User` row. This can happen when the module owner sends a message using the CLI's `spacetime call`. In this case, we'll print `unknown`.
+To `Program.cs`, add:
+
```csharp
-void PrintMessage(Message message)
+/// Our `Message.OnInsert` callback: print new messages.
+void Message_OnInsert(EventContext ctx, Message insertedValue)
{
- var sender = User.FindByIdentity(message.Sender);
- var senderName = "unknown";
- if (sender != null)
+ // We are filtering out messages inserted during the subscription being applied,
+ // since we will be printing those in the OnSubscriptionApplied callback,
+ // where we will be able to first sort the messages before printing.
+ if (ctx.Event is not Event.SubscribeApplied)
{
- senderName = UserNameOrIdentity(sender);
+ PrintMessage(ctx.Db, insertedValue);
}
-
- Console.WriteLine($"{senderName}: {message.Text}");
}
-void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent)
+void PrintMessage(RemoteTables tables, Message message)
{
- if (dbEvent != null)
+ var sender = tables.User.Identity.Find(message.Sender);
+ var senderName = "unknown";
+ if (sender != null)
{
- PrintMessage(insertedValue);
+ senderName = UserNameOrIdentity(sender);
}
+
+ Console.WriteLine($"{senderName}: {message.Text}");
}
```
@@ -232,11 +349,11 @@ We can also register callbacks to run each time a reducer is invoked. We registe
Each reducer callback takes one fixed argument:
-The ReducerEvent that triggered the callback. It contains several fields. The ones we care about are:
+The `ReducerEventContext` of the callback, which contains an `Event` that contains several fields. The ones we care about are:
-1. The `Identity` of the client that called the reducer.
+1. The `CallerIdentity`, the `Identity` of the client that called the reducer.
2. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`.
-3. The error message, if any, that the reducer returned.
+3. If we get a `Status.Failed`, an error message is nested inside that we'll want to write to the console.
It also takes a variable amount of additional arguments that match the reducer's arguments.
@@ -251,16 +368,16 @@ We already handle successful `SetName` invocations using our `User.OnUpdate` cal
We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes.
+To `Program.cs`, add:
+
```csharp
-void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name)
+/// Our `OnSetNameEvent` callback: print a warning if the reducer failed.
+void Reducer_OnSetNameEvent(ReducerEventContext ctx, string name)
{
- bool localIdentityFailedToChangeName =
- reducerEvent.Identity == local_identity &&
- reducerEvent.Status == ClientApi.Event.Types.Status.Failed;
-
- if (localIdentityFailedToChangeName)
+ var e = ctx.Event;
+ if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error))
{
- Console.Write($"Failed to change name to {name}");
+ Console.Write($"Failed to change name to {name}: {error}");
}
}
```
@@ -269,43 +386,42 @@ void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name)
We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different.
+To `Program.cs`, add:
+
```csharp
-void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text)
+/// Our `OnSendMessageEvent` callback: print a warning if the reducer failed.
+void Reducer_OnSendMessageEvent(ReducerEventContext ctx, string text)
{
- bool localIdentityFailedToSendMessage =
- reducerEvent.Identity == local_identity &&
- reducerEvent.Status == ClientApi.Event.Types.Status.Failed;
-
- if (localIdentityFailedToSendMessage)
+ var e = ctx.Event;
+ if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error))
{
- Console.Write($"Failed to send message {text}");
+ Console.Write($"Failed to send message {text}: {error}");
}
}
```
-## Connect callback
+## Subscribe to queries
-Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database.
+SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database using `SubscribeToAllTables`.
-```csharp
-void OnConnect()
-{
- SpacetimeDBClient.instance.Subscribe(new List
- {
- "SELECT * FROM User", "SELECT * FROM Message"
- });
-}
-```
+You can also subscribe to specific tables using SQL syntax, e.g. `SELECT * FROM my_table`. Our [SQL documentation](/docs/sql) enumerates the operations that are accepted in our SQL syntax.
+
+When we specify our subscriptions, we can supply an `OnApplied` callback. This will run when the subscription is applied and the matching rows become available in our client cache. We'll use this opportunity to print the message backlog in proper order.
-## OnIdentityReceived callback
+We can also provide an `OnError` callback. This will run if the subscription fails, usually due to an invalid or malformed SQL queries. We can't handle this case, so we'll just print out the error and exit the process.
-This callback is executed when we receive our credentials from the SpacetimeDB module. We'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app.
+In `Program.cs`, update our `OnConnected` function to include `conn.SubscriptionBuilder().OnApplied(OnSubscriptionApplied).SubscribeToAllTables();` so that it reads:
```csharp
-void OnIdentityReceived(string authToken, Identity identity, Address _address)
+/// Our `OnConnect` callback: save our credentials to a file.
+void OnConnected(DbConnection conn, Identity identity, string authToken)
{
local_identity = identity;
AuthToken.SaveToken(authToken);
+
+ conn.SubscriptionBuilder()
+ .OnApplied(OnSubscriptionApplied)
+ .SubscribeToAllTables();
}
```
@@ -313,59 +429,60 @@ void OnIdentityReceived(string authToken, Identity identity, Address _address)
Once our subscription is applied, we'll print all the previously sent messages. We'll define a function `PrintMessagesInOrder` to do this. `PrintMessagesInOrder` calls the automatically generated `Iter` function on our `Message` table, which returns an iterator over all rows in the table. We'll use the `OrderBy` method on the iterator to sort the messages by their `Sent` timestamp.
+To `Program.cs`, add:
+
```csharp
-void PrintMessagesInOrder()
+/// Our `OnSubscriptionApplied` callback:
+/// sort all past messages and print them in timestamp order.
+void OnSubscriptionApplied(SubscriptionEventContext ctx)
{
- foreach (Message message in Message.Iter().OrderBy(item => item.Sent))
- {
- PrintMessage(message);
- }
+ Console.WriteLine("Connected");
+ PrintMessagesInOrder(ctx.Db);
}
-void OnSubscriptionApplied()
+void PrintMessagesInOrder(RemoteTables tables)
{
- Console.WriteLine("Connected");
- PrintMessagesInOrder();
+ foreach (Message message in tables.Message.Iter().OrderBy(item => item.Sent))
+ {
+ PrintMessage(tables, message);
+ }
}
```
-
-
## Process thread
-Since the input loop will be blocking, we'll run our processing code in a separate thread. This thread will:
-
-1. Connect to the module. We'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`. We will also store if SSL is enabled in a constant called `SSL_ENABLED`. This only needs to be `true` if we are using `SpacetimeDB Cloud`. Replace `` with the name you chose when publishing your module during the module quickstart.
+Since the input loop will be blocking, we'll run our processing code in a separate thread.
-`Connect` takes an auth token, which is `null` for a new connection, or a stored string for a returning user. We are going to use the optional AuthToken module which uses local storage to store the auth token. If you want to use your own way to associate an auth token with a user, you can pass in your own auth token here.
+This thread will loop until the thread is signaled to exit, calling the update function `FrameTick` on the `DbConnection` to process any updates received from the module, and `ProcessCommand` to process any commands received from the input loop.
-2. Loop until the thread is signaled to exit, calling `Update` on the SpacetimeDBClient to process any updates received from the module, and `ProcessCommand` to process any commands received from the input loop.
+Afterward, close the connection to the module.
-3. Finally, Close the connection to the module.
+To `Program.cs`, add:
```csharp
-const string HOST = "http://localhost:3000";
-const string DBNAME = "module";
-
-void ProcessThread()
+/// Our separate thread from main, where we can call process updates and process commands without blocking the main thread.
+void ProcessThread(DbConnection conn, CancellationToken ct)
{
- SpacetimeDBClient.instance.Connect(AuthToken.Token, HOST, DBNAME);
-
- // loop until cancellation token
- while (!cancel_token.IsCancellationRequested)
+ try
{
- SpacetimeDBClient.instance.Update();
+ // loop until cancellation token
+ while (!ct.IsCancellationRequested)
+ {
+ conn.FrameTick();
- ProcessCommands();
+ ProcessCommands(conn.Reducers);
- Thread.Sleep(100);
+ Thread.Sleep(100);
+ }
+ }
+ finally
+ {
+ conn.Disconnect();
}
-
- SpacetimeDBClient.instance.Close();
}
```
-## Input loop and ProcessCommands
+## Handle user input
The input loop will read commands from standard input and send them to the processing thread using the input queue. The `ProcessCommands` function is called every 100ms by the processing thread to process any pending commands.
@@ -375,7 +492,10 @@ Supported Commands:
2. Set name: `name`, will send the new name to the module by calling `Reducer.SetName` which is automatically generated by `spacetime generate`.
+To `Program.cs`, add:
+
```csharp
+/// Read each line of standard input, and either set our name or send a message as appropriate.
void InputLoop()
{
while (true)
@@ -388,7 +508,7 @@ void InputLoop()
if (input.StartsWith("/name "))
{
- input_queue.Enqueue(("name", input.Substring(6)));
+ input_queue.Enqueue(("name", input[6..]));
continue;
}
else
@@ -398,18 +518,18 @@ void InputLoop()
}
}
-void ProcessCommands()
+void ProcessCommands(RemoteReducers reducers)
{
// process input queue commands
while (input_queue.TryDequeue(out var command))
{
- switch (command.Item1)
+ switch (command.Command)
{
case "message":
- Reducer.SendMessage(command.Item2);
+ reducers.SendMessage(command.Args);
break;
case "name":
- Reducer.SetName(command.Item2);
+ reducers.SetName(command.Args);
break;
}
}
@@ -418,7 +538,9 @@ void ProcessCommands()
## Run the client
-Finally we just need to add a call to `Main` in `Program.cs`:
+Finally, we just need to add a call to `Main`.
+
+To `Program.cs`, add:
```csharp
Main();
@@ -432,4 +554,10 @@ dotnet run --project client
## What's next?
-Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example.
+Congratulations! You've built a simple chat app using SpacetimeDB.
+
+You can find the full code for this client [in the C# client SDK's examples](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/tree/master/examples~/quickstart-chat/client).
+
+Check out the [C# client SDK Reference](/docs/sdks/c-sharp) for a more comprehensive view of the SpacetimeDB C# client SDK.
+
+If you are interested in developing in the Unity game engine, check out our [Unity Comprehensive Tutorial](/docs/unity) and [Blackholio](https://github.com/ClockworkLabs/Blackholio) game example.
diff --git a/docs/sdks/rust/index.md b/docs/sdks/rust/index.md
index d8befe53..a6dd23bb 100644
--- a/docs/sdks/rust/index.md
+++ b/docs/sdks/rust/index.md
@@ -2,7 +2,21 @@
The SpacetimeDB client SDK for Rust contains all the tools you need to build native clients for SpacetimeDB modules using Rust.
-## Install the SDK
+| Name | Description |
+|-------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
+| [Project setup](#project-setup) | Configure a Rust crate to use the SpacetimeDB Rust client SDK. |
+| [Generate module bindings](#generate-module-bindings) | Use the SpacetimeDB CLI to generate module-specific types and interfaces. |
+| [`DbConnection` type](#type-dbconnection) | A connection to a remote database. |
+| [`DbContext` trait](#trait-dbcontext) | Methods for interacting with the remote database. Implemented by [`DbConnection`](#type-dbconnection) and various event context types. |
+| [`EventContext` type](#type-eventcontext) | [`DbContext`](#trait-dbcontext) available in [row callbacks](#callback-on_insert). |
+| [`ReducerEventContext` type](#type-reducereventcontext) | [`DbContext`](#trait-dbcontext) available in [reducer callbacks](#observe-and-invoke-reducers). |
+| [`SubscriptionEventContext` type](#type-subscriptioneventcontext) | [`DbContext`](#trait-dbcontext) available in [subscription-related callbacks](#subscribe-to-queries). |
+| [`ErrorContext` type](#type-errorcontext) | [`DbContext`](#trait-dbcontext) available in error-related callbacks. |
+| [Access the client cache](#access-the-client-cache) | Make local queries against subscribed rows, and register [row callbacks](#callback-on_insert) to run when subscribed rows change. |
+| [Observe and invoke reducers](#observe-and-invoke-reducers) | Send requests to the database to run reducers, and register callbacks to run when notified of reducers. |
+| [Identify a client](#identify-a-client) | Types for identifying users and client connections. |
+
+## Project setup
First, create a new project using `cargo new` and add the SpacetimeDB SDK to your dependencies:
@@ -37,7 +51,13 @@ module_bindings::DbConnection
A connection to a remote database is represented by the `module_bindings::DbConnection` type. This type is generated per-module, and contains information about the types, tables and reducers defined by your module.
-### Connect to a module - `DbConnection::builder()` and `.build()`
+| Name | Description |
+|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
+| [Connect to a module](#connect-to-a-module) | Construct a `DbConnection`. |
+| [Advance the connection](#advance-the-connection-and-process-messages) | Poll the `DbConnection`, or set up a background worker to run it. |
+| [Access tables and reducers](#access-tables-and-reducers) | Access subscribed rows in the client cache, request reducer invocations, and register callbacks. |
+
+### Connect to a module
```rust
impl DbConnection {
@@ -45,7 +65,17 @@ impl DbConnection {
}
```
-Construct a `DbConnection` by calling `DbConnection::builder()` and chaining configuration methods, then calling `.build()`. You must at least specify `with_uri`, to supply the URI of the SpacetimeDB to which you published your module, and `with_module_name`, to supply the human-readable SpacetimeDB domain name or the raw address which identifies the module.
+Construct a `DbConnection` by calling `DbConnection::builder()` and chaining configuration methods, then calling `.build()`. You must at least specify `with_uri`, to supply the URI of the SpacetimeDB to which you published your module, and `with_module_name`, to supply the human-readable SpacetimeDB domain name or the raw `Identity` which identifies the module.
+
+| Name | Description |
+|-----------------------------------------------------------|--------------------------------------------------------------------------------------|
+| [`with_uri` method](#method-with_uri) | Set the URI of the SpacetimeDB instance which hosts the remote database. |
+| [`with_module_name` method](#method-with_module_name) | Set the name or `Identity` of the remote database. |
+| [`on_connect` callback](#callback-on_connect) | Register a callback to run when the connection is successfully established. |
+| [`on_connect_error` callback](#callback-on_connect_error) | Register a callback to run if the connection is rejected or the host is unreachable. |
+| [`on_disconnect` callback](#callback-on_disconnect) | Register a callback to run when the connection ends. |
+| [`with_token` method](#method-with_token) | Supply a token to authenticate with the remote database. |
+| [`build` method](#method-build) | Finalize configuration and connect. |
#### Method `with_uri`
@@ -61,11 +91,11 @@ Configure the URI of the SpacetimeDB instance or cluster which hosts the remote
```rust
impl DbConnectionBuilder {
- fn with_module_name(self, name_or_address: impl ToString) -> Self;
+ fn with_module_name(self, name_or_identity: impl ToString) -> Self;
}
```
-Configure the SpacetimeDB domain name or address of the remote module which identifies it within the SpacetimeDB instance or cluster.
+Configure the SpacetimeDB domain name or `Identity` of the remote module which identifies it within the SpacetimeDB instance or cluster.
#### Callback `on_connect`
@@ -75,41 +105,54 @@ impl DbConnectionBuilder {
}
```
-Chain a call to `.on_connect(callback)` to your builder to register a callback to run when your new `DbConnection` successfully initiates its connection to the remote module. The callback accepts three arguments: a reference to the `DbConnection`, the `Identity` by which SpacetimeDB identifies this connection, and a private access token which can be saved and later passed to [`with_credentials`](#method-with_credentials) to authenticate the same user in future connections.
+Chain a call to `.on_connect(callback)` to your builder to register a callback to run when your new `DbConnection` successfully initiates its connection to the remote module. The callback accepts three arguments: a reference to the `DbConnection`, the `Identity` by which SpacetimeDB identifies this connection, and a private access token which can be saved and later passed to [`with_token`](#method-with_token) to authenticate the same user in future connections.
This interface may change in an upcoming release as we rework SpacetimeDB's authentication model.
#### Callback `on_connect_error`
-Currently unused.
+```rust
+impl DbConnectionBuilder {
+ fn on_connect_error(
+ self,
+ callback: impl FnOnce(&ErrorContext, spacetimedb_sdk::Error),
+ ) -> DbConnectionBuilder;
+}
+```
+
+Chain a call to `.on_connect_error(callback)` to your builder to register a callback to run when your connection fails.
+
+A known bug in the SpacetimeDB Rust client SDK currently causes this callback never to be invoked. [`on_disconnect`](#callback-on_disconnect) callbacks are invoked instead.
#### Callback `on_disconnect`
```rust
impl DbConnectionBuilder {
- fn on_disconnect(self, callback: impl FnOnce(&DbConnection, Option<&anyhow::Error>)) -> DbConnectionBuilder;
+ fn on_disconnect(
+ self,
+ callback: impl FnOnce(&ErrorContext, Option),
+ ) -> DbConnectionBuilder;
}
```
-Chain a call to `.on_connect(callback)` to your builder to register a callback to run when your `DbConnection` disconnects from the remote module, either as a result of a call to [`disconnect`](#method-disconnect) or due to an error.
+Chain a call to `.on_disconnect(callback)` to your builder to register a callback to run when your `DbConnection` disconnects from the remote module, either as a result of a call to [`disconnect`](#method-disconnect) or due to an error.
-#### Method `with_credentials`
+#### Method `with_token`
```rust
impl DbConnectionBuilder {
- fn with_credentials(self, credentials: Option<(Identity, String)>) -> Self;
+ fn with_token(self, token: Option>) -> Self;
}
```
-Chain a call to `.with_credentials(credentials)` to your builder to provide an `Identity` and private access token to authenticate with, or to explicitly select an anonymous connection. If this method is not called or `None` is passed, SpacetimeDB will generate a new `Identity` and sign a new private access token for the connection.
+Chain a call to `.with_token(token)` to your builder to provide an OpenID Connect compliant JSON Web Token to authenticate with, or to explicitly select an anonymous connection. If this method is not called or `None` is passed, SpacetimeDB will generate a new `Identity` and sign a new private access token for the connection.
-This interface may change in an upcoming release as we rework SpacetimeDB's authentication model.
#### Method `build`
```rust
impl DbConnectionBuilder {
- fn build(self) -> anyhow::Result;
+ fn build(self) -> Result;
}
```
@@ -119,7 +162,13 @@ After configuring the connection and registering callbacks, attempt to open the
In the interest of supporting a wide variety of client applications with different execution strategies, the SpacetimeDB SDK allows you to choose when the `DbConnection` spends compute time and processes messages. If you do not arrange for the connection to advance by calling one of these methods, the `DbConnection` will never advance, and no callbacks will ever be invoked.
-#### Run in the background - method `run_threaded`
+| Name | Description |
+|-----------------------------------------------|-------------------------------------------------------|
+| [`run_threaded` method](#method-run_threaded) | Spawn a thread to process messages in the background. |
+| [`run_async` method](#method-run_async) | Process messages in an async task. |
+| [`frame_tick` method](#method-frame_tick) | Process messages on the main thread without blocking. |
+
+#### Method `run_threaded`
```rust
impl DbConnection {
@@ -129,45 +178,150 @@ impl DbConnection {
`run_threaded` spawns a thread which will continuously advance the connection, sleeping when there is no work to do. The thread will panic if the connection disconnects erroneously, or return if it disconnects as a result of a call to [`disconnect`](#method-disconnect).
-#### Run asynchronously - method `run_async`
+#### Method `run_async`
```rust
impl DbConnection {
- async fn run_async(&self) -> anyhow::Result<()>;
+ async fn run_async(&self) -> Result<(), spacetimedb_sdk::Error>;
}
```
`run_async` will continuously advance the connection, `await`-ing when there is no work to do. The task will return an `Err` if the connection disconnects erroneously, or return `Ok(())` if it disconnects as a result of a call to [`disconnect`](#method-disconnect).
-#### Run on the main thread without blocking - method `frame_tick`
+#### Method `frame_tick`
```rust
impl DbConnection {
- fn frame_tick(&self) -> anyhow::Result<()>;
+ fn frame_tick(&self) -> Result<(), spacetimedb_sdk::Error>;
}
```
`frame_tick` will advance the connection until no work remains, then return rather than blocking or `await`-ing. Games might arrange for this message to be called every frame. `frame_tick` returns `Ok` if the connection remains active afterwards, or `Err` if the connection disconnected before or during the call.
+### Access tables and reducers
+
+#### Field `db`
+
+```rust
+struct DbConnection {
+ pub db: RemoteTables,
+ /* other members */
+}
+```
+
+The `db` field of the `DbConnection` provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache).
+
+#### Field `reducers`
+
+```rust
+struct DbConnection {
+ pub reducers: RemoteReducers,
+ /* other members */
+}
+```
+
+The `reducers` field of the `DbConnection` provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers).
+
## Trait `DbContext`
-[`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext) both implement `DbContext`, which allows
+```rust
+trait spacetimedb_sdk::DbContext {
+ /* methods */
+}
+```
+
+[`DbConnection`](#type-dbconnection), [`EventContext`](#type-eventcontext), [`ReducerEventContext`](#type-reducereventcontext), [`SubscriptionEventContext`](#type-subscriptioneventcontext) and [`ErrorContext`](#type-errorcontext) all implement `DbContext`. `DbContext` has methods for inspecting and configuring your connection to the remote database, including [`ctx.db()`](#method-db), a trait-generic alternative to reading the `.db` property on a concrete-typed context object.
+
+The `DbContext` trait is implemented by connections and contexts to *every* module. This means that its [`DbView`](#method-db) and [`Reducers`](#method-reducers) are associated types.
+
+| Name | Description |
+|-------------------------------------------------------|--------------------------------------------------------------------------|
+| [`RemoteDbContext` trait](#trait-remotedbcontext) | Module-specific `DbContext` extension trait with associated types bound. |
+| [`db` method](#method-db) | Trait-generic alternative to the `db` field of `DbConnection`. |
+| [`reducers` method](#method-reducers) | Trait-generic alternative to the `reducers` field of `DbConnection`. |
+| [`disconnect` method](#method-disconnect) | End the connection. |
+| [Subscribe to queries](#subscribe-to-queries) | Register SQL queries to receive updates about matching rows. |
+| [Read connection metadata](#read-connection-metadata) | Access the connection's `Identity` and `ConnectionId` |
+
+### Trait `RemoteDbContext`
+
+```rust
+trait module_bindings::RemoteDbContext
+ : spacetimedb_sdk::DbContext* Associated type constraints */> {}
+```
+
+Each module's `module_bindings` exports a trait `RemoteDbContext` which extends `DbContext`, with the associated types `DbView` and `Reducers` bound to the types defined for that module. This can be more convenient when creating functions that can be called from any callback for a specific module, but which access the database or invoke reducers, and so must know the type of the `DbView` or `Reducers`.
+
+### Method `db`
+
+```rust
+trait DbContext {
+ fn db(&self) -> &Self::DbView;
+}
+```
+
+When operating in trait-generic contexts, it is necessary to call the `ctx.db()` method, rather than accessing the `ctx.db` field, as Rust traits cannot expose fields.
+
+#### Example
+
+```rust
+fn print_users(ctx: &impl RemoteDbContext) {
+ for user in ctx.db().user().iter() {
+ println!("{}", user.name);
+ }
+}
+```
+
+### Method `reducers`
+
+```rust
+trait DbContext {
+ fn reducerrs(&self) -> &Self::Reducers;
+}
+```
+
+When operating in trait-generic contexts, it is necessary to call the `ctx.reducers()` method, rather than accessing the `ctx.reducers` field, as Rust traits cannot expose fields.
+
+#### Example
+
+```rust
+fn call_say_hello(ctx: &impl RemoteDbContext) {
+ ctx.reducers.say_hello();
+}
+```
### Method `disconnect`
```rust
trait DbContext {
- fn disconnect(&self) -> anyhow::Result<()>;
+ fn disconnect(&self) -> spacetimedb_sdk::Result<()>;
}
```
Gracefully close the `DbConnection`. Returns an `Err` if the connection is already disconnected.
-### Subscribe to queries - `DbContext::subscription_builder` and `.subscribe()`
+### Subscribe to queries
-This interface is subject to change in an upcoming SpacetimeDB release.
+| Name | Description |
+|---------------------------------------------------------|-------------------------------------------------------------|
+| [`SubscriptionBuilder` type](#type-subscriptionbuilder) | Builder-pattern constructor to register subscribed queries. |
+| [`SubscriptionHandle` type](#type-subscriptionhandle) | Manage an active subscripion. |
-A known issue in the SpacetimeDB Rust SDK causes inconsistent behaviors after re-subscribing. This will be fixed in an upcoming SpacetimeDB release. For now, Rust clients should issue only one subscription per `DbConnection`.
+#### Type `SubscriptionBuilder`
+
+```rust
+spacetimedb_sdk::SubscriptionBuilder
+```
+
+| Name | Description |
+|----------------------------------------------------------------------------------|-----------------------------------------------------------------|
+| [`ctx.subscription_builder()` constructor](#constructor-ctxsubscription_builder) | Begin configuring a new subscription. |
+| [`on_applied` callback](#callback-on_applied) | Register a callback to run when matching rows become available. |
+| [`on_error` callback](#callback-on_error) | Register a callback to run if the subscription fails. |
+| [`subscribe` method](#method-subscribe) | Finish configuration and subscribe to one or more SQL queries. |
+| [`subscribe_to_all_tables` method](#method-subscribe_to_all_tables) | Convenience method to subscribe to the entire database. |
+
+##### Constructor `ctx.subscription_builder()`
```rust
trait DbContext {
@@ -177,17 +331,28 @@ trait DbContext {
Subscribe to queries by calling `ctx.subscription_builder()` and chaining configuration methods, then calling `.subscribe(queries)`.
-#### Callback `on_applied`
+##### Callback `on_applied`
+
+```rust
+impl SubscriptionBuilder {
+ fn on_applied(self, callback: impl FnOnce(&SubscriptionEventContext)) -> Self;
+}
+```
+
+Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache.
+
+##### Callback `on_error`
```rust
impl SubscriptionBuilder {
- fn on_applied(self, callback: impl FnOnce(&EventContext)) -> Self;
+ fn on_error(self, callback: impl FnOnce(&ErrorContext, spacetimedb_sdk::Error)) -> Self;
}
```
-Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache. The [`EventContext`](#type-eventcontext) passed to the callback will have `Event::SubscribeApplied` as its `event`.
+Register a callback to run if the subscription is rejected or unexpectedly terminated by the server. This is most frequently caused by passing an invalid query to [`subscribe`](#method-subscribe).
-#### Method `subscribe`
+
+##### Method `subscribe`
```rust
impl SubscriptionBuilder {
@@ -195,11 +360,87 @@ impl SubscriptionBuilder {
}
```
-Subscribe to a set of queries. `queries` should be an array or slice of strings.
+Subscribe to a set of queries. `queries` should be a string or an array, vec or slice of strings.
+
+See [the SpacetimeDB SQL Reference](/docs/sql#subscriptions) for information on the queries SpacetimeDB supports as subscriptions.
+
+##### Method `subscribe_to_all_tables`
+
+```rust
+impl SubscriptionBuilder {
+ fn subscribe_to_all_tables(self);
+}
+```
+
+Subscribe to all rows from all public tables. This method is provided as a convenience for simple clients. The subscription initiated by `subscribe_to_all_tables` cannot be canceled after it is initiated. You should [`subscribe` to specific queries](#method-subscribe) if you need fine-grained control over the lifecycle of your subscriptions.
+
+#### Type `SubscriptionHandle`
+
+```rust
+module_bindings::SubscriptionHandle
+```
+
+A `SubscriptionHandle` represents a subscribed query or a group of subscribed queries.
+
+The `SubscriptionHandle` does not contain or provide access to the subscribed rows. Subscribed rows of all subscriptions by a connection are contained within that connection's [`ctx.db`](#field-db). See [Access the client cache](#access-the-client-cache).
+
+| Name | Description |
+|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
+| [`is_ended` method](#method-is_ended) | Determine whether the subscription has ended. |
+| [`is_active` method](#method-is_active) | Determine whether the subscription is active and its matching rows are present in the client cache. |
+| [`unsubscribe` method](#method-unsubscribe) | Discard a subscription. |
+| [`unsubscribe_then` method](#method-unsubscribe_then) | Discard a subscription, and register a callback to run when its matching rows are removed from the client cache. |
+
+##### Method `is_ended`
+
+```rust
+impl SubscriptionHandle {
+ fn is_ended(&self) -> bool;
+}
+```
+
+Returns true if this subscription has been terminated due to an unsubscribe call or an error.
+
+##### Method `is_active`
+
+```rust
+impl SubscriptionHandle {
+ fn is_active(&self) -> bool;
+}
+```
+
+Returns true if this subscription has been applied and has not yet been unsubscribed.
+
+##### Method `unsubscribe`
+
+```rust
+impl SubscriptionHandle {
+ fn unsubscribe(&self) -> Result<(), spacetimedb_sdk::Error>;
+}
+```
+
+Terminate this subscription, causing matching rows to be removed from the client cache. Any rows removed from the client cache this way will have [`on_delete` callbacks](#callback-on_delete) run for them.
-The returned `SubscriptionHandle` is currently not useful, but will become significant in a future version of SpacetimeDB.
+Unsubscribing is an asynchronous operation. Matching rows are not removed from the client cache immediately. Use [`unsubscribe_then`](#method-unsubscribe_then) to run a callback once the unsubscribe operation is completed.
-### Identity a client
+Returns an error if the subscription has already ended, either due to a previous call to `unsubscribe` or [`unsubscribe_then`](#method-unsubscribe_then), or due to an error.
+
+##### Method `unsubscribe_then`
+
+```rust
+impl SubscriptionHandle {
+ fn unsubscribe_then(
+ self,
+ on_end: impl FnOnce(&SubscriptionEventContext),
+ ) -> Result<(), spacetimedb_sdk::Error>;
+}
+```
+
+Terminate this subscription, and run the `on_end` callback when the subscription is ended and its matching rows are removed from the client cache. Any rows removed from the client cache this way will have [`on_delete` callbacks](#callback-on_delete) run for them.
+
+Returns an error if the subscription has already ended, either due to a previous call to [`unsubscribe`](#method-unsubscribe) or `unsubscribe_then`, or due to an error.
+
+### Read connection metadata
#### Method `identity`
@@ -221,6 +462,16 @@ trait DbContext {
Like [`DbContext::identity`](#method-identity), but returns `None` instead of panicking if the `Identity` is not yet available.
+#### Method `connection_id`
+
+```rust
+trait DbContext {
+ fn connection_id(&self) -> ConnectionId;
+}
+```
+
+Get the [`ConnectionId`](#type-connectionid) with which SpacetimeDB identifies the connection.
+
#### Method `is_active`
```rust
@@ -237,7 +488,47 @@ trait DbContext {
module_bindings::EventContext
```
-An `EventContext` is a [`DbContext`](#trait-dbcontext) augmented with a field `event: Event`.
+An `EventContext` is a [`DbContext`](#trait-dbcontext) augmented with a field [`event: Event`](#enum-event). `EventContext`s are passed as the first argument to row callbacks [`on_insert`](#callback-on_insert), [`on_delete`](#callback-on_delete) and [`on_update`](#callback-on_update).
+
+| Name | Description |
+|-------------------------------------|---------------------------------------------------------------|
+| [`event` field](#field-event) | Enum describing the cause of the current row callback. |
+| [`db` field](#field-db) | Provides access to the client cache. |
+| [`reducers` field](#field-reducers) | Allows requesting reducers run on the remote database. |
+| [`Event` enum](#enum-event) | Possible events which can cause a row callback to be invoked. |
+
+### Field `event`
+
+```rust
+struct EventContext {
+ pub event: spacetimedb_sdk::Event,
+ /* other fields */
+}
+```
+
+The [`Event`](#enum-event) contained in the `EventContext` describes what happened to cause the current row callback to be invoked.
+
+### Field `db`
+
+```rust
+struct EventContext {
+ pub db: RemoteTables,
+ /* other members */
+}
+```
+
+The `db` field of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache).
+
+### Field `reducers`
+
+```rust
+struct EventContext {
+ pub reducers: RemoteReducers,
+ /* other members */
+}
+```
+
+The `reducers` field of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers).
### Enum `Event`
@@ -245,6 +536,17 @@ An `EventContext` is a [`DbContext`](#trait-dbcontext) augmented with a field `e
spacetimedb_sdk::Event
```
+| Name | Description |
+|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
+| [`Reducer` variant](#variant-reducer) | A reducer ran in the remote database. |
+| [`SubscribeApplied` variant](#variant-subscribeapplied) | A new subscription was applied to the client cache. |
+| [`UnsubscribeApplied` variant](#variant-unsubscribeapplied) | A previous subscription was removed from the client cache after a call to [`unsubscribe`](#method-unsubscribe). |
+| [`SubscribeError` variant](#variant-subscribeerror) | A previous subscription was removed from the client cache due to an error. |
+| [`UnknownTransaction` variant](#variant-unknowntransaction) | A transaction ran in the remote database, but was not attributed to a known reducer. |
+| [`ReducerEvent` struct](#struct-reducerevent) | Metadata about a reducer run. Contained in [`Event::Reducer`](#variant-reducer) and [`ReducerEventContext`](#type-reducereventcontext). |
+| [`Status` enum](#enum-status) | Completion status of a reducer run. |
+| [`Reducer` enum](#enum-reducer) | Module-specific generated enum with a variant for each reducer defined by the module. |
+
#### Variant `Reducer`
```rust
@@ -253,7 +555,7 @@ spacetimedb_sdk::Event::Reducer(spacetimedb_sdk::ReducerEvent {
/// The `Identity` of the SpacetimeDB actor which invoked the reducer.
caller_identity: Identity,
- /// The `Address` of the SpacetimeDB actor which invoked the reducer,
- /// or `None` if the actor did not supply an address.
- caller_address: Option