From f859208f19acd155b35c61789045a54710dc73fa Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:39:47 -0700 Subject: [PATCH] Release v0.12.0 docs updates (#97) * Update quickstart.md (#81) Revert the find_by changes in rust which were never made. * Update Rust Quickstart to use correct function to find User (#80) Update quickstart.md * Explicitly remind the reader to start the server (#43) * Fix broken tutorial package link (#86) * prettier (#85) Push * Update quickstart.md (#84) * Fix typo in modules/rust/index.md (#83) Person -> Unique (because that belongs to `Unique` table) * Update part-2b-c-sharp.md (#75) The intent is to throw an exception if the player already exists, not the other way 'round. * Fixed code examples in rust reference regarding insertion (#42) * Rust client quickstart updated for 0.12 (#92) * Rust client updated for 0.12 * Small update * More updates * Final pass --------- Co-authored-by: John Detter * I didn't notice that auto-merge was enabled, so here's my review (#94) * Update Rust SDK ref for the new SDK (#93) * Updated rust quickstart for 0.12 (#88) * Updated rust quickstart for 0.12 * Suggested tweaks --------- Co-authored-by: John Detter * Update rust index page for 0.12 (#89) * Updated rust quickstart for 0.12 * Suggested tweaks * Initial updates to the index file * More updates to index, rolled back changes from another PR I'm working on * Small improvements --------- Co-authored-by: John Detter --------- Co-authored-by: Tyler Cloutier Co-authored-by: Mats Bennervall <44610444+Savalige@users.noreply.github.com> Co-authored-by: ike709 Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> Co-authored-by: Puru Vijay <47742487+PuruVJ@users.noreply.github.com> Co-authored-by: Egor Gavrilov Co-authored-by: Arrel Neumiller Co-authored-by: Muthsera Co-authored-by: John Detter Co-authored-by: Phoebe Goldman Co-authored-by: Zeke Foppa --- .prettierrc | 11 + README.md | 1 + docs/getting-started.md | 2 +- docs/http/database.md | 72 +- docs/http/energy.md | 4 +- docs/http/identity.md | 12 +- docs/modules/c-sharp/index.md | 21 +- docs/modules/c-sharp/quickstart.md | 8 +- docs/modules/rust/index.md | 163 ++-- docs/modules/rust/quickstart.md | 91 +-- docs/nav.js | 116 +-- docs/sdks/c-sharp/index.md | 6 +- docs/sdks/index.md | 2 +- docs/sdks/rust/index.md | 1153 ++++++---------------------- docs/sdks/rust/quickstart.md | 270 ++++--- docs/sdks/typescript/index.md | 170 ++-- docs/sdks/typescript/quickstart.md | 42 +- docs/unity/part-1.md | 4 +- docs/unity/part-2b-c-sharp.md | 12 +- docs/unity/part-4.md | 2 +- docs/ws/index.md | 4 +- nav.ts | 119 +-- package.json | 2 +- 23 files changed, 836 insertions(+), 1451 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2921455 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "jsxSingleQuote": false, + "trailingComma": "es5", + "endOfLine": "auto", + "printWidth": 80 +} diff --git a/README.md b/README.md index 0f9998b..c31b2c3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ git clone ssh://git@github.com//spacetime-docs git add . git commit -m "A specific description of the changes I made and why" ``` + 5. Push your changes to your fork as a branch ```bash diff --git a/docs/getting-started.md b/docs/getting-started.md index 4b0cdda..33265dc 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -28,6 +28,6 @@ You are ready to start developing SpacetimeDB modules. See below for a quickstar ### Client - [Rust](/docs/sdks/rust/quickstart) -- [C# (Standalone)](/docs/sdks/c-sharp/quickstart) +- [C# (Standalone)](/docs/sdks/c-sharp/quickstart) - [C# (Unity)](/docs/unity/part-1) - [Typescript](/docs/sdks/typescript/quickstart) diff --git a/docs/http/database.md b/docs/http/database.md index 16ee729..9b6e048 100644 --- a/docs/http/database.md +++ b/docs/http/database.md @@ -15,7 +15,7 @@ The HTTP endpoints in `/database` allow clients to interact with Spacetime datab | [`/database/confirm_recovery_code GET`](#databaseconfirm_recovery_code-get) | Recover a login token from a recovery code. | | [`/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/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. | @@ -92,8 +92,8 @@ Accessible through the CLI as `spacetime dns set-name
`. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Returns @@ -145,8 +145,8 @@ Accessible through the CLI as `spacetime dns register-tld `. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Returns @@ -183,11 +183,11 @@ Accessible through the CLI as `spacetime identity recover `. #### Query Parameters -| Name | Value | -| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `identity` | The identity whose token should be recovered. | +| Name | Value | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `identity` | The identity whose token should be recovered. | | `email` | The email to send the recovery code or link to. This email must be associated with the identity, either during creation via [`/identity`](/docs/http/identity#identity-post) or afterwards via [`/identity/:identity/set-email`](/docs/http/identity#identityidentityset_email-post). | -| `link` | A boolean; whether to send a clickable link rather than a recovery code. | +| `link` | A boolean; whether to send a clickable link rather than a recovery code. | ## `/database/confirm_recovery_code GET` @@ -229,8 +229,8 @@ Accessible through the CLI as `spacetime publish`. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Data @@ -281,8 +281,8 @@ Accessible through the CLI as `spacetime delete
`. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | ## `/database/subscribe/:name_or_address GET` @@ -299,18 +299,18 @@ Begin a [WebSocket connection](/docs/ws) with a database. For more information about WebSocket headers, see [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). -| Name | Value | -| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| 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. | +| `Connection` | `Updgrade` | +| `Upgrade` | `websocket` | +| `Sec-WebSocket-Version` | `13` | +| `Sec-WebSocket-Key` | A 16-byte value, generated randomly by the client, encoded as Base64. | #### Optional Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | ## `/database/call/:name_or_address/:reducer POST` @@ -326,8 +326,8 @@ Invoke a reducer in a database. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Data @@ -444,10 +444,10 @@ The `"entities"` will be an object whose keys are table and reducer names, and w } ``` -| Entity field | Value | -| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `arity` | For tables, the number of colums; for reducers, the number of arguments. | -| `type` | For tables, `"table"`; for reducers, `"reducer"`. | +| 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]`. @@ -484,10 +484,10 @@ Returns a single entity in the same format as in the `"entities"` returned by [t } ``` -| Field | Value | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `arity` | For tables, the number of colums; for reducers, the number of arguments. | -| `type` | For tables, `"table"`; for reducers, `"reducer"`. | +| 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` @@ -514,7 +514,7 @@ Returns JSON in the form: ``` | 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"`. | @@ -541,8 +541,8 @@ Accessible through the CLI as `spacetime logs `. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Returns @@ -563,8 +563,8 @@ Accessible through the CLI as `spacetime sql `. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Data diff --git a/docs/http/energy.md b/docs/http/energy.md index b49a1ee..6f00831 100644 --- a/docs/http/energy.md +++ b/docs/http/energy.md @@ -57,8 +57,8 @@ Accessible through the CLI as `spacetime energy set-balance #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Returns diff --git a/docs/http/identity.md b/docs/http/identity.md index 5fb4586..6f1e22c 100644 --- a/docs/http/identity.md +++ b/docs/http/identity.md @@ -71,8 +71,8 @@ Generate a short-lived access token which can be used in untrusted contexts, e.g #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Returns @@ -107,8 +107,8 @@ Accessible through the CLI as `spacetime identity set-email `. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | ## `/identity/:identity/databases GET` @@ -145,8 +145,8 @@ Verify the validity of an identity/token pair. #### Required Headers -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | +| Name | Value | +| --------------- | --------------------------------------------------------------- | | `Authorization` | A Spacetime token [encoded as Basic authorization](/docs/http). | #### Returns diff --git a/docs/modules/c-sharp/index.md b/docs/modules/c-sharp/index.md index 7380467..f6763fc 100644 --- a/docs/modules/c-sharp/index.md +++ b/docs/modules/c-sharp/index.md @@ -42,7 +42,7 @@ static partial class Module { // 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. @@ -120,7 +120,6 @@ And a couple of special custom types: - `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. - #### Custom types `[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. @@ -245,10 +244,10 @@ public partial struct Person // 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); - + // 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); - + // 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); } @@ -295,14 +294,14 @@ public static void PrintInfo(ReducerContext e) } ``` - ### Scheduler Tables + Tables can be used to schedule a reducer calls either at a specific timestamp or at regular intervals. ```csharp public static partial class Timers { - + // The `Scheduled` attribute links this table to a reducer. [SpacetimeDB.Table(Scheduled = nameof(SendScheduledMessage))] public partial struct SendMessageTimer @@ -310,7 +309,7 @@ public static partial class Timers public string Text; } - + // 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] @@ -354,10 +353,10 @@ public static partial class Timers public partial struct SendMessageTimer { public string Text; // fields of original struct - + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] public ulong ScheduledId; // unique identifier to be used internally - + public SpacetimeDB.ScheduleAt ScheduleAt; // Scheduling details (Time or Inteval) } } @@ -375,10 +374,9 @@ These are four special kinds of reducers that can be used to respond to module l - `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. - Example: -```csharp +````csharp [SpacetimeDB.Reducer(ReducerKind.Init)] public static void Init() { @@ -402,3 +400,4 @@ public static void OnDisconnect(DbEventArgs ctx) { Log($"{ctx.Sender} has disconnected."); }``` +```` diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index 21e4fcd..5d8c873 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -23,6 +23,7 @@ If you haven't already, start by [installing SpacetimeDB](/install). This will i Next we need to [install .NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) so that we can build and publish our module. You may already have .NET 8 and can be checked: + ```bash dotnet --list-sdks ``` @@ -50,7 +51,7 @@ spacetime init --lang csharp server ## Declare imports -`spacetime init` generated a few files: +`spacetime init` generated a few files: 1. Open `server/StdbModule.csproj` to generate a .sln file for intellisense/validation support. 2. Open `server/Lib.cs`, a trivial module. @@ -81,7 +82,6 @@ To get our chat server running, we'll need to store two kinds of data: informati For each `User`, we'll store their `Identity`, an optional name they can set to identify themselves to other users, and whether they're online or not. We'll designate the `Identity` as our primary key, which enforces that it must be unique, indexes it for faster lookup, and allows clients to track updates. - In `server/Lib.cs`, add the definition of the table `User` to the `Module` class: ```csharp @@ -281,10 +281,10 @@ npm i wasm-opt -g 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 SendMessage "Hello, World!" ``` -Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. +Once we've called our `SendMessage` reducer, we can check to make sure it ran by running the `logs` command. ```bash spacetime logs diff --git a/docs/modules/rust/index.md b/docs/modules/rust/index.md index c2acf5c..83a751b 100644 --- a/docs/modules/rust/index.md +++ b/docs/modules/rust/index.md @@ -23,7 +23,7 @@ struct Location { 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: +// 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}; @@ -31,7 +31,7 @@ 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. -#[spacetimedb(table(public))] +#[table(name = person, public)] pub struct Person { name: String, } @@ -39,26 +39,26 @@ pub struct Person { // 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. -#[spacetimedb(reducer)] -pub fn add(name: String) { +#[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: - Person::insert(person) + 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 knows about all their types. Reducers also have to be top level +// SpacetimeDB recognizes their types. Reducers also have to be top level // functions, not methods. -#[spacetimedb(reducer)] -pub fn say_hello() { +#[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 Person::iter() { + 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 @@ -72,13 +72,13 @@ pub fn say_hello() { // 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. -#[spacetimedb(reducer)] -pub fn add_person(name: String) -> Result<(), String> { +#[reducer] +pub fn add_person(ctx: &ReducerContext, name: String) -> Result<(), String> { if name.is_empty() { return Err("Name cannot be empty"); } - Person::insert(Person { name }) + ctx.db.person().insert(Person { name }) } ``` @@ -88,15 +88,15 @@ Now we'll get into details on all the macro APIs SpacetimeDB provides, starting ### Defining tables -The `#[spacetimedb(table)]` is applied to a Rust struct with named fields. +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 `#[spacetimedb(table(public))]` macro makes a table public. **Public** tables are readable by all users, but can still only be modified by your 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 -#[spacetimedb(table(public))] -struct Table { +#[table(name = my_table, public)] +struct MyTable { field1: String, field2: u32, } @@ -104,7 +104,7 @@ struct Table { 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. +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: @@ -120,10 +120,10 @@ And common data structures: - `Option where T: SpacetimeType` - `Vec where T: SpacetimeType` -All `#[spacetimedb(table)]` types are `SpacetimeType`s, and accordingly, all of their fields have to be. +All `#[table(..)]` types are `SpacetimeType`s, and accordingly, all of their fields have to be. ```rust -#[spacetimedb(table(public))] +#[table(name = another_table, public)] struct AnotherTable { // Fine, some builtin types. id: u64, @@ -155,7 +155,7 @@ enum Serial { 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 -#[spacetimedb(table(public))] +#[table(name = person, public)] struct Person { #[unique] id: u64, @@ -167,53 +167,54 @@ struct Person { ### Defining reducers -`#[spacetimedb(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>`. +`#[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 -#[spacetimedb(reducer)] -fn give_player_item(player_id: u64, item_id: u64) -> Result<(), GameErr> { +#[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 = Item::find_by_item_id(id).ok_or(GameErr::InvalidId)?; + let mut item = ctx.db.item().item_id().find(id).ok_or(GameErr::InvalidId)?; item.owner = Some(player_id); - Item::update_by_id(id, item); + ctx.db.item().item_id().update(item); Ok(()) } +#[table(name = item, public)] struct Item { - #[unique] + #[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. `#[unique]` and `#[autoinc]` are covered below, describing how those attributes affect the semantics of inserting, filtering, and so on. +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. -#[spacetimedb(table, scheduled(send_message))] +#[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: +The `scheduled` attribute adds a couple of default fields and expands as follows: + ```rust -#[spacetimedb(table)] +#[table(name = send_message_timer, scheduled(send_message)] struct SendMessageTimer { text: String, // original field - #[primary] + #[primary_key] #[autoinc] scheduled_id: u64, // identifier for internal purpose scheduled_at: ScheduleAt, //schedule details @@ -229,37 +230,37 @@ pub enum ScheduleAt { } ``` -Managing timers with scheduled table is as simple as inserting or deleting rows from table. -```rust -#[spacetimedb(reducer)] +Managing timers with a scheduled table is as simple as inserting or deleting rows from the table. -// Reducers linked to the scheduler table should have their first argument as `ReducerContext` +```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> { +fn send_message(ctx: &ReducerContext, arg: SendMessageTimer) -> Result<(), String> { // ... } // Scheduling reducers inside `init` reducer -fn init() { +#[reducer(init)] +fn init(ctx: &ReducerContext) { // Scheduling a reducer for a specific Timestamp - SendMessageTimer::insert(SendMessageTimer { + 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`. + //`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. - SendMessageTimer::insert(SendMessageTimer { + 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`. + //`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. @@ -281,8 +282,8 @@ use spacetimedb::{ dbg, }; -#[spacetimedb(reducer)] -fn output(i: i32) { +#[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"); @@ -296,7 +297,7 @@ fn output(i: i32) { // before passing the value of |i| along to the calling function. // // The output is logged log::Level::Debug. - OutputtedNumbers::insert(dbg!(i)); + ctx.db.outputted_number().insert(dbg!(i)); } ``` @@ -307,16 +308,16 @@ We'll work off these structs to see what functions SpacetimeDB generates: This table has a plain old column. ```rust -#[spacetimedb(table(public))] +#[table(name = ordinary, public)] struct Ordinary { ordinary_field: u64, } ``` -This table has a unique column. Every row in the `Person` table must have distinct values of the `unique_field` column. Attempting to insert a row with a duplicate value will fail. +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 -#[spacetimedb(table(public))] +#[table(name = unique, public)] struct Unique { // A unique column: #[unique] @@ -329,7 +330,7 @@ This table has an automatically incrementing column. SpacetimeDB automatically p Only integer types can be `#[unique]`: `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64` and `i128`. ```rust -#[spacetimedb(table(public))] +#[table(name = autoinc, public)] struct Autoinc { #[autoinc] autoinc_field: u64, @@ -339,7 +340,7 @@ struct Autoinc { These attributes can be combined, to create an automatically assigned ID usable for filtering. ```rust -#[spacetimedb(table(public))] +#[table(name = identity, public)] struct Identity { #[autoinc] #[unique] @@ -351,15 +352,15 @@ struct Identity { 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 `Ordinary::insert` method. +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 -#[spacetimedb(reducer)] -fn insert_ordinary(value: u64) { +#[reducer] +fn insert_ordinary(ctx: &ReducerContext, value: u64) { let ordinary = Ordinary { ordinary_field: value }; - let result = Ordinary::insert(ordinary); + let result = ctx.db.ordinary().insert(ordinary); assert_eq!(ordinary.ordinary_field, result.ordinary_field); } ``` @@ -369,12 +370,12 @@ When there is a unique column constraint on the table, insertion can fail if a u If we insert two rows which have the same value of a unique column, the second will fail. ```rust -#[spacetimedb(reducer)] -fn insert_unique(value: u64) { - let result = Ordinary::insert(Unique { unique_field: value }); +#[reducer] +fn insert_unique(ctx: &ReducerContext, value: u64) { + let result = ctx.db.unique().insert(Unique { unique_field: value }); assert!(result.is_ok()); - let result = Ordinary::insert(Unique { unique_field: value }); + let result = ctx.db.unique().insert(Unique { unique_field: value }); assert!(result.is_err()); } ``` @@ -384,26 +385,26 @@ When inserting a table with an `#[autoinc]` column, the database will automatica The returned row has the `autoinc` column set to the value that was actually written into the database. ```rust -#[spacetimedb(reducer)] -fn insert_autoinc() { +#[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 = Autoinc::insert(Autoinc { autoinc_field: 23 }) + let actual = ctx.db.autoinc().insert(Autoinc { autoinc_field: 23 }) assert_eq!(actual.autoinc_field, i); } } -#[spacetimedb(reducer)] -fn insert_id() { +#[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. - Identity::insert(Identity { autoinc_field: 23 }) + ctx.db.identity().insert(Identity { id_field: 23 }) } } ``` @@ -413,7 +414,7 @@ fn insert_id() { Given a table, we can iterate over all the rows in it. ```rust -#[spacetimedb(table(public))] +#[table(name = person, public)] struct Person { #[unique] id: u64, @@ -424,20 +425,20 @@ struct Person { } ``` -// Every table structure an iter function, like: +// Every table structure has a generated iter function, like: ```rust -fn MyTable::iter() -> TableIter +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. ``` -#[spacetimedb(reducer)] -fn iteration() { +#[reducer] +fn iteration(ctx: &ReducerContext) { let mut addresses = HashSet::new(); - for person in Person::iter() { + for person in ctx.db.person().iter() { addresses.insert(person.address); } @@ -456,9 +457,9 @@ Our `Person` table has a unique id column, so we can filter for a row matching t The name of the filter method just corresponds to the column name. ```rust -#[spacetimedb(reducer)] -fn filtering(id: u64) { - match Person::find_by_id(&id) { +#[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}"), } @@ -468,9 +469,9 @@ fn filtering(id: u64) { Our `Person` table also has a column for age. Unlike IDs, ages aren't unique. Filtering for every person who is 21, then, gives us an `Iterator` rather than an `Option`. ```rust -#[spacetimedb(reducer)] -fn filtering_non_unique() { - for person in Person::find_by_age(&21) { +#[reducer] +fn filtering_non_unique(ctx: &ReducerContext) { + for person in ctx.db.person().age().find(21) { println!("{person} has turned 21"); } } @@ -481,9 +482,9 @@ fn filtering_non_unique() { Like filtering, we can delete by a unique column instead of the entire row. ```rust -#[spacetimedb(reducer)] -fn delete_id(id: u64) { - Person::delete_by_id(&id) +#[reducer] +fn delete_id(ctx: &ReducerContext, id: u64) { + ctx.db.person().id().delete(id) } ``` diff --git a/docs/modules/rust/quickstart.md b/docs/modules/rust/quickstart.md index d3544f1..9fcfe30 100644 --- a/docs/modules/rust/quickstart.md +++ b/docs/modules/rust/quickstart.md @@ -2,27 +2,28 @@ In this tutorial, we'll implement a simple chat server as a SpacetimeDB module. -A SpacetimeDB module is code that gets compiled to WebAssembly and is uploaded to SpacetimeDB. This code becomes server-side logic that interfaces directly with the Spacetime relational database. +A SpacetimeDB module is code that gets compiled to a WebAssembly binary and is uploaded to SpacetimeDB. This code becomes server-side logic that interfaces directly with the SpacetimeDB relational database. Each SpacetimeDB module defines a set of tables and a set of reducers. -Each table is defined as a Rust `struct` annotated with `#[spacetimedb(table)]`, where an instance represents a row, and each field represents a column. +Each table is defined as a Rust struct annotated with `#[table(name = table_name)]`. An instance of the struct represents a row, and each field represents a column. + 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))]` macro makes a table public. **Public** tables are readable by all users, but can still only be modified by your 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 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 Rust, reducers are defined as functions annotated with `#[spacetimedb(reducer)]`, and may return a `Result<()>`, with an `Err` return aborting the transaction. +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 -If you haven't already, start by [installing SpacetimeDB](/install). This will install the `spacetime` command line interface (CLI), which contains all the functionality for interacting with SpacetimeDB. +If you haven't already, start by [installing SpacetimeDB](/install). This will install the `spacetime` command line interface (CLI), which provides all the functionality needed to interact with SpacetimeDB. ## Install Rust Next we need to [install Rust](https://www.rust-lang.org/tools/install) so that we can create our database module. -On MacOS and Linux run this command to install the Rust compiler: +On macOS and Linux run this command to install the Rust compiler: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -47,17 +48,19 @@ spacetime init --lang rust server ## Declare imports -`spacetime init` should have pre-populated `server/src/lib.rs` with a trivial module. Clear it out, so we can write a module that's still pretty simple: a bare-bones chat server. +`spacetime init` should have pre-populated `server/src/lib.rs` with a trivial module. Clear it out so we can write a new, simple module: a bare-bones chat server. To the top of `server/src/lib.rs`, add some imports we'll be using: ```rust -use spacetimedb::{spacetimedb, ReducerContext, Identity, Timestamp}; +use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp}; ``` From `spacetimedb`, we import: -- `spacetimedb`, an attribute macro we'll use to define tables and reducers. +- `table`, a macro used to define SpacetimeDB tables. +- `reducer`, a macro used to define SpacetimeDB reducers. +- `Table`, a rust trait which allows us to interact with tables. - `ReducerContext`, a special argument passed to each reducer. - `Identity`, a unique identifier for each user. - `Timestamp`, a point in time. Specifically, an unsigned 64-bit count of milliseconds since the UNIX epoch. @@ -71,9 +74,9 @@ For each `User`, we'll store their `Identity`, an optional name they can set to To `server/src/lib.rs`, add the definition of the table `User`: ```rust -#[spacetimedb(table(public))] +#[table(name = user, public)] pub struct User { - #[primarykey] + #[primary_key] identity: Identity, name: Option, online: bool, @@ -85,7 +88,7 @@ For each `Message`, we'll store the `Identity` of the user who sent it, the `Tim To `server/src/lib.rs`, add the definition of the table `Message`: ```rust -#[spacetimedb(table(public))] +#[table(name = message, public)] pub struct Message { sender: Identity, sent: Timestamp, @@ -97,19 +100,19 @@ 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. For now, we only need the `Identity`, `ctx.sender`. +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`. 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. To `server/src/lib.rs`, add: ```rust -#[spacetimedb(reducer)] -/// Clientss invoke this reducer to set their user names. -pub fn set_name(ctx: ReducerContext, name: String) -> Result<(), String> { +#[reducer] +/// Clients invoke this reducer to set their user names. +pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> { let name = validate_name(name)?; - if let Some(user) = User::filter_by_identity(&ctx.sender) { - User::update_by_identity(&ctx.sender, User { name: Some(name), ..user }); + if let Some(user) = ctx.db.user().identity().find(ctx.sender) { + ctx.db.user().identity().update(User { name: Some(name), ..user }) Ok(()) } else { Err("Cannot set name for unknown user".to_string()) @@ -140,17 +143,17 @@ fn validate_name(name: String) -> Result { ## Send messages -We define a reducer `send_message`, which clients will call to send messages. It will validate the message's text, then insert a new `Message` record using `Message::insert`, with the `sender` identity and `sent` timestamp taken from the `ReducerContext`. Because `Message` does not have any columns with unique constraints, `Message::insert` is infallible; it does not return a `Result`. +We define a reducer `send_message`, which clients will call to send messages. It will validate the message's text, then insert a new `Message` record using `ctx.db.message().insert(..)`, with the `sender` identity and `sent` timestamp taken from the `ReducerContext`. Because the `Message` table does not have any columns with a unique constraint, `ctx.db.message().insert()` is infallible and does not return a `Result`. To `server/src/lib.rs`, add: ```rust -#[spacetimedb(reducer)] +#[reducer] /// Clients invoke this reducer to send messages. -pub fn send_message(ctx: ReducerContext, text: String) -> Result<(), String> { +pub fn send_message(ctx: &ReducerContext, text: String) -> Result<(), String> { let text = validate_message(text)?; log::info!("{}", text); - Message::insert(Message { + ctx.db.message().insert(Message { sender: ctx.sender, text, sent: ctx.timestamp, @@ -181,40 +184,39 @@ You could extend the validation in `validate_message` in similar ways to `valida ## Set users' online status -Whenever a client connects, the module will run a special reducer, annotated with `#[spacetimedb(connect)]`, if it's defined. By convention, it's named `identity_connected`. We'll use it to create a `User` record for the client if it doesn't yet exist, and to set its online status. +Whenever a client connects, the module will run a special reducer, annotated with `#[reducer(client_connected)]`, if it's defined. By convention, it's named `client_connected`. We'll use it to create a `User` record for the client if it doesn't yet exist, and to set its online status. -We'll use `User::filter_by_identity` to look up a `User` row for `ctx.sender`, if one exists. If we find one, we'll use `User::update_by_identity` 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)]` macro, with rows and behavior based on the row attributes. `filter_by_identity` returns an `Option`, because the unique constraint from the `#[primarykey]` attribute means there will be either zero or one matching rows. `insert` returns a `Result<(), UniqueConstraintViolation>` because of the same unique constraint; if we want to overwrite a `User` row, we need to do so explicitly using `update_by_identity`. +We'll use `ctx.db.user().identity().find(ctx.sender)` to look up a `User` row for `ctx.sender`, if one exists. If we find one, we'll use `ctx.db.user().identity().update(..)` to overwrite it with a row that has `online: true`. If not, we'll use `ctx.db.user().insert(..)` to insert a new row for our new user. All three of these methods are generated by the `#[table(..)]` macro, with rows and behavior based on the row attributes. `ctx.db.user().find(..)` returns an `Option`, because of the unique constraint from the `#[primary_key]` attribute. This means there will be either zero or one matching rows. If we used `try_insert` here it would return a `Result<(), UniqueConstraintViolation>` because of the same unique constraint. However, because we're already checking if there is a user with the given sender identity we know that inserting into this table will not fail. Therefore, we use `insert`, which automatically unwraps the result, simplifying the code. If we want to overwrite a `User` row, we need to do so explicitly using `ctx.db.user().identity().update(..)`. To `server/src/lib.rs`, add the definition of the connect reducer: ```rust -#[spacetimedb(connect)] +#[reducer(client_connected)] // Called when a client connects to the SpacetimeDB -pub fn identity_connected(ctx: ReducerContext) { - if let Some(user) = User::filter_by_identity(&ctx.sender) { +pub fn client_connected(ctx: &ReducerContext) { + if let Some(user) = ctx.db.user().identity().find(ctx.sender) { // 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::update_by_identity(&ctx.sender, User { online: true, ..user }); + ctx.db.user().identity().update(User { online: true, ..user }); } else { // If this is a new user, create a `User` row for the `Identity`, // which is online, but hasn't set a name. - User::insert(User { + ctx.db.user().insert(User { name: None, identity: ctx.sender, online: true, - }).unwrap(); + }); } -} -``` +}``` -Similarly, whenever a client disconnects, the module will run the `#[spacetimedb(disconnect)]` reducer if it's defined. By convention, it's named `identity_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 run the `#[reducer(client_disconnected)]` reducer if it's defined. By convention, it's named `client_disconnected`. We'll use it to un-set the `online` status of the `User` for the disconnected client. ```rust -#[spacetimedb(disconnect)] +#[reducer(client_disconnected)] // Called when a client disconnects from SpacetimeDB -pub fn identity_disconnected(ctx: ReducerContext) { - if let Some(user) = User::filter_by_identity(&ctx.sender) { - User::update_by_identity(&ctx.sender, User { online: false, ..user }); +pub fn identity_disconnected(ctx: &ReducerContext) { + if let Some(user) = ctx.db.user().identity().find(ctx.sender) { + ctx.db.user().identity().update(User { online: false, ..user }); } else { // This branch should be unreachable, // as it doesn't make sense for a client to disconnect without connecting first. @@ -225,7 +227,7 @@ 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 pleasant. 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 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 ``. From the `quickstart-chat` directory, run: @@ -250,7 +252,10 @@ spacetime logs You should now see the output that your module printed in the database. ```bash -info: Hello, World! + INFO: spacetimedb: Creating table `message` + INFO: spacetimedb: Creating table `user` + INFO: spacetimedb: Database initialized + INFO: src/lib.rs:43: Hello, world! ``` ## SQL Queries @@ -258,13 +263,13 @@ 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 "SELECT * FROM message" ``` ```bash - text ---------- - "Hello, World!" + sender | sent | text +--------------------------------------------------------------------+------------------+----------------- + 0x93dda09db9a56d8fa6c024d843e805d8262191db3b4ba84c5efcd1ad451fed4e | 1727858455560802 | "Hello, world!" ``` ## What's next? diff --git a/docs/nav.js b/docs/nav.js index 6949c4f..5a66950 100644 --- a/docs/nav.js +++ b/docs/nav.js @@ -1,55 +1,75 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); function page(title, slug, path, props) { - return { type: "page", path, slug, title, ...props }; + return { type: 'page', path, slug, title, ...props }; } function section(title) { - return { type: "section", title }; + return { type: 'section', title }; } const nav = { - items: [ - section("Intro"), - 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("Unity Tutorial - Basic Multiplayer"), - page("Overview", "unity-tutorial", "unity/index.md"), - page("1 - Setup", "unity/part-1", "unity/part-1.md"), - page("2a - Server (Rust)", "unity/part-2a-rust", "unity/part-2a-rust.md"), - page("2b - Server (C#)", "unity/part-2b-c-sharp", "unity/part-2b-c-sharp.md"), - page("3 - Client", "unity/part-3", "unity/part-3.md"), - section("Unity Tutorial - Advanced"), - page("4 - Resources And Scheduling", "unity/part-4", "unity/part-4.md"), - page("5 - BitCraft Mini", "unity/part-5", "unity/part-5.md"), - section("Server Module Languages"), - page("Overview", "modules", "modules/index.md"), - page("Rust Quickstart", "modules/rust/quickstart", "modules/rust/quickstart.md"), - page("Rust Reference", "modules/rust", "modules/rust/index.md"), - page("C# Quickstart", "modules/c-sharp/quickstart", "modules/c-sharp/quickstart.md"), - 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"), - 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"), - ], + items: [ + section('Intro'), + 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('Unity Tutorial - Basic Multiplayer'), + page('Overview', 'unity-tutorial', 'unity/index.md'), + page('1 - Setup', 'unity/part-1', 'unity/part-1.md'), + page('2a - Server (Rust)', 'unity/part-2a-rust', 'unity/part-2a-rust.md'), + page( + '2b - Server (C#)', + 'unity/part-2b-c-sharp', + 'unity/part-2b-c-sharp.md' + ), + page('3 - Client', 'unity/part-3', 'unity/part-3.md'), + section('Unity Tutorial - Advanced'), + page('4 - Resources And Scheduling', 'unity/part-4', 'unity/part-4.md'), + page('5 - BitCraft Mini', 'unity/part-5', 'unity/part-5.md'), + section('Server Module Languages'), + page('Overview', 'modules', 'modules/index.md'), + page( + 'Rust Quickstart', + 'modules/rust/quickstart', + 'modules/rust/quickstart.md' + ), + page('Rust Reference', 'modules/rust', 'modules/rust/index.md'), + page( + 'C# Quickstart', + 'modules/c-sharp/quickstart', + 'modules/c-sharp/quickstart.md' + ), + 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'), + 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'), + ], }; exports.default = nav; diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md index e8a3d01..d85f570 100644 --- a/docs/sdks/c-sharp/index.md +++ b/docs/sdks/c-sharp/index.md @@ -849,7 +849,7 @@ Save a token to the filesystem. ### Class `Identity` ```cs -namespace SpacetimeDB +namespace SpacetimeDB { public struct Identity : IEquatable { @@ -869,7 +869,7 @@ A unique public identifier for a user of a database. 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. ```cs -namespace SpacetimeDB +namespace SpacetimeDB { public struct Address : IEquatable
{ @@ -888,7 +888,7 @@ An opaque identifier for a client connection to a database, intended to differen The SpacetimeDB C# SDK performs internal logging. -A default logger is set up automatically for you - a [`ConsoleLogger`](#class-consolelogger) for C# projects and [`UnityDebugLogger`](#class-unitydebuglogger) for Unity projects. +A default logger is set up automatically for you - a [`ConsoleLogger`](#class-consolelogger) for C# projects and [`UnityDebugLogger`](#class-unitydebuglogger) for Unity projects. 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. diff --git a/docs/sdks/index.md b/docs/sdks/index.md index 940f06a..46078cb 100644 --- a/docs/sdks/index.md +++ b/docs/sdks/index.md @@ -1,4 +1,4 @@ - SpacetimeDB Client SDKs Overview +SpacetimeDB Client SDKs Overview The SpacetimeDB Client SDKs provide a comprehensive interface to interact with the SpacetimeDB server engine from various programming languages. Currently, SDKs are available for diff --git a/docs/sdks/rust/index.md b/docs/sdks/rust/index.md index dbc2311..50e8aa9 100644 --- a/docs/sdks/rust/index.md +++ b/docs/sdks/rust/index.md @@ -7,7 +7,7 @@ The SpacetimeDB client SDK for Rust contains all the tools you need to build nat First, create a new project using `cargo new` and add the SpacetimeDB SDK to your dependencies: ```bash -cargo add spacetimedb +cargo add spacetimedb_sdk ``` ## Generate module bindings @@ -29,1165 +29,454 @@ Declare a `mod` for the bindings in your client's `src/main.rs`: mod module_bindings; ``` -## API at a glance - -| Definition | Description | -| ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | -| Function [`module_bindings::connect`](#function-connect) | Autogenerated function to connect to a database. | -| Function [`spacetimedb_sdk::disconnect`](#function-disconnect) | Close the active connection. | -| Function [`spacetimedb_sdk::on_disconnect`](#function-on_disconnect) | Register a `FnMut` callback to run when a connection ends. | -| Function [`spacetimedb_sdk::once_on_disconnect`](#function-once_on_disconnect) | Register a `FnOnce` callback to run the next time a connection ends. | -| Function [`spacetimedb_sdk::remove_on_disconnect`](#function-remove_on_disconnect) | Cancel an `on_disconnect` or `once_on_disconnect` callback. | -| Function [`spacetimedb_sdk::subscribe`](#function-subscribe) | Subscribe to queries with a `&[&str]`. | -| Function [`spacetimedb_sdk::subscribe_owned`](#function-subscribe_owned) | Subscribe to queries with a `Vec`. | -| Function [`spacetimedb_sdk::on_subscription_applied`](#function-on_subscription_applied) | Register a `FnMut` callback to run when a subscription's initial rows become available. | -| Function [`spacetimedb_sdk::once_on_subscription_applied`](#function-once_on_subscription_applied) | Register a `FnOnce` callback to run the next time a subscription's initial rows become available. | -| Function [`spacetimedb_sdk::remove_on_subscription_applied`](#function-remove_on_subscription_applied) | Cancel an `on_subscription_applied` or `once_on_subscription_applied` callback. | -| Type [`spacetimedb_sdk::identity::Identity`](#type-identity) | A unique public identifier for a client. | -| Type [`spacetimedb_sdk::identity::Token`](#type-token) | A private authentication token corresponding to an `Identity`. | -| Type [`spacetimedb_sdk::identity::Credentials`](#type-credentials) | An `Identity` paired with its `Token`. | -| Type [`spacetimedb_sdk::Address`](#type-address) | An opaque identifier for differentiating connections by the same `Identity`. | -| Function [`spacetimedb_sdk::identity::identity`](#function-identity) | Return the current connection's `Identity`. | -| Function [`spacetimedb_sdk::identity::token`](#function-token) | Return the current connection's `Token`. | -| Function [`spacetimedb_sdk::identity::credentials`](#function-credentials) | Return the current connection's [`Credentials`](#type-credentials). | -| Function [`spacetimedb_sdk::identity::address`](#function-address) | Return the current connection's [`Address`](#type-address). | -| Function [`spacetimedb_sdk::identity::on_connect`](#function-on_connect) | Register a `FnMut` callback to run when the connection's [`Credentials`](#type-credentials) are verified with the database. | -| Function [`spacetimedb_sdk::identity::once_on_connect`](#function-once_on_connect) | Register a `FnOnce` callback to run when the connection's [`Credentials`](#type-credentials) are verified with the database. | -| Function [`spacetimedb_sdk::identity::remove_on_connect`](#function-remove_on_connect) | Cancel an `on_connect` or `once_on_connect` callback. | -| Function [`spacetimedb_sdk::identity::load_credentials`](#function-load_credentials) | Load a saved [`Credentials`](#type-credentials) from a file. | -| Function [`spacetimedb_sdk::identity::save_credentials`](#function-save_credentials) | Save a [`Credentials`](#type-credentials) to a file. | -| Type [`module_bindings::{TABLE}`](#type-table) | Autogenerated `struct` type for a table, holding one row. | -| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](#method-filter_by_column) | Autogenerated method to iterate over subscribed rows where a column matches a value. | -| Method [`module_bindings::{TABLE}::find_by_{COLUMN}`](#method-find_by_column) | Autogenerated method to seek a subscribed row where a unique column matches a value. | -| Trait [`spacetimedb_sdk::table::TableType`](#trait-tabletype) | Automatically implemented for all tables defined by a module. | -| Method [`spacetimedb_sdk::table::TableType::count`](#method-count) | Count the number of subscribed rows in a table. | -| Method [`spacetimedb_sdk::table::TableType::iter`](#method-iter) | Iterate over all subscribed rows. | -| Method [`spacetimedb_sdk::table::TableType::filter`](#method-filter) | Iterate over a subset of subscribed rows matching a predicate. | -| Method [`spacetimedb_sdk::table::TableType::find`](#method-find) | Return one subscribed row matching a predicate. | -| Method [`spacetimedb_sdk::table::TableType::on_insert`](#method-on_insert) | Register a `FnMut` callback to run whenever a new subscribed row is inserted. | -| Method [`spacetimedb_sdk::table::TableType::remove_on_insert`](#method-remove_on_insert) | Cancel an `on_insert` callback. | -| Method [`spacetimedb_sdk::table::TableType::on_delete`](#method-on_delete) | Register a `FnMut` callback to run whenever a subscribed row is deleted. | -| Method [`spacetimedb_sdk::table::TableType::remove_on_delete`](#method-remove_on_delete) | Cancel an `on_delete` callback. | -| Trait [`spacetimedb_sdk::table::TableWithPrimaryKey`](#trait-tablewithprimarykey) | Automatically implemented for tables with a column designated `#[primarykey]`. | -| Method [`spacetimedb_sdk::table::TableWithPrimaryKey::on_update`](#method-on_update) | Register a `FnMut` callback to run whenever an existing subscribed row is updated. | -| Method [`spacetimedb_sdk::table::TableWithPrimaryKey::remove_on_update`](#method-remove_on_update) | Cancel an `on_update` callback. | -| Type [`module_bindings::ReducerEvent`](#type-reducerevent) | Autogenerated enum with a variant for each reducer defined by the module. | -| Type [`module_bindings::{REDUCER}Args`](#type-reducerargs) | Autogenerated `struct` type for a reducer, holding its arguments. | -| Function [`module_bindings::{REDUCER}`](#function-reducer) | Autogenerated function to invoke a reducer. | -| Function [`module_bindings::on_{REDUCER}`](#function-on_reducer) | Autogenerated function to register a `FnMut` callback to run whenever the reducer is invoked. | -| Function [`module_bindings::once_on_{REDUCER}`](#function-once_on_reducer) | Autogenerated function to register a `FnOnce` callback to run the next time the reducer is invoked. | -| Function [`module_bindings::remove_on_{REDUCER}`](#function-remove_on_reducer) | Autogenerated function to cancel an `on_{REDUCER}` or `once_on_{REDUCER}` callback. | -| Type [`spacetimedb_sdk::reducer::Status`](#type-status) | Enum representing reducer completion statuses. | - -## Connect to a database - -### Function `connect` +## Type `DbConnection` ```rust -module_bindings::connect( - spacetimedb_uri: impl TryInto, - db_name: &str, - credentials: Option, -) -> anyhow::Result<()> +module_bindings::DbConnection ``` -Connect to a database named `db_name` accessible over the internet at the URI `spacetimedb_uri`. +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. -| Argument | Type | Meaning | -| ----------------- | --------------------- | ------------------------------------------------------------ | -| `spacetimedb_uri` | `impl TryInto` | URI of the SpacetimeDB instance running the module. | -| `db_name` | `&str` | Name of the module. | -| `credentials` | `Option` | [`Credentials`](#type-credentials) to authenticate the user. | - -If `credentials` are supplied, they will be passed to the new connection to identify and authenticate the user. Otherwise, a set of [`Credentials`](#type-credentials) will be generated by the server. - -```rust -const MODULE_NAME: &str = "my-module-name"; - -// Connect to a local DB with a fresh identity -connect("http://localhost:3000", MODULE_NAME, None) - .expect("Connection failed"); - -// Connect to cloud with a fresh identity. -connect("https://testnet.spacetimedb.com", MODULE_NAME, None) - .expect("Connection failed"); - -// Connect with a saved identity -const CREDENTIALS_DIR: &str = ".my-module"; -connect( - "https://testnet.spacetimedb.com", - MODULE_NAME, - load_credentials(CREDENTIALS_DIR) - .expect("Error while loading credentials"), -).expect("Connection failed"); -``` - -### Function `disconnect` +### Connect to a module - `DbConnection::builder()` and `.build()` ```rust -spacetimedb_sdk::disconnect() +impl DbConnection { + fn builder() -> DbConnectionBuilder; +} ``` -Gracefully close the current WebSocket connection. +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. -If there is no active connection, this operation does nothing. +#### Method `with_uri` ```rust -connect(SPACETIMEDB_URI, MODULE_NAME, credentials) - .expect("Connection failed"); - -run_app(); - -disconnect(); +impl DbConnectionBuilder { + fn with_uri(self, uri: impl TryInto) -> Self; +} ``` -### Function `on_disconnect` - -```rust -spacetimedb_sdk::on_disconnect( - callback: impl FnMut() + Send + 'static, -) -> DisconnectCallbackId -``` - -Register a callback to be invoked when a connection ends. - -| Argument | Type | Meaning | -| ---------- | ------------------------------- | ------------------------------------------------------ | -| `callback` | `impl FnMut() + Send + 'static` | Callback to be invoked when subscriptions are applied. | - -The callback will be invoked after calling [`disconnect`](#function-disconnect), or when a connection is closed by the server. - -The returned `DisconnectCallbackId` can be passed to [`remove_on_disconnect`](#function-remove_on_disconnect) to unregister the callback. - -```rust -on_disconnect(|| println!("Disconnected!")); - -connect(SPACETIMEDB_URI, MODULE_NAME, credentials) - .expect("Connection failed"); - -disconnect(); - -// Will print "Disconnected!" -``` +Configure the URI of the SpacetimeDB instance or cluster which hosts the remote module. -### Function `once_on_disconnect` +#### Method `with_module_name` ```rust -spacetimedb_sdk::once_on_disconnect( - callback: impl FnOnce() + Send + 'static, -) -> DisconnectCallbackId +impl DbConnectionBuilder { + fn with_module_name(self, name_or_address: impl ToString) -> Self; +} ``` -Register a callback to be invoked the next time a connection ends. - -| Argument | Type | Meaning | -| ---------- | ------------------------------- | ------------------------------------------------------ | -| `callback` | `impl FnMut() + Send + 'static` | Callback to be invoked when subscriptions are applied. | - -The callback will be invoked after calling [`disconnect`](#function-disconnect), or when a connection is closed by the server. +Configure the SpacetimeDB domain name or address of the remote module which identifies it within the SpacetimeDB instance or cluster. -The callback will be unregistered after running. - -The returned `DisconnectCallbackId` can be passed to [`remove_on_disconnect`](#function-remove_on_disconnect) to unregister the callback. +#### Callback `on_connect` ```rust -once_on_disconnect(|| println!("Disconnected!")); - -connect(SPACETIMEDB_URI, MODULE_NAME, credentials) - .expect("Connection failed"); - -disconnect(); - -// Will print "Disconnected!" - -connect(SPACETIMEDB_URI, MODULE_NAME, credentials) - .expect("Connection failed"); - -disconnect(); - -// Nothing printed this time. +impl DbConnectionBuilder { + fn on_connect(self, callback: impl FnOnce(&DbConnection, Identity, &str)) -> DbConnectionBuilder; +} ``` -### Function `remove_on_disconnect` +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. -```rust -spacetimedb_sdk::remove_on_disconnect( - id: DisconnectCallbackId, -) -``` +This interface may change in an upcoming release as we rework SpacetimeDB's authentication model. -Unregister a previously-registered [`on_disconnect`](#function-on_disconnect) callback. +#### Callback `on_connect_error` -| Argument | Type | Meaning | -| -------- | ---------------------- | ------------------------------------------ | -| `id` | `DisconnectCallbackId` | Identifier for the callback to be removed. | +Currently unused. -If `id` does not refer to a currently-registered callback, this operation does nothing. +#### Callback `on_disconnect` ```rust -let id = on_disconnect(|| unreachable!()); - -remove_on_disconnect(id); - -disconnect(); - -// No `unreachable` panic. +impl DbConnectionBuilder { + fn on_disconnect(self, callback: impl FnOnce(&DbConnection, Option<&anyhow::Error>)) -> DbConnectionBuilder; +} ``` -## Subscribe to queries +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. -### Function `subscribe` +#### Method `with_credentials` ```rust -spacetimedb_sdk::subscribe(queries: &[&str]) -> anyhow::Result<()> +impl DbConnectionBuilder { + fn with_credentials(self, credentials: Option<(Identity, String)>) -> Self; +} ``` -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -| Argument | Type | Meaning | -| --------- | --------- | ---------------------------- | -| `queries` | `&[&str]` | SQL queries to subscribe to. | - -The `queries` should be a slice of strings representing SQL queries. - -`subscribe` will return an error if called before establishing a connection with the autogenerated [`connect`](#function-connect) function. In that case, the queries are not registered. - -`subscribe` does not return data directly. The SDK will generate types [`module_bindings::{TABLE}`](#type-table) corresponding to each of the tables in your module. These types implement the trait [`spacetimedb_sdk::table_type::TableType`](#trait-tabletype), which contains methods such as [`TableType::on_insert`](#method-on_insert). Use these methods to receive data from the queries you subscribe to. +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. -A new call to `subscribe` (or [`subscribe_owned`](#function-subscribe_owned)) 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 [`TableType::on_delete`](#method-on_delete) callbacks will be invoked for them. +This interface may change in an upcoming release as we rework SpacetimeDB's authentication model. -```rust -subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) - .expect("Called `subscribe` before `connect`"); -``` - -### Function `subscribe_owned` +#### Method `build` ```rust -spacetimedb_sdk::subscribe_owned(queries: Vec) -> anyhow::Result<()> +impl DbConnectionBuilder { + fn build(self) -> anyhow::Result; +} ``` -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -| Argument | Type | Meaning | -| --------- | ------------- | ---------------------------- | -| `queries` | `Vec` | SQL queries to subscribe to. | - -The `queries` should be a `Vec` of `String`s representing SQL queries. +After configuring the connection and registering callbacks, attempt to open the connection. -A new call to `subscribe_owned` (or [`subscribe`](#function-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 [`TableType::on_delete`](#method-on_delete) callbacks will be invoked for them. +### Advance the connection and process messages -`subscribe_owned` will return an error if called before establishing a connection with the autogenerated [`connect`](#function-connect) function. In that case, the queries are not registered. +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. -```rust -let query = format!("SELECT * FROM User WHERE name = '{}';", compute_my_name()); - -subscribe_owned(vec![query]) - .expect("Called `subscribe_owned` before `connect`"); -``` - -### Function `on_subscription_applied` +#### Run in the background - method `run_threaded` ```rust -spacetimedb_sdk::on_subscription_applied( - callback: impl FnMut() + Send + 'static, -) -> SubscriptionCallbackId +impl DbConnection { + fn run_threaded(&self) -> std::thread::JoinHandle<()>; +} ``` -Register a callback to be invoked the first time a subscription's matching rows becoming available. - -| Argument | Type | Meaning | -| ---------- | ------------------------------- | ------------------------------------------------------ | -| `callback` | `impl FnMut() + Send + 'static` | Callback to be invoked when subscriptions are applied. | - -The callback will be invoked after a successful [`subscribe`](#function-subscribe) or [`subscribe_owned`](#function-subscribe_owned) call when the initial set of matching rows becomes available. - -The returned `SubscriptionCallbackId` can be passed to [`remove_on_subscription_applied`](#function-remove_on_subscription_applied) to unregister the callback. - -```rust -on_subscription_applied(|| println!("Subscription applied!")); - -subscribe(&["SELECT * FROM User;"]) - .expect("Called `subscribe` before `connect`"); - -sleep(Duration::from_secs(1)); - -// Will print "Subscription applied!" - -subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) - .expect("Called `subscribe` before `connect`"); - -// Will print again. -``` +`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). -### Function `once_on_subscription_applied` +#### Run asynchronously - method `run_async` ```rust -spacetimedb_sdk::once_on_subscription_applied( - callback: impl FnOnce() + Send + 'static, -) -> SubscriptionCallbackId +impl DbConnection { + async fn run_async(&self) -> anyhow::Result<()>; +} ``` -Register a callback to be invoked the next time a subscription's matching rows become available. - -| Argument | Type | Meaning | -| ---------- | ------------------------------- | ------------------------------------------------------ | -| `callback` | `impl FnMut() + Send + 'static` | Callback to be invoked when subscriptions are applied. | - -The callback will be invoked after a successful [`subscribe`](#function-subscribe) or [`subscribe_owned`](#function-subscribe_owned) call when the initial set of matching rows becomes available. +`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). -The callback will be unregistered after running. - -The returned `SubscriptionCallbackId` can be passed to [`remove_on_subscription_applied`](#function-remove_on_subscription_applied) to unregister the callback. +#### Run on the main thread without blocking - method `frame_tick` ```rust -once_on_subscription_applied(|| println!("Subscription applied!")); - -subscribe(&["SELECT * FROM User;"]) - .expect("Called `subscribe` before `connect`"); - -sleep(Duration::from_secs(1)); - -// Will print "Subscription applied!" - -subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) - .expect("Called `subscribe` before `connect`"); - -// Nothing printed this time. +impl DbConnection { + fn frame_tick(&self) -> anyhow::Result<()>; +} ``` -### Function `remove_on_subscription_applied` - -```rust -spacetimedb_sdk::remove_on_subscription_applied( - id: SubscriptionCallbackId, -) -``` - -Unregister a previously-registered [`on_subscription_applied`](#function-on_subscription_applied) callback. - -| Argument | Type | Meaning | -| -------- | ------------------------ | ------------------------------------------ | -| `id` | `SubscriptionCallbackId` | Identifier for the callback to be removed. | - -If `id` does not refer to a currently-registered callback, this operation does nothing. - -```rust -let id = on_subscription_applied(|| println!("Subscription applied!")); - -subscribe(&["SELECT * FROM User;"]) - .expect("Called `subscribe` before `connect`"); - -sleep(Duration::from_secs(1)); +`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. -// Will print "Subscription applied!" +## Trait `spacetimedb_sdk::DbContext` -remove_on_subscription_applied(id); +[`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext) both implement `DbContext`, which allows -subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) - .expect("Called `subscribe` before `connect`"); - -// Nothing printed this time. -``` - -## Identify a client - -### Type `Identity` +### Method `disconnect` ```rust -spacetimedb_sdk::identity::Identity +trait DbContext { + fn disconnect(&self) -> anyhow::Result<()>; +} ``` -A unique public identifier for a client connected to a database. - -### Type `Token` +Gracefully close the `DbConnection`. Returns an `Err` if the connection is already disconnected. -```rust -spacetimedb_sdk::identity::Token -``` +### Subscribe to queries - `DbContext::subscription_builder` and `.subscribe()` -A private access token for a client connected to a database. +This interface is subject to change in an upcoming SpacetimeDB release. -### Type `Credentials` +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`. ```rust -spacetimedb_sdk::identity::Credentials +trait DbContext { + fn subscription_builder(&self) -> SubscriptionBuilder; +} ``` -Credentials, including a private access token, sufficient to authenticate a client connected to a database. +Subscribe to queries by calling `ctx.subscription_builder()` and chaining configuration methods, then calling `.subscribe(queries)`. -| Field | Type | -| ---------- | ---------------------------- | -| `identity` | [`Identity`](#type-identity) | -| `token` | [`Token`](#type-token) | - -### Type `Address` +#### Callback `on_applied` ```rust -spacetimedb_sdk::Address +impl SubscriptionBuilder { + fn on_applied(self, callback: impl FnOnce(&EventContext)) -> Self; +} ``` -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity). +Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache. The [`EventContext`](#type-module_bindings-eventcontext) passed to the callback will have `Event::SubscribeApplied` as its `event`. -### Function `identity` +#### Method `subscribe` ```rust -spacetimedb_sdk::identity::identity() -> Result +impl SubscriptionBuilder { + fn subscribe(self, queries: impl IntoQueries) -> SubscriptionHandle; +} ``` -Read the current connection's public [`Identity`](#type-identity). +Subscribe to a set of queries. `queries` should be an array or slice of strings. -Returns an error if: +The returned `SubscriptionHandle` is currently not useful, but will become significant in a future version of SpacetimeDB. -- [`connect`](#function-connect) has not yet been called. -- We connected anonymously, and we have not yet received our credentials. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -sleep(Duration::from_secs(1)); - -println!("My identity is {:?}", identity()); - -// Prints "My identity is Ok(Identity { bytes: [...several u8s...] })" -``` +### Identity a client -### Function `token` +#### Method `identity` ```rust -spacetimedb_sdk::identity::token() -> Result +trait DbContext { + fn identity(&self) -> Identity; +} ``` -Read the current connection's private [`Token`](#type-token). +Get the `Identity` with which SpacetimeDB identifies the connection. This method may panic if the connection was initiated anonymously and the newly-generated `Identity` has not yet been received, i.e. if called before the [`on_connect` callback](#callback-on_connect) is invoked. -Returns an error if: - -- [`connect`](#function-connect) has not yet been called. -- We connected anonymously, and we have not yet received our credentials. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -sleep(Duration::from_secs(1)); - -println!("My token is {:?}", token()); - -// Prints "My token is Ok(Token {string: "...several Base64 digits..." })" -``` - -### Function `credentials` +#### Method `try_identity` ```rust -spacetimedb_sdk::identity::credentials() -> Result +trait DbContext { + fn try_identity(&self) -> Option; +} ``` -Read the current connection's [`Credentials`](#type-credentials), including a public [`Identity`](#type-identity) and a private [`Token`](#type-token). +Like [`DbContext::identity`](#method-identity), but returns `None` instead of panicking if the `Identity` is not yet available. -Returns an error if: - -- [`connect`](#function-connect) has not yet been called. -- We connected anonymously, and we have not yet received our credentials. +#### Method `is_active` ```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -sleep(Duration::from_secs(1)); - -println!("My credentials are {:?}", credentials()); - -// Prints "My credentials are Ok(Credentials { -// identity: Identity { bytes: [...several u8s...] }, -// token: Token { string: "...several Base64 digits..."}, -// })" +trait DbContext { + fn is_active(&self) -> bool; +} ``` -### Function `address` - -```rust -spacetimedb_sdk::identity::address() -> Result
-``` - -Read the current connection's [`Address`](#type-address). - -Returns an error if [`connect`](#function-connect) has not yet been called. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -sleep(Duration::from_secs(1)); - -println!("My address is {:?}", address()); -``` +`true` if the connection has not yet disconnected. Note that a connection `is_active` when it is constructed, before its [`on_connect` callback](#callback-on_connect) is invoked. -### Function `on_connect` +## Type `EventContext` ```rust -spacetimedb_sdk::identity::on_connect( - callback: impl FnMut(&Credentials, Address) + Send + 'static, -) -> ConnectCallbackId +module_bindings::EventContext ``` -Register a callback to be invoked upon authentication with the database. - -| Argument | Type | Meaning | -|------------|----------------------------------------------------|--------------------------------------------------------| -| `callback` | `impl FnMut(&Credentials, Address) + Send + 'sync` | Callback to be invoked upon successful authentication. | - -The callback will be invoked with the [`Credentials`](#type-credentials) and [`Address`](#type-address) provided by the database to identify this connection. If [`Credentials`](#type-credentials) were supplied to [`connect`](#function-connect), those passed to the callback will be equivalent to the ones used to connect. If the initial connection was anonymous, a new set of [`Credentials`](#type-credentials) will be generated by the database to identify this user. - -The [`Credentials`](#type-credentials) passed to the callback can be saved and used to authenticate the same user in future connections. +An `EventContext` is a [`DbContext`](#trait-dbcontext) augmented with a field `event: Event`. -The returned `ConnectCallbackId` can be passed to [`remove_on_connect`](#function-remove_on_connect) to unregister the callback. +### Enum `Event` ```rust -on_connect( - |creds, addr| - println!("Successfully connected! My credentials are: {:?} and my address is: {:?}", creds, addr) -); - -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -sleep(Duration::from_secs(1)); - -// Will print "Successfully connected! My credentials are: " -// followed by a printed representation of the client's `Credentials`. +spacetimedb_sdk::Event ``` -### Function `once_on_connect` +#### Variant `Reducer` ```rust -spacetimedb_sdk::identity::once_on_connect( - callback: impl FnOnce(&Credentials, Address) + Send + 'static, -) -> ConnectCallbackId +spacetimedb_sdk::Event::Reducer(spacetimedb_sdk::ReducerEvent) ``` -Register a callback to be invoked once upon authentication with the database. - -| Argument | Type | Meaning | -|------------|-----------------------------------------------------|------------------------------------------------------------------| -| `callback` | `impl FnOnce(&Credentials, Address) + Send + 'sync` | Callback to be invoked once upon next successful authentication. | - -The callback will be invoked with the [`Credentials`](#type-credentials) and [`Address`](#type-address) provided by the database to identify this connection. If [`Credentials`](#type-credentials) were supplied to [`connect`](#function-connect), those passed to the callback will be equivalent to the ones used to connect. If the initial connection was anonymous, a new set of [`Credentials`](#type-credentials) will be generated by the database to identify this user. +Event when we are notified that a reducer ran in the remote module. The [`ReducerEvent`](#struct-reducerevent) contains metadata about the reducer run, including its arguments and termination [`Status`](#enum-status). -The [`Credentials`](#type-credentials) passed to the callback can be saved and used to authenticate the same user in future connections. +This event is passed to reducer callbacks, and to row callbacks resulting from modifications by the reducer. -The callback will be unregistered after running. - -The returned `ConnectCallbackId` can be passed to [`remove_on_connect`](#function-remove_on_connect) to unregister the callback. - -### Function `remove_on_connect` +#### Variant `SubscribeApplied` ```rust -spacetimedb_sdk::identity::remove_on_connect(id: ConnectCallbackId) +spacetimedb_sdk::Event::SubscribeApplied ``` -Unregister a previously-registered [`on_connect`](#function-on_connect) or [`once_on_connect`](#function-once_on_connect) callback. - -| Argument | Type | Meaning | -| -------- | ------------------- | ------------------------------------------ | -| `id` | `ConnectCallbackId` | Identifier for the callback to be removed. | +Event when our subscription is applied and its rows are inserted into the client cache. -If `id` does not refer to a currently-registered callback, this operation does nothing. - -```rust -let id = on_connect(|_creds, _addr| unreachable!()); +This event is passed to [subscription `on_applied` callbacks](#callback-on_applied), and to [row `on_insert` callbacks](#callback-on_insert) resulting from the new subscription. -remove_on_connect(id); +#### Variant `UnsubscribeApplied` -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); +Currently unused. -sleep(Duration::from_secs(1)); +#### Variant `SubscribeError` -// No `unreachable` panic. -``` - -### Function `load_credentials` - -```rust -spacetimedb_sdk::identity::load_credentials( - dirname: &str, -) -> Result> -``` +Currently unused. -Load a saved [`Credentials`](#type-credentials) from a file within `~/dirname`, if one exists. +#### Variant `UnknownTransaction` -| Argument | Type | Meaning | -| --------- | ------ | ----------------------------------------------------- | -| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | +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. -`dirname` is treated as a directory in the user's home directory. If it contains a file named `credentials`, that file is treated as a BSATN-encoded [`Credentials`](#type-credentials), deserialized and returned. These files are created by [`save_credentials`](#function-save_credentials) with the same `dirname` argument. +This event is passed to row callbacks resulting from modifications by the transaction. -Returns `Ok(None)` if the directory or the credentials file does not exist. Returns `Err` when IO or deserialization fails. The returned `Result` may be unwrapped, and the contained `Option` passed to [`connect`](#function-connect). +### Struct `ReducerEvent` ```rust -const CREDENTIALS_DIR = ".my-module"; - -let creds = load_credentials(CREDENTIALS_DIR) - .expect("Error while loading credentials"); - -connect(SPACETIMEDB_URI, DB_NAME, creds) - .expect("Failed to connect"); +spacetimedb_sdk::ReducerEvent ``` -### Function `save_credentials` +A `ReducerEvent` contains metadata about a reducer run. ```rust -spacetimedb_sdk::identity::save_credentials( - dirname: &str, - credentials: &Credentials, -) -> Result<()> -``` - -Store a [`Credentials`](#type-credentials) to a file within `~/dirname`, to be later loaded with [`load_credentials`](#function-load_credentials). +struct spacetimedb_sdk::ReducerEvent { + /// The time at which the reducer was invoked. + timestamp: SystemTime, -| Argument | Type | Meaning | -| ------------- | -------------- | ----------------------------------------------------- | -| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | -| `credentials` | `&Credentials` | [`Credentials`](#type-credentials) to store. | + /// Whether the reducer committed, was aborted due to insufficient energy, or failed with an error message. + status: Status, -`dirname` is treated as a directory in the user's home directory. The directory is created if it does not already exists. A file within it named `credentials` is created or replaced, containing `creds` encoded as BSATN. The saved credentials can be retrieved by [`load_credentials`](#function-load_credentials) with the same `dirname` argument. + /// The `Identity` of the SpacetimeDB actor which invoked the reducer. + caller_identity: Identity, -Returns `Err` when IO or serialization fails. + /// The `Address` of the SpacetimeDB actor which invoked the reducer, + /// or `None` if the actor did not supply an address. + caller_address: Option
, -```rust -const CREDENTIALS_DIR = ".my-module"; - -let creds = load_credentials(CREDENTIALS_DIRectory) - .expect("Error while loading credentials"); + /// The amount of energy consumed by the reducer run, in eV. + /// (Not literal eV, but our SpacetimeDB energy unit eV.) + /// + /// May be `None` if the module is configured not to broadcast energy consumed. + energy_consumed: Option, -on_connect(|creds, _addr| { - if let Err(e) = save_credentials(CREDENTIALS_DIR, creds) { - eprintln!("Error while saving credentials: {:?}", e); - } -}); + /// The `Reducer` enum defined by the `module_bindings`, which encodes which reducer ran and its arguments. + reducer: R, -connect(SPACETIMEDB_URI, DB_NAME, creds) - .expect("Failed to connect"); + // ...private fields +} ``` -## View subscribed rows of tables - -### Type `{TABLE}` +### Enum `Status` ```rust -module_bindings::{TABLE} +spacetimedb_sdk::Status ``` -For each table defined by a module, `spacetime generate` generates a struct in the `module_bindings` mod whose name is that table's name converted to `PascalCase`. The generated struct has a field for each of the table's columns, whose names are the column names converted to `snake_case`. - -### Method `filter_by_{COLUMN}` +#### Variant `Committed` ```rust -module_bindings::{TABLE}::filter_by_{COLUMN}( - value: {COLUMN_TYPE}, -) -> impl Iterator +spacetimedb_sdk::Status::Committed ``` -For each column of a table, `spacetime generate` generates a static method on the [table struct](#type-table) to filter subscribed rows where that column matches a requested value. - -These methods are named `filter_by_{COLUMN}`, where `{COLUMN}` is the column name converted to `snake_case`. The method's return type is an `Iterator` over the `{TABLE}` rows which match the requested value. +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`](#struct-reducerevent). -### Method `find_by_{COLUMN}` +#### Variant `Failed` ```rust -module_bindings::{TABLE}::find_by_{COLUMN}( - value: {COLUMN_TYPE}, -) -> {FILTER_RESULT}<{TABLE}> +spacetimedb_sdk::Status::Failed(Box) ``` -For each unique column of a table (those annotated `#[unique]` and `#[primarykey]`), `spacetime generate` generates a static method on the [table struct](#type-table) to seek a subscribed row where that column matches a requested value. +The reducer returned an error, panicked, or threw an exception. The enum 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. -These methods are named `find_by_{COLUMN}`, where `{COLUMN}` is the column name converted to `snake_case`. The method's return type is `Option<{TABLE}>`. +#### Variant `OutOfEnergy` -### Trait `TableType` +The reducer was aborted due to insufficient energy balance of the module owner. -```rust -spacetimedb_sdk::table::TableType -``` - -Every [generated table struct](#type-table) implements the trait `TableType`. - -#### Method `count` +### Enum `Reducer` ```rust -TableType::count() -> usize +module_bindings::Reducer ``` -Return the number of subscribed rows in the table, or 0 if there is no active connection. +The module bindings contains an enum `Reducer` with a variant for each reducer defined by the module. Each variant has a payload containing the arguments to the reducer. -This method acquires a global lock. +## Access the client cache -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -on_subscription_applied(|| println!("There are {} users", User::count())); +Both [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext) have fields `.db`, which in turn has methods for accessing tables in the client cache. The trait method `DbContext::db(&self)` can also be used in contexts with an `impl DbContext` rather than a concrete-typed `EventContext` or `DbConnection`. -subscribe(&["SELECT * FROM User;"]) - .unwrap(); +Each table defined by a module has an accessor method, whose name is the table name converted to `snake_case`, on this `.db` field. The methods are defined via extension traits, which `rustc` or your IDE should help you identify and import where necessary. The table accessor methods return table handles, which implement [`Table`](#trait-table), may implement [`TableWithPrimaryKey`](#trait-tablewithprimarykey), and have methods for searching by unique index. -sleep(Duration::from_secs(1)); - -// Will the number of `User` rows in the database. -``` - -#### Method `iter` +### Trait `Table` ```rust -TableType::iter() -> impl Iterator +spacetimedb_sdk::Table ``` -Iterate over all the subscribed rows in the table. - -This method acquires a global lock, but the iterator does not hold it. - -This method must heap-allocate enough memory to hold all of the rows being iterated over. [`TableType::filter`](#method-filter) allocates significantly less, so prefer it when possible. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -on_subscription_applied(|| for user in User::iter() { - println!("{:?}", user); -}); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -sleep(Duration::from_secs(1)); - -// Will print a line for each `User` row in the database. -``` - -#### Method `filter` - -```rust -TableType::filter( - predicate: impl FnMut(&Self) -> bool, -) -> impl Iterator -``` - -Iterate over the subscribed rows in the table for which `predicate` returns `true`. - -| Argument | Type | Meaning | -| ----------- | --------------------------- | ------------------------------------------------------------------------------- | -| `predicate` | `impl FnMut(&Self) -> bool` | Test which returns `true` if a row should be included in the filtered iterator. | - -This method acquires a global lock, and the `predicate` runs while the lock is held. The returned iterator does not hold the lock. - -The `predicate` is called eagerly for each subscribed row in the table, even if the returned iterator is never consumed. +Implemented by all table handles. -This method must heap-allocate enough memory to hold all of the matching rows, but does not allocate space for subscribed rows which do not match the `predicate`. - -Client authors should prefer calling [tables' generated `filter_by_{COLUMN}` methods](#method-filter_by_column) when possible rather than calling `TableType::filter`. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -on_subscription_applied(|| { - for user in User::filter(|user| user.age >= 30 - && user.country == Country::USA) { - println!("{:?}", user); - } -}); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -sleep(Duration::from_secs(1)); - -// Will print a line for each `User` row in the database -// who is at least 30 years old and who lives in the United States. -``` - -#### Method `find` +#### Associated type `Row` ```rust -TableType::find( - predicate: impl FnMut(&Self) -> bool, -) -> Option +trait spacetimedb_sdk::Table { + type Table::Row; +} ``` -Locate a subscribed row for which `predicate` returns `true`, if one exists. - -| Argument | Type | Meaning | -| ----------- | --------------------------- | ------------------------------------------------------ | -| `predicate` | `impl FnMut(&Self) -> bool` | Test which returns `true` if a row should be returned. | - -This method acquires a global lock. - -If multiple subscribed rows match `predicate`, one is chosen arbitrarily. The choice may not be stable across different calls to `find` with the same `predicate`. - -Client authors should prefer calling [tables' generated `find_by_{COLUMN}` methods](#method-find_by_column) when possible rather than calling `TableType::find`. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -on_subscription_applied(|| { - if let Some(tyler) = User::find(|user| user.first_name == "Tyler" - && user.surname == "Cloutier") { - println!("Found Tyler: {:?}", tyler); - } else { - println!("Tyler isn't registered :("); - } -}); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -sleep(Duration::from_secs(1)); - -// Will tell us whether Tyler Cloutier is registered in the database. -``` +The type of rows in the table. -#### Method `on_insert` +#### Method `count` ```rust -TableType::on_insert( - callback: impl FnMut(&Self, Option<&ReducerEvent>) + Send + 'static, -) -> InsertCallbackId +trait spacetimedb_sdk::Table { + fn count(&self) -> u64; +} ``` -Register an `on_insert` callback for when a subscribed row is newly inserted into the database. - -| Argument | Type | Meaning | -| ---------- | ----------------------------------------------------------- | ------------------------------------------------------ | -| `callback` | `impl FnMut(&Self, Option<&ReducerEvent>) + Send + 'static` | Callback to run whenever a subscribed row is inserted. | - -The callback takes two arguments: - -- `row: &Self`, the newly-inserted row value. -- `reducer_event: Option<&ReducerEvent>`, the [`ReducerEvent`](#type-reducerevent) which caused this row to be inserted, or `None` if this row is being inserted while initializing a subscription. - -The returned `InsertCallbackId` can be passed to [`remove_on_insert`](#method-remove_on_insert) to remove the callback. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -User::on_insert(|user, reducer_event| { - if let Some(reducer_event) = reducer_event { - println!("New user inserted by reducer {:?}: {:?}", reducer_event, user); - } else { - println!("New user received during subscription update: {:?}", user); - } -}); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); +Returns the number of rows of this table resident in the client cache, i.e. the total number which match any subscribed query. -sleep(Duration::from_secs(1)); - -// Will print a note whenever a new `User` row is inserted. -``` - -#### Method `remove_on_insert` +#### Method `iter` ```rust -TableType::remove_on_insert(id: InsertCallbackId) +trait spacetimedb_sdk::Table { + fn iter(&self) -> impl Iterator; +} ``` -Unregister a previously-registered [`on_insert`](#method-on_insert) callback. - -| Argument | Type | Meaning | -| -------- | ------------------------ | ----------------------------------------------------------------------- | -| `id` | `InsertCallbackId` | Identifier for the [`on_insert`](#method-on_insert) callback to remove. | +An iterator over all the subscribed rows in the client cache, i.e. those which match any subscribed query. -If `id` does not refer to a currently-registered callback, this operation does nothing. +#### Callback `on_insert` ```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -let id = User::on_insert(|_, _| unreachable!()); - -User::remove_on_insert(id); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -sleep(Duration::from_secs(1)); +trait spacetimedb_sdk::Table { + type InsertCallbackId; + + fn on_insert(&self, callback: impl FnMut(&EventContext, &Self::Row)) -> Self::InsertCallbackId; -// No `unreachable` panic. + fn remove_on_insert(&self, callback: Self::InsertCallbackId); +} ``` -#### Method `on_delete` +The `on_insert` 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`](#enum-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. -```rust -TableType::on_delete( - callback: impl FnMut(&Self, Option<&ReducerEvent>) + Send + 'static, -) -> DeleteCallbackId -``` - -Register an `on_delete` callback for when a subscribed row is removed from the database. - -| Argument | Type | Meaning | -| ---------- | ----------------------------------------------------------- | ----------------------------------------------------- | -| `callback` | `impl FnMut(&Self, Option<&ReducerEvent>) + Send + 'static` | Callback to run whenever a subscribed row is deleted. | - -The callback takes two arguments: +Registering an `on_insert` callback returns a callback id, which can later be passed to `remove_on_insert` to cancel the callback. Newly registered or canceled callbacks do not take effect until the following event. -- `row: &Self`, the previously-present row which is no longer resident in the database. -- `reducer_event: Option<&ReducerEvent>`, the [`ReducerEvent`](#type-reducerevent) which caused this row to be deleted, or `None` if this row was previously subscribed but no longer matches the new queries while initializing a subscription. - -The returned `DeleteCallbackId` can be passed to [`remove_on_delete`](#method-remove_on_delete) to remove the callback. +#### Callback `on_delete` ```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -User::on_delete(|user, reducer_event| { - if let Some(reducer_event) = reducer_event { - println!("User deleted by reducer {:?}: {:?}", reducer_event, user); - } else { - println!("User no longer subscribed during subscription update: {:?}", user); - } -}); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -// Invoke a reducer which will delete a `User` row. -delete_user_by_name("Tyler Cloutier".to_string()); - -sleep(Duration::from_secs(1)); +trait spacetimedb_sdk::Table { + type DeleteCallbackId; + + fn on_delete(&self, callback: impl FnMut(&EventContext, &Self::Row)) -> Self::DeleteCallbackId; -// Will print a note whenever a `User` row is inserted, -// including "User deleted by reducer ReducerEvent::DeleteUserByName( -// DeleteUserByNameArgs { name: "Tyler Cloutier" } -// ): User { first_name: "Tyler", surname: "Cloutier" }" + fn remove_on_delete(&self, callback: Self::DeleteCallbackId); +} ``` -#### Method `remove_on_delete` - -```rust -TableType::remove_on_delete(id: DeleteCallbackId) -``` - -Unregister a previously-registered [`on_delete`](#method-on_delete) callback. - -| Argument | Type | Meaning | -| -------- | ------------------------ | ----------------------------------------------------------------------- | -| `id` | `DeleteCallbackId` | Identifier for the [`on_delete`](#method-on_delete) callback to remove. | - -If `id` does not refer to a currently-registered callback, this operation does nothing. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -let id = User::on_delete(|_, _| unreachable!()); - -User::remove_on_delete(id); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -// Invoke a reducer which will delete a `User` row. -delete_user_by_name("Tyler Cloutier".to_string()); - -sleep(Duration::from_secs(1)); - -// No `unreachable` panic. -``` +The `on_delete` callback runs whenever a previously-resident row is deleted from the client cache. Registering an `on_delete` callback returns a callback id, which can later be passed to `remove_on_delete` to cancel the callback. Newly registered or canceled callbacks do not take effect until the following event. ### Trait `TableWithPrimaryKey` ```rust -spacetimedb_sdk::table::TableWithPrimaryKey -``` - -[Generated table structs](#type-table) with a column designated `#[primarykey]` implement the trait `TableWithPrimaryKey`. - -#### Method `on_update` - -```rust -TableWithPrimaryKey::on_update( - callback: impl FnMut(&Self, &Self, Option<&Self::ReducerEvent>) + Send + 'static, -) -> UpdateCallbackId -``` - -Register an `on_update` callback for when an existing row is modified. - -| Argument | Type | Meaning | -| ---------- | ------------------------------------------------------------------ | ----------------------------------------------------- | -| `callback` | `impl FnMut(&Self, &Self, Option<&ReducerEvent>) + Send + 'static` | Callback to run whenever a subscribed row is updated. | - -The callback takes three arguments: - -- `old: &Self`, the previous row value which has been replaced in the database. -- `new: &Self`, the updated row value which is now resident in the database. -- `reducer_event: Option<&ReducerEvent>`, the [`ReducerEvent`](#type-reducerevent) which caused this row to be inserted. - -The returned `UpdateCallbackId` can be passed to [`remove_on_update`](#method-remove_on_update) to remove the callback. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -User::on_update(|old, new, reducer_event| { - println!("User updated by reducer {:?}: from {:?} to {:?}", reducer_event, old, new); -}); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -// Prints a line whenever a `User` row is updated by primary key. -``` - -#### Method `remove_on_update` - -```rust -TableWithPrimaryKey::remove_on_update(id: UpdateCallbackId) +spacetimedb_sdk::TableWithPrimaryKey ``` -| Argument | Type | Meaning | -| -------- | ------------------------ | ----------------------------------------------------------------------- | -| `id` | `UpdateCallbackId` | Identifier for the [`on_update`](#method-on_update) callback to remove. | - -Unregister a previously-registered [`on_update`](#method-on_update) callback. +Implemented for table handles whose tables have a primary key. -If `id` does not refer to a currently-registered callback, this operation does nothing. +#### Callback `on_delete` ```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -let id = User::on_update(|_, _, _| unreachable!); - -User::remove_on_update(id); +trait spacetimedb_sdk::TableWithPrimaryKey { + type UpdateCallbackId; + + fn on_update(&self, callback: impl FnMut(&EventContext, &Self::Row, &Self::Row)) -> Self::UpdateCallbackId; -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -// No `unreachable` panic. + fn remove_on_update(&self, callback: Self::UpdateCallbackId); +} ``` -## Observe and request reducer invocations +The `on_update` 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. Registering an `on_update` callback returns a callback id, which can later be passed to `remove_on_update` to cancel the callback. Newly registered or canceled callbacks do not take effect until the following event. -### Type `ReducerEvent` +### Unique constraint index access -```rust -module_bindings::ReducerEvent -``` +For each unique constraint on a table, its table handle has a method whose name is the unique column name which returns a unique index handle. The unique index handle has a method `.find(desired_val: &Col) -> Option`, where `Col` is the type of the column, and `Row` the type of rows. If a row with `desired_val` in the unique column is resident in the client cache, `.find` returns it. -`spacetime generate` defines an enum `ReducerEvent` with a variant for each reducer defined by a module. The variant's name will be the reducer's name converted to `PascalCase`, and the variant will hold an instance of [the autogenerated reducer arguments struct for that reducer](#type-reducerargs). +### BTree index access -[`on_insert`](#method-on_insert), [`on_delete`](#method-on_delete) and [`on_update`](#method-on_update) callbacks accept an `Option<&ReducerEvent>` which identifies the reducer which caused the row to be inserted, deleted or updated. +Not currently implemented in the Rust SDK. Coming soon! -### Type `{REDUCER}Args` +## Observe and invoke reducers -```rust -module_bindings::{REDUCER}Args -``` +Both [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext) have fields `.reducers`, which in turn has methods for invoking reducers defined by the module and registering callbacks on it. The trait method `DbContext::reducers(&self)` can also be used in contexts with an `impl DbContext` rather than a concrete-typed `EventContext` or `DbConnection`. -For each reducer defined by a module, `spacetime generate` generates a struct whose name is that reducer's name converted to `PascalCase`, suffixed with `Args`. The generated struct has a field for each of the reducer's arguments, whose names are the argument names converted to `snake_case`. +Each reducer defined by the module has three methods on the `.reducers`: -For reducers which accept a `ReducerContext` as their first argument, the `ReducerContext` is not included in the arguments struct. - -### Function `{REDUCER}` - -```rust -module_bindings::{REDUCER}({ARGS...}) -``` +- An invoke method, whose name is the reducer's name converted to snake case. This requests that the module run the reducer. +- A callback registation method, whose name is prefixed with `on_`. 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_`. This cancels a callback previously registered via the callback registration method. -For each reducer defined by a module, `spacetime generate` generates a function which sends a request to the database to invoke that reducer. The generated function's name is the reducer's name converted to `snake_case`. - -For reducers which accept a `ReducerContext` as their first argument, the `ReducerContext` is not included in the generated function's argument list. - -### Function `on_{REDUCER}` - -```rust -module_bindings::on_{REDUCER}( - callback: impl FnMut(&Identity, Option
, Status, {&ARGS...}) + Send + 'static, -) -> ReducerCallbackId<{REDUCER}Args> -``` - -For each reducer defined by a module, `spacetime generate` generates a function which registers a `FnMut` callback to run each time the reducer is invoked. The generated functions are named `on_{REDUCER}`, where `{REDUCER}` is the reducer's name converted to `snake_case`. - -| Argument | Type | Meaning | -| ---------- | ------------------------------------------------------------- | ------------------------------------------------ | -| `callback` | `impl FnMut(&Identity, Option
&Status, {&ARGS...}) + Send + 'static` | Callback to run whenever the reducer is invoked. | - -The callback always accepts three arguments: - -- `caller_id: &Identity`, the [`Identity`](#type-identity) of the client which invoked the reducer. -- `caller_address: Option
`, the [`Address`](#type-address) of the client which invoked the reducer. This may be `None` for scheduled reducers. - -In addition, the callback accepts a reference to each of the reducer's arguments. - -Clients will only be notified of reducer runs if either of two criteria is met: - -- The reducer inserted, deleted or updated at least one row to which the client is subscribed. -- The reducer invocation was requested by this client, and the run failed. - -The `on_{REDUCER}` function returns a `ReducerCallbackId<{REDUCER}Args>`, where `{REDUCER}Args` is the [generated reducer arguments struct](#type-reducerargs). This `ReducerCallbackId` can be passed to the [generated `remove_on_{REDUCER}` function](#function-remove_on_reducer) to cancel the callback. - -### Function `once_on_{REDUCER}` - -```rust -module_bindings::once_on_{REDUCER}( - callback: impl FnOnce(&Identity, Option
, &Status, {&ARGS...}) + Send + 'static, -) -> ReducerCallbackId<{REDUCER}Args> -``` - -For each reducer defined by a module, `spacetime generate` generates a function which registers a `FnOnce` callback to run the next time the reducer is invoked. The generated functions are named `once_on_{REDUCER}`, where `{REDUCER}` is the reducer's name converted to `snake_case`. - -| Argument | Type | Meaning | -| ---------- | -------------------------------------------------------------- | ----------------------------------------------------- | -| `callback` | `impl FnOnce(&Identity, Option
, &Status, {&ARGS...}) + Send + 'static` | Callback to run the next time the reducer is invoked. | - -The callback accepts the same arguments as an [on-reducer callback](#function-on_reducer), but may be a `FnOnce` rather than a `FnMut`. - -The callback will be invoked in the same circumstances as an on-reducer callback. - -The `once_on_{REDUCER}` function returns a `ReducerCallbackId<{REDUCER}Args>`, where `{REDUCER}Args` is the [generated reducer arguments struct](#type-reducerargs). This `ReducerCallbackId` can be passed to the [generated `remove_on_{REDUCER}` function](#function-remove_on_reducer) to cancel the callback. +## Identify a client -### Function `remove_on_{REDUCER}` +### Type `Identity` ```rust -module_bindings::remove_on_{REDUCER}(id: ReducerCallbackId<{REDUCER}Args>) +spacetimedb_sdk::Identity ``` -For each reducer defined by a module, `spacetime generate` generates a function which unregisters a previously-registered [on-reducer](#function-on_reducer) or [once-on-reducer](#function-once_on_reducer) callback. - -| Argument | Type | Meaning | -| -------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | -| `id` | `UpdateCallbackId` | Identifier for the [`on_{REDUCER}`](#function-on_reducer) or [`once_on_{REDUCER}`](#function-once_on_reducer) callback to remove. | - -If `id` does not refer to a currently-registered callback, this operation does nothing. +A unique public identifier for a client connected to a database. -### Type `Status` +### Type `Address` ```rust -spacetimedb_sdk::reducer::Status +spacetimedb_sdk::Address ``` -An enum whose variants represent possible reducer completion statuses. - -A `Status` is passed as the second argument to [`on_{REDUCER}`](#function-on_reducer) and [`once_on_{REDUCER}`](#function-once_on_reducer) callbacks. - -#### Variant `Status::Committed` - -The reducer finished successfully, and its row changes were committed to the database. - -#### Variant `Status::Failed(String)` - -The reducer failed, either by panicking or returning an `Err`. - -| Field | Type | Meaning | -| ----- | -------- | --------------------------------------------------- | -| 0 | `String` | The error message which caused the reducer to fail. | - -#### Variant `Status::OutOfEnergy` - -The reducer was canceled because the module owner had insufficient energy to allow it to run to completion. +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity). This will be removed in a future SpacetimeDB version in favor of a connection or session ID. diff --git a/docs/sdks/rust/quickstart.md b/docs/sdks/rust/quickstart.md index af07e40..38d9dee 100644 --- a/docs/sdks/rust/quickstart.md +++ b/docs/sdks/rust/quickstart.md @@ -28,7 +28,7 @@ cargo new client Below the `[dependencies]` line in `client/Cargo.toml`, add: ```toml -spacetimedb-sdk = "0.7" +spacetimedb-sdk = "0.12" hex = "0.4" ``` @@ -56,18 +56,20 @@ mkdir -p client/src/module_bindings spacetime generate --lang rust --out-dir client/src/module_bindings --project-path server ``` -Take a look inside `client/src/module_bindings`. The CLI should have generated five files: +Take a look inside `client/src/module_bindings`. The CLI should have generated a few files: ``` module_bindings -├── message.rs +├── message_table.rs +├── message_type.rs ├── mod.rs ├── send_message_reducer.rs ├── set_name_reducer.rs -└── user.rs +├── user_table.rs +└── user_type.rs ``` -We need to declare the module in our client crate, and we'll want to import its definitions. +To use these, we'll declare the module in our client crate and import its definitions. To `client/src/main.rs`, add: @@ -78,43 +80,33 @@ use module_bindings::*; ## Add more imports -We'll need a whole boatload of imports from `spacetimedb_sdk`, which we'll describe when we use them. +We'll need additional imports from `spacetimedb_sdk` for interacting with the database, handling credentials, and managing events. To `client/src/main.rs`, add: ```rust -use spacetimedb_sdk::{ - Address, - disconnect, - identity::{load_credentials, once_on_connect, save_credentials, Credentials, Identity}, - on_disconnect, on_subscription_applied, - reducer::Status, - subscribe, - table::{TableType, TableWithPrimaryKey}, -}; +use spacetimedb_sdk::{anyhow, DbContext, Event, Identity, Status, Table, TableWithPrimaryKey}; +use spacetimedb_sdk::credentials::File; ``` -## Define main function +## Define the main function -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 five things: +Our `main` function will do the following: +1. Connect to the database. This will also start a new thread for handling network messages. +2. Handle user input from the command line. -1. 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. Establish a connection to the database. This will involve authenticating with our credentials, if we're a returning user. -3. Subscribe to receive updates on tables. -4. Loop, processing user input from standard input. This will be how we enable users to set their names and send messages. -5. Close our connection. This one is easy; we just call `spacetimedb_sdk::disconnect`. - -To `client/src/main.rs`, add: +We'll see the implementation of these functions a bit later, but for now add to `client/src/main.rs`: ```rust fn main() { - register_callbacks(); - connect_to_db(); - subscribe_to_tables(); - user_input_loop(); + // Connect to the database + let conn = connect_to_db(); + // Handle CLI input + user_input_loop(&conn); } ``` + ## Register callbacks We need to handle several sorts of events: @@ -132,69 +124,89 @@ To `client/src/main.rs`, add: ```rust /// Register all the callbacks our app will use to respond to database events. -fn register_callbacks() { - // When we receive our `Credentials`, save them to a file. - once_on_connect(on_connected); - +fn register_callbacks(conn: &DbConnection) { // When a new user joins, print a notification. - User::on_insert(on_user_inserted); + conn.db.user().on_insert(on_user_inserted); // When a user's status changes, print a notification. - User::on_update(on_user_updated); + conn.db.user().on_update(on_user_updated); // When a new message is received, print it. - Message::on_insert(on_message_inserted); + conn.db.message().on_insert(on_message_inserted); // When we receive the message backlog, print it in timestamp order. - on_subscription_applied(on_sub_applied); + conn.subscription_builder().on_applied(on_sub_applied); // When we fail to set our name, print a warning. - on_set_name(on_name_set); + conn.reducers.on_set_name(on_name_set); // When we fail to send a message, print a warning. - on_send_message(on_message_sent); - - // When our connection closes, inform the user and exit. - on_disconnect(on_disconnected); + conn.reducers.on_send_message(on_message_sent); } ``` -### Save credentials +## Save credentials Each user has a `Credentials`, which consists of two parts: - An `Identity`, a unique public identifier. We're using these to identify `User` rows. - A `Token`, a private key which SpacetimeDB uses to authenticate the client. -`Credentials` are generated by SpacetimeDB each time a new client connects, and sent to the client so they can be saved, in order to re-connect with the same identity. The Rust SDK provides a pair of functions, `save_credentials` and `load_credentials`, for storing these credentials in a file. We'll save our credentials into a file in the directory `~/.spacetime_chat`, which should be unintrusive. If saving our credentials fails, we'll print a message to standard error, but otherwise continue normally; even though the user won't be able to reconnect with the same identity, they can still chat normally. - -Each client also has an `Address`, which modules can use to distinguish multiple concurrent connections by the same `Identity`. We don't need to know our `Address`, so we'll ignore that argument. +`Credentials` are generated by SpacetimeDB each time a new client connects, and sent to the client so they can be saved, in order to re-connect with the same identity. The Rust SDK provides a pair of functions in `File`, `save` and `load`, for saving and storing these credentials in a file. By default the `save` and `load` will look for credentials in the `$HOME/.spacetimedb_client_credentials/` directory, which should be unintrusive. If saving our credentials fails, we'll print a message to standard error, but otherwise continue normally; even though the user won't be able to reconnect with the same identity, they can still chat normally. To `client/src/main.rs`, add: ```rust /// Our `on_connect` callback: save our credentials to a file. -fn on_connected(creds: &Credentials, _client_address: Address) { - if let Err(e) = save_credentials(CREDS_DIR, creds) { +fn on_connected(conn: &DbConnection, ident: Identity, token: &str) { + let file = File::new(CREDS_NAME); + if let Err(e) = file.save(ident, token) { eprintln!("Failed to save credentials: {:?}", e); } + + println!("Connected to SpacetimeDB."); + println!("Use /name to set your username, otherwise enter your message!"); + + // Subscribe to the data we care about + subscribe_to_tables(&conn); + // Register callbacks for reducers + register_callbacks(&conn); +} +``` + +You can see here that when we connect we're going to register our callbacks, which we defined above. + +## Handle errors and disconnections + +We need to handle connection errors and disconnections by printing appropriate messages and exiting the program. + +To `client/src/main.rs`, add: + +```rust +/// Our `on_connect_error` callback: print the error, then exit the process. +fn on_connect_error(err: &anyhow::Error) { + eprintln!("Connection error: {:?}", err); } -const CREDS_DIR: &str = ".spacetime_chat"; +/// Our `on_disconnect` callback: print a note, then exit the process. +fn on_disconnected(_conn: &DbConnection, _err: Option<&anyhow::Error>) { + eprintln!("Disconnected!"); + std::process::exit(0) +} ``` -### Notify about new users +## Notify about new users -For each table, we can register on-insert and on-delete callbacks to be run whenever a subscribed row is inserted or deleted. We register these callbacks using the `on_insert` and `on_delete` methods of the trait `TableType`, which is automatically implemented for each table by `spacetime generate`. +For each table, we can register on-insert and on-delete callbacks to be run whenever a subscribed row is inserted or deleted. We register these callbacks using the `on_insert` and `on_delete`, which is automatically implemented for each table by `spacetime generate`. These callbacks can fire in two contexts: - After a reducer runs, when the client's cache is updated about changes to subscribed rows. - After calling `subscribe`, when the client's cache is initialized with all existing matching rows. -This second case means that, even though the module only ever inserts online users, the client's `User::on_insert` callbacks may be invoked with users who are offline. We'll only notify about online users. +This second case means that, even though the module only ever inserts online users, the client's `conn.db.user().on_insert(..)` callbacks may be invoked with users who are offline. We'll only notify about online users. -`on_insert` and `on_delete` callbacks take two arguments: the altered row, and an `Option<&ReducerEvent>`. This will be `Some` for rows altered by a reducer run, and `None` 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. +`on_insert` and `on_delete` callbacks take two arguments: `&EventContext` and the row data (in the case of insert it's a new row and in the case of delete it's the row that was deleted). You can determine whether the insert/delete operation was caused by a reducer or subscription update by checking the type of `ctx.event`. If `ctx.event` is a `Event::Reducer` then the row was changed by a reducer call, otherwise it was modified by a subscription update. `Reducer` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument. 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 functions `user_name_or_identity` and `identity_leading_hex` to handle this. @@ -203,7 +215,7 @@ To `client/src/main.rs`, add: ```rust /// Our `User::on_insert` callback: /// if the user is online, print a notification. -fn on_user_inserted(user: &User, _: Option<&ReducerEvent>) { +fn on_user_inserted(_ctx: &EventContext, user: &User) { if user.online { println!("User {} connected.", user_name_or_identity(user)); } @@ -212,17 +224,13 @@ fn on_user_inserted(user: &User, _: Option<&ReducerEvent>) { fn user_name_or_identity(user: &User) -> String { user.name .clone() - .unwrap_or_else(|| identity_leading_hex(&user.identity)) -} - -fn identity_leading_hex(id: &Identity) -> String { - hex::encode(&id.bytes()[0..8]) + .unwrap_or_else(|| user.identity.to_hex().to_string()) } ``` ### Notify about updated users -Because we declared a `#[primarykey]` 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 `on_update` method of the trait `TableWithPrimaryKey`, which is automatically implemented by `spacetime generate` for any table with a `#[primarykey]` 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 `ctx.db.user().identity().update(..) calls. We register these callbacks using the `on_update` method of the trait `TableWithPrimaryKey`, which is automatically implemented by `spacetime generate` for any table with a `#[primary_key]` column. `on_update` callbacks take three arguments: the old row, the new row, and an `Option<&ReducerEvent>`. @@ -256,36 +264,38 @@ fn on_user_updated(old: &User, new: &User, _: Option<&ReducerEvent>) { } ``` -### Print messages +## Print messages -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 `send_message` 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 `print_new_message` callback will check if its `reducer_event` argument is `Some`, and only print in that case. +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 `send_message` 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 `on_message_inserted` callback will check if the ctx.event type is an `Event::Reducer`, and only print in that case. -To find the `User` based on the message's `sender` identity, we'll use `User::find_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `find_by_identity` accepts an owned `Identity`, rather than a reference. We can `clone` the identity held in `message.sender`. +To find the `User` based on the message's `sender` identity, we'll use `ctx.db.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`. +We'll handle message-related events, such as receiving new messages or loading past messages. + To `client/src/main.rs`, add: ```rust /// Our `Message::on_insert` callback: print new messages. -fn on_message_inserted(message: &Message, reducer_event: Option<&ReducerEvent>) { - if reducer_event.is_some() { - print_message(message); +fn on_message_inserted(ctx: &EventContext, message: &Message) { + if let Event::Reducer(_) = ctx.event { + print_message(ctx, message) } } -fn print_message(message: &Message) { - let sender = User::find_by_identity(message.sender.clone()) +fn print_message(ctx: &EventContext, message: &Message) { + let sender = ctx.db.user().identity().find(&message.sender.clone()) .map(|u| user_name_or_identity(&u)) .unwrap_or_else(|| "unknown".to_string()); println!("{}: {}", sender, message.text); } -``` ### Print past messages in order Messages we receive live will come in order, but when we connect, we'll receive all the past messages at once. We can't just print these in the order we receive them; the logs would be all shuffled around, and would make no sense. Instead, when we receive the log of past messages, we'll sort them by their sent timestamps and print them in order. + We'll handle this in our function `print_messages_in_order`, which we registered as an `on_subscription_applied` callback. `print_messages_in_order` iterates over all the `Message`s we've received, sorts them, and then prints them. `Message::iter()` is defined on the trait `TableType`, and returns an iterator over all the messages in the client's cache. Rust iterators can't be sorted in-place, so we'll collect it to a `Vec`, then use the `sort_by_key` method to sort by timestamp. To `client/src/main.rs`, add: @@ -293,26 +303,20 @@ To `client/src/main.rs`, add: ```rust /// Our `on_subscription_applied` callback: /// sort all past messages and print them in timestamp order. -fn on_sub_applied() { - let mut messages = Message::iter().collect::>(); +fn on_sub_applied(ctx: &EventContext) { + let mut messages = ctx.db.message().iter().collect::>(); messages.sort_by_key(|m| m.sent); for message in messages { - print_message(&message); + print_message(ctx, &message); } } ``` -### Warn if our name was rejected +## Handle reducer failures We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `on_reducer` method of the `Reducer` trait, which is automatically implemented for each reducer by `spacetime generate`. -Each reducer callback takes at least three arguments: - -1. The `Identity` of the client who requested the reducer invocation. -2. The `Address` of the client who requested the reducer invocation, which may be `None` for scheduled reducers. -3. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`. `Status::Failed` holds the error which caused the reducer to fail, as a `String`. - -In addition, it takes a reference to each of the arguments passed to the reducer itself. +Each reducer callback first takes an `&EventContext` which contains all of the information from the reducer call including the reducer arguments, the identity of the caller, and whether or not the reducer call suceeded. These callbacks will be invoked in one of two cases: @@ -321,54 +325,35 @@ These callbacks will be invoked in one of two cases: Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. -We already handle successful `set_name` invocations using our `User::on_update` callback, but if the module rejects a user's chosen name, we'd like that user's client to let them know. We define a function `warn_if_name_rejected` as a `SetNameArgs::on_reducer` callback which checks if the reducer failed, and if it did, prints a message including the rejected name and the error. +We already handle successful `set_name` invocations using our `ctx.db.user().on_update(..)` callback, but if the module rejects a user's chosen name, we'd like that user's client to let them know. We define a function `on_set_name` as a `conn.reducers.on_set_name(..)` callback which checks if the reducer failed, and if it did, prints a message including the rejected name and the error. + To `client/src/main.rs`, add: ```rust /// Our `on_set_name` callback: print a warning if the reducer failed. -fn on_name_set(_sender_id: &Identity, _sender_address: Option
, status: &Status, name: &String) { - if let Status::Failed(err) = status { - eprintln!("Failed to change name to {:?}: {}", name, err); +fn on_name_set(ctx: &EventContext, name: &String) { + if let Event::Reducer(reducer) = &ctx.event { + if let Status::Failed(err) = reducer.status.clone() { + eprintln!("Failed to change name to {:?}: {}", name, err); + } } } -``` - -### Warn if our message was rejected - -We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different. -To `client/src/main.rs`, add: - -```rust /// Our `on_send_message` callback: print a warning if the reducer failed. -fn on_message_sent(_sender_id: &Identity, _sender_address: Option
, status: &Status, text: &String) { - if let Status::Failed(err) = status { - eprintln!("Failed to send message {:?}: {}", text, err); +fn on_message_sent(ctx: &EventContext, text: &String) { + if let Event::Reducer(reducer) = &ctx.event { + if let Status::Failed(err) = reducer.status.clone() { + eprintln!("Failed to send message {:?}: {}", text, err); + } } } ``` -### Exit on disconnect - -We can register callbacks to run when our connection ends using `on_disconnect`. These callbacks will run either when the client disconnects by calling `disconnect`, or when the server closes our connection. More involved apps might attempt to reconnect in this case, or do some sort of client-side cleanup, but we'll just print a note to the user and then exit the process. - -To `client/src/main.rs`, add: - -```rust -/// Our `on_disconnect` callback: print a note, then exit the process. -fn on_disconnected() { - eprintln!("Disconnected!"); - std::process::exit(0) -} -``` - ## Connect to the database Now that our callbacks are all set up, we can connect to the database. We'll store the URI of the SpacetimeDB instance and our module name in constants `SPACETIMEDB_URI` and `DB_NAME`. Replace `` with the name you chose when publishing your module during the module quickstart. -`connect` takes an `Option`, which is `None` for a new connection, or `Some` for a returning user. The Rust SDK defines `load_credentials`, the counterpart to the `save_credentials` we used in our `save_credentials_or_log_error`, to load `Credentials` from a file. `load_credentials` returns `Result>`, with `Ok(None)` meaning the credentials haven't been saved yet, and an `Err` meaning reading from disk failed. We can `expect` to handle the `Result`, and pass the `Option` directly to `connect`. - To `client/src/main.rs`, add: ```rust @@ -378,14 +363,22 @@ const SPACETIMEDB_URI: &str = "http://localhost:3000"; /// The module name we chose when we published our module. const DB_NAME: &str = ""; +/// You should change this value to a unique name based on your application. +const CREDS_NAME: &str = "rust-sdk-quickstart"; + /// Load credentials from a file and connect to the database. -fn connect_to_db() { - connect( - SPACETIMEDB_URI, - DB_NAME, - load_credentials(CREDS_DIR).expect("Error reading stored credentials"), - ) - .expect("Failed to connect"); +fn connect_to_db() -> DbConnection { + let credentials = File::new(CREDS_NAME); + let conn = DbConnection::builder() + .on_connect(on_connected) + .on_connect_error(on_connect_error) + .on_disconnect(on_disconnected) + .with_uri(SPACETIMEDB_URI) + .with_module_name(DB_NAME) + .with_credentials(credentials.load().unwrap()) + .build().expect("Failed to connect"); + conn.run_threaded(); + conn } ``` @@ -397,30 +390,33 @@ To `client/src/main.rs`, add: ```rust /// Register subscriptions for all rows of both tables. -fn subscribe_to_tables() { - subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]).unwrap(); +fn subscribe_to_tables(conn: &DbConnection) { + conn.subscription_builder().subscribe([ + "SELECT * FROM user;", + "SELECT * FROM message;", + ]); } ``` ## Handle user input -A user should interact with our client by typing lines into their terminal. A line that starts with `/name ` will set the user's name to the rest of the line. Any other line will send a message. +Our app should allow the user to interact by typing lines into their terminal. If the line starts with `/name `, we'll change the user's name. Any other line will send a message. -`spacetime generate` defined two functions for us, `set_name` and `send_message`, which send a message to the database to invoke the corresponding reducer. The first argument, the `ReducerContext`, is supplied by the server, but we pass all other arguments ourselves. In our case, that means that both `set_name` and `send_message` take one argument, a `String`. +The functions `set_name` and `send_message` are generated from the server module via `spacetime generate`. We pass them a `String`, which gets sent to the server to execute the corresponding reducer. To `client/src/main.rs`, add: ```rust /// Read each line of standard input, and either set our name or send a message as appropriate. -fn user_input_loop() { +fn user_input_loop(conn: &DbConnection) { for line in std::io::stdin().lines() { let Ok(line) = line else { panic!("Failed to read from stdin."); }; if let Some(name) = line.strip_prefix("/name ") { - set_name(name.to_string()); + conn.reducers.set_name(name.to_string()).unwrap(); } else { - send_message(line); + conn.reducers.send_message(line).unwrap(); } } } @@ -428,7 +424,7 @@ fn user_input_loop() { ## Run it -Change your directory to the client app, then compile and run it. From the `quickstart-chat` directory, run: +After setting everything up, change your directory to the client app, then compile and run it. From the `quickstart-chat` directory, run: ```bash cd client @@ -441,25 +437,25 @@ You should see something like: User d9e25c51996dea2f connected. ``` -Now try sending a message. Type `Hello, world!` and press enter. You should see something like: +Now try sending a message by typing `Hello, world!` and pressing enter. You should see: ``` d9e25c51996dea2f: Hello, world! ``` -Next, set your name. Type `/name `, replacing `` with your name. You should see something like: +Next, set your name by typing `/name `, replacing `` with your desired username. You should see: ``` User d9e25c51996dea2f renamed to . ``` -Then send another message. Type `Hello after naming myself.` and press enter. You should see: +Then, send another message: ``` : Hello after naming myself. ``` -Now, close the app by hitting control-c, and start it again with `cargo run`. You should see yourself connecting, and your past messages in order: +Now, close the app by hitting `Ctrl+C`, and start it again with `cargo run`. You'll see yourself connecting, and your past messages will load in order: ``` User connected. @@ -473,15 +469,13 @@ You can find the full code for this client [in the Rust SDK's examples](https:// Check out the [Rust SDK Reference](/docs/sdks/rust) for a more comprehensive view of the SpacetimeDB Rust SDK. -Our bare-bones terminal interface has some quirks. Incoming messages can appear while the user is typing and be spliced into the middle of user input, which is less than ideal. Also, the user's input is interspersed with the program's output, so messages the user sends will seem to appear twice. Why not try building a better interface using [Rustyline](https://crates.io/crates/rustyline), [Cursive](https://crates.io/crates/cursive), or even a full-fledged GUI? We went for the Cursive route, and you can check out what we came up with [in the Rust SDK's examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/sdk/examples/cursive-chat). - -Once our chat server runs for a while, messages will accumulate, and it will get frustrating to see the entire backlog each time you connect. Instead, you could refine your `Message` subscription query, subscribing only to messages newer than, say, half an hour before the user connected. +Our basic terminal interface has some limitations. Incoming messages can appear while the user is typing, which is less than ideal. Additionally, the user's input gets mixed with the program's output, making messages the user sends appear twice. You might want to try improving the interface by using [Rustyline](https://crates.io/crates/rustyline), [Cursive](https://crates.io/crates/cursive), or even creating a full-fledged GUI. -You could also add support for styling messages, perhaps by interpreting HTML tags in the messages and printing appropriate [ANSI escapes](https://en.wikipedia.org/wiki/ANSI_escape_code). +Once your chat server runs for a while, you might want to limit the messages your client loads by refining your `Message` subscription query, only subscribing to messages sent within the last half-hour. -Or, you could extend the module and the client together, perhaps: +You could also add features like: -- Adding a `moderator: bool` flag to `User` and allowing moderators to time-out or ban naughty chatters. -- Adding a message of the day which gets shown to users whenever they connect, or some rules which get shown only to new users. -- Supporting separate rooms or channels which users can join or leave, and maybe even direct messages. -- Allowing users to set their status, which could be displayed alongside their username. +- Styling messages by interpreting HTML tags and printing appropriate [ANSI escapes](https://en.wikipedia.org/wiki/ANSI_escape_code). +- Adding a `moderator` flag to the `User` table, allowing moderators to manage users (e.g., time-out, ban). +- Adding rooms or channels that users can join or leave. +- Supporting direct messages or displaying user statuses next to their usernames. diff --git a/docs/sdks/typescript/index.md b/docs/sdks/typescript/index.md index 0091781..4f4e17d 100644 --- a/docs/sdks/typescript/index.md +++ b/docs/sdks/typescript/index.md @@ -10,11 +10,11 @@ First, create a new client project, and add the following to your `tsconfig.json ```json { - "compilerOptions": { - //You can use any target higher than this one - //https://www.typescriptlang.org/tsconfig#target - "target": "es2015" - } + "compilerOptions": { + //You can use any target higher than this one + //https://www.typescriptlang.org/tsconfig#target + "target": "es2015" + } } ``` @@ -147,7 +147,12 @@ const name_or_address = 'database_name'; const auth_token = undefined; const protocol = 'binary'; -var spacetimeDBClient = new SpacetimeDBClient(host, name_or_address, auth_token, protocol); +var spacetimeDBClient = new SpacetimeDBClient( + host, + name_or_address, + auth_token, + protocol +); ``` ## Class methods @@ -268,7 +273,11 @@ const host = 'ws://localhost:3000'; const name_or_address = 'database_name'; const auth_token = undefined; -var spacetimeDBClient = new SpacetimeDBClient(host, name_or_address, auth_token); +var spacetimeDBClient = new SpacetimeDBClient( + host, + name_or_address, + auth_token +); // Connect with the initial parameters spacetimeDBClient.connect(); //Set the `auth_token` @@ -288,7 +297,10 @@ disconnect(): void #### Example ```ts -var spacetimeDBClient = new SpacetimeDBClient('ws://localhost:3000', 'database_name'); +var spacetimeDBClient = new SpacetimeDBClient( + 'ws://localhost:3000', + 'database_name' +); spacetimeDBClient.disconnect(); ``` @@ -343,10 +355,10 @@ The credentials passed to the callback can be saved and used to authenticate the ```ts spacetimeDBClient.onConnect((token, identity, address) => { - console.log('Connected to SpacetimeDB'); - console.log('Token', token); - console.log('Identity', identity); - console.log('Address', address); + console.log('Connected to SpacetimeDB'); + console.log('Token', token); + console.log('Identity', identity); + console.log('Address', address); }); ``` @@ -370,7 +382,7 @@ onError(callback: (...args: any[]) => void): void ```ts spacetimeDBClient.onError((...args: any[]) => { - console.error('ERROR', args); + console.error('ERROR', args); }); ``` @@ -546,22 +558,22 @@ For each table defined by a module, `spacetime generate` generates a `class` in The generated class has a field for each of the table's columns, whose names are the column names converted to `snake_case`. -| Properties | Description | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| [`Table.name`](#table-name) | The name of the class. | -| [`Table.tableName`](#table-tableName) | The name of the table in the database. | -| Methods | | -| [`Table.isEqual`](#table-isequal) | Method to compare two identities. | -| [`Table.all`](#table-all) | Return all the subscribed rows in the table. | -| [`Table.filterBy{COLUMN}`](#table-filterbycolumn) | Autogenerated; return subscribed rows with a given value in a particular column. `{COLUMN}` is a placeholder for a column name. | -| [`Table.findBy{COLUMN}`](#table-findbycolumn) | Autogenerated; return a subscribed row with a given value in a particular unique column. `{COLUMN}` is a placeholder for a column name. | -| Events | | -| [`Table.onInsert`](#table-oninsert) | Register an `onInsert` callback for when a subscribed row is newly inserted into the database. | -| [`Table.removeOnInsert`](#table-removeoninsert) | Unregister a previously-registered [`onInsert`](#table-oninsert) callback. | -| [`Table.onUpdate`](#table-onupdate) | Register an `onUpdate` callback for when an existing row is modified. | -| [`Table.removeOnUpdate`](#table-removeonupdate) | Unregister a previously-registered [`onUpdate`](#table-onupdate) callback. | -| [`Table.onDelete`](#table-ondelete) | Register an `onDelete` callback for when a subscribed row is removed from the database. | -| [`Table.removeOnDelete`](#table-removeondelete) | Unregister a previously-registered [`onDelete`](#table-removeondelete) callback. | +| Properties | Description | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`Table.name`](#table-name) | The name of the class. | +| [`Table.tableName`](#table-tableName) | The name of the table in the database. | +| Methods | | +| [`Table.isEqual`](#table-isequal) | Method to compare two identities. | +| [`Table.all`](#table-all) | Return all the subscribed rows in the table. | +| [`Table.filterBy{COLUMN}`](#table-filterbycolumn) | Autogenerated; return subscribed rows with a given value in a particular column. `{COLUMN}` is a placeholder for a column name. | +| [`Table.findBy{COLUMN}`](#table-findbycolumn) | Autogenerated; return a subscribed row with a given value in a particular unique column. `{COLUMN}` is a placeholder for a column name. | +| Events | | +| [`Table.onInsert`](#table-oninsert) | Register an `onInsert` callback for when a subscribed row is newly inserted into the database. | +| [`Table.removeOnInsert`](#table-removeoninsert) | Unregister a previously-registered [`onInsert`](#table-oninsert) callback. | +| [`Table.onUpdate`](#table-onupdate) | Register an `onUpdate` callback for when an existing row is modified. | +| [`Table.removeOnUpdate`](#table-removeonupdate) | Unregister a previously-registered [`onUpdate`](#table-onupdate) callback. | +| [`Table.onDelete`](#table-ondelete) | Register an `onDelete` callback for when a subscribed row is removed from the database. | +| [`Table.removeOnDelete`](#table-removeondelete) | Unregister a previously-registered [`onDelete`](#table-removeondelete) callback. | ## Properties @@ -596,14 +608,17 @@ Return all the subscribed rows in the table. #### Example ```ts -var spacetimeDBClient = new SpacetimeDBClient('ws://localhost:3000', 'database_name'); +var spacetimeDBClient = new SpacetimeDBClient( + 'ws://localhost:3000', + 'database_name' +); spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(['SELECT * FROM Person']); + spacetimeDBClient.subscribe(['SELECT * FROM Person']); - setTimeout(() => { - console.log(Person.all()); // Prints all the `Person` rows in the database. - }, 5000); + setTimeout(() => { + console.log(Person.all()); // Prints all the `Person` rows in the database. + }, 5000); }); ``` @@ -624,14 +639,17 @@ Return the number of subscribed rows in the table, or 0 if there is no active co #### Example ```ts -var spacetimeDBClient = new SpacetimeDBClient('ws://localhost:3000', 'database_name'); +var spacetimeDBClient = new SpacetimeDBClient( + 'ws://localhost:3000', + 'database_name' +); spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(['SELECT * FROM Person']); + spacetimeDBClient.subscribe(['SELECT * FROM Person']); - setTimeout(() => { - console.log(Person.count()); - }, 5000); + setTimeout(() => { + console.log(Person.count()); + }, 5000); }); ``` @@ -660,14 +678,17 @@ These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name #### Example ```ts -var spacetimeDBClient = new SpacetimeDBClient('ws://localhost:3000', 'database_name'); +var spacetimeDBClient = new SpacetimeDBClient( + 'ws://localhost:3000', + 'database_name' +); spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(['SELECT * FROM Person']); + spacetimeDBClient.subscribe(['SELECT * FROM Person']); - setTimeout(() => { - console.log(...Person.filterByName('John')); // prints all the `Person` rows named John. - }, 5000); + setTimeout(() => { + console.log(...Person.filterByName('John')); // prints all the `Person` rows named John. + }, 5000); }); ``` @@ -696,14 +717,17 @@ These methods are named `findBy{COLUMN}`, where `{COLUMN}` is the column name co #### Example ```ts -var spacetimeDBClient = new SpacetimeDBClient('ws://localhost:3000', 'database_name'); +var spacetimeDBClient = new SpacetimeDBClient( + 'ws://localhost:3000', + 'database_name' +); spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(['SELECT * FROM Person']); + spacetimeDBClient.subscribe(['SELECT * FROM Person']); - setTimeout(() => { - console.log(Person.findById(0)); // prints a `Person` row with id 0. - }, 5000); + setTimeout(() => { + console.log(Person.findById(0)); // prints a `Person` row with id 0. + }, 5000); }); ``` @@ -762,17 +786,20 @@ Register an `onInsert` callback for when a subscribed row is newly inserted into #### Example ```ts -var spacetimeDBClient = new SpacetimeDBClient('ws://localhost:3000', 'database_name'); +var spacetimeDBClient = new SpacetimeDBClient( + 'ws://localhost:3000', + 'database_name' +); spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(['SELECT * FROM Person']); + spacetimeDBClient.subscribe(['SELECT * FROM Person']); }); Person.onInsert((person, reducerEvent) => { - if (reducerEvent) { - console.log('New person inserted by reducer', reducerEvent, person); - } else { - console.log('New person received during subscription update', person); - } + if (reducerEvent) { + console.log('New person inserted by reducer', reducerEvent, person); + } else { + console.log('New person received during subscription update', person); + } }); ``` @@ -813,13 +840,16 @@ Register an `onUpdate` callback to run when an existing row is modified by prima #### Example ```ts -var spacetimeDBClient = new SpacetimeDBClient('ws://localhost:3000', 'database_name'); +var spacetimeDBClient = new SpacetimeDBClient( + 'ws://localhost:3000', + 'database_name' +); spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(['SELECT * FROM Person']); + spacetimeDBClient.subscribe(['SELECT * FROM Person']); }); Person.onUpdate((oldPerson, newPerson, reducerEvent) => { - console.log('Person updated by reducer', reducerEvent, oldPerson, newPerson); + console.log('Person updated by reducer', reducerEvent, oldPerson, newPerson); }); ``` @@ -858,17 +888,23 @@ Register an `onDelete` callback for when a subscribed row is removed from the da #### Example ```ts -var spacetimeDBClient = new SpacetimeDBClient('ws://localhost:3000', 'database_name'); +var spacetimeDBClient = new SpacetimeDBClient( + 'ws://localhost:3000', + 'database_name' +); spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(['SELECT * FROM Person']); + spacetimeDBClient.subscribe(['SELECT * FROM Person']); }); Person.onDelete((person, reducerEvent) => { - if (reducerEvent) { - console.log('Person deleted by reducer', reducerEvent, person); - } else { - console.log('Person no longer subscribed during subscription update', person); - } + if (reducerEvent) { + console.log('Person deleted by reducer', reducerEvent, person); + } else { + console.log( + 'Person no longer subscribed during subscription update', + person + ); + } }); ``` @@ -941,6 +977,6 @@ Clients will only be notified of reducer runs if either of two criteria is met: ```ts SayHelloReducer.on((reducerEvent, ...reducerArgs) => { - console.log('SayHelloReducer called', reducerEvent, reducerArgs); + console.log('SayHelloReducer called', reducerEvent, reducerArgs); }); ``` diff --git a/docs/sdks/typescript/quickstart.md b/docs/sdks/typescript/quickstart.md index 46b758e..96725cb 100644 --- a/docs/sdks/typescript/quickstart.md +++ b/docs/sdks/typescript/quickstart.md @@ -168,12 +168,16 @@ module_bindings We need to import these types into our `client/src/App.tsx`. While we are at it, we will also import the SpacetimeDBClient class from our SDK. In order to let the SDK know what tables and reducers we will be using we need to also register them. ```typescript -import { SpacetimeDBClient, Identity, Address } from "@clockworklabs/spacetimedb-sdk"; +import { + SpacetimeDBClient, + Identity, + Address, +} from '@clockworklabs/spacetimedb-sdk'; -import Message from "./module_bindings/message"; -import User from "./module_bindings/user"; -import SendMessageReducer from "./module_bindings/send_message_reducer"; -import SetNameReducer from "./module_bindings/set_name_reducer"; +import Message from './module_bindings/message'; +import User from './module_bindings/user'; +import SendMessageReducer from './module_bindings/send_message_reducer'; +import SetNameReducer from './module_bindings/set_name_reducer'; SpacetimeDBClient.registerReducers(SendMessageReducer, SetNameReducer); SpacetimeDBClient.registerTables(Message, User); @@ -190,10 +194,10 @@ Replace `` with the name you chose when publishing your module duri Add this before the `App` function declaration: ```typescript -let token = localStorage.getItem("auth_token") || undefined; +let token = localStorage.getItem('auth_token') || undefined; var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "chat", + 'ws://localhost:3000', + 'chat', token ); ``` @@ -241,13 +245,13 @@ To the body of `App`, add: ```typescript client.current.onConnect((token, identity, address) => { - console.log("Connected to SpacetimeDB"); + console.log('Connected to SpacetimeDB'); local_identity.current = identity; - localStorage.setItem("auth_token", token); + localStorage.setItem('auth_token', token); - client.current.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); + client.current.subscribe(['SELECT * FROM User', 'SELECT * FROM Message']); }); ``` @@ -269,7 +273,7 @@ To the body of `App`, add: function userNameOrIdentity(user: User): string { console.log(`Name: ${user.name} `); if (user.name !== null) { - return user.name || ""; + return user.name || ''; } else { var identityStr = new Identity(user.identity).toHexString(); console.log(`Name: ${identityStr} `); @@ -281,11 +285,11 @@ function setAllMessagesInOrder() { let messages = Array.from(Message.all()); messages.sort((a, b) => (a.sent > b.sent ? 1 : a.sent < b.sent ? -1 : 0)); - let messagesType: MessageType[] = messages.map((message) => { + let messagesType: MessageType[] = messages.map(message => { let sender_identity = User.findByIdentity(message.sender); let display_name = sender_identity ? userNameOrIdentity(sender_identity) - : "unknown"; + : 'unknown'; return { name: display_name, @@ -296,7 +300,7 @@ function setAllMessagesInOrder() { setMessages(messagesType); } -client.current.on("initialStateSync", () => { +client.current.on('initialStateSync', () => { setAllMessagesInOrder(); var user = User.findByIdentity(local_identity?.current?.toUint8Array()!); setName(userNameOrIdentity(user!)); @@ -337,7 +341,7 @@ To the body of `App`, add: ```typescript // Helper function to append a line to the systemMessage state function appendToSystemMessage(line: String) { - setSystemMessage((prevMessage) => prevMessage + "\n" + line); + setSystemMessage(prevMessage => prevMessage + '\n' + line); } User.onInsert((user, reducerEvent) => { @@ -416,9 +420,9 @@ SetNameReducer.on((reducerEvent, newName) => { local_identity.current && reducerEvent.callerIdentity.isEqual(local_identity.current) ) { - if (reducerEvent.status === "failed") { + if (reducerEvent.status === 'failed') { appendToSystemMessage(`Error setting name: ${reducerEvent.message} `); - } else if (reducerEvent.status === "committed") { + } else if (reducerEvent.status === 'committed') { setName(newName); } } @@ -437,7 +441,7 @@ SendMessageReducer.on((reducerEvent, newMessage) => { local_identity.current && reducerEvent.callerIdentity.isEqual(local_identity.current) ) { - if (reducerEvent.status === "failed") { + if (reducerEvent.status === 'failed') { appendToSystemMessage(`Error sending message: ${reducerEvent.message} `); } } diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 14eb240..8e0a49e 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -119,5 +119,5 @@ We chose ECS for this example project because it promotes scalability, modularit From here, the tutorial continues with your favorite server module language of choice: - - [Rust](part-2a-rust.md) - - [C#](part-2b-csharp.md) +- [Rust](part-2a-rust.md) +- [C#](part-2b-csharp.md) diff --git a/docs/unity/part-2b-c-sharp.md b/docs/unity/part-2b-c-sharp.md index 5be1c7c..b1d50e8 100644 --- a/docs/unity/part-2b-c-sharp.md +++ b/docs/unity/part-2b-c-sharp.md @@ -113,12 +113,11 @@ public static void CreatePlayer(ReducerContext ctx, string username) // Get the Identity of the client who called this reducer Identity sender = ctx.Sender; - // Make sure we don't already have a player with this identity - PlayerComponent? user = PlayerComponent.FindByIdentity(sender); - if (user is null) - { - throw new ArgumentException("Player already exists"); - } + PlayerComponent? existingPlayer = PlayerComponent.FindByIdentity(sender); + if (existingPlayer != null) + { + throw new InvalidOperationException($"Player already exists for identity: {sender}"); + } // Create a new entity for this player try @@ -327,6 +326,7 @@ public static void SendChatMessage(ReducerContext ctx, string text) ## Wrapping Up ### Publishing a Module to SpacetimeDB + 💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) Now that we've written the code for our server module and reached a clean checkpoint, we need to publish it to SpacetimeDB. This will create the database and call the init reducer. In your terminal or command window, run the following commands. diff --git a/docs/unity/part-4.md b/docs/unity/part-4.md index d7c2228..029fbe1 100644 --- a/docs/unity/part-4.md +++ b/docs/unity/part-4.md @@ -162,7 +162,6 @@ pub fn resource_spawner_agent(_ctx: ReducerContext, _arg: ResourceSpawnAgentSche } ``` - 2. Since this reducer uses `rand::Rng` we need add include it. Add this `use` statement to the top of lib.rs. ```rust @@ -179,6 +178,7 @@ use rand::Rng; scheduled_at: duration!(1000ms).into() }).expect(); ``` + struct ResouceSpawnAgentSchedueler { 4. Next we need to generate our client code and publish the module. Since we changed the schema we need to make sure we include the `--clear-database` flag. Run the following commands from your Server directory: diff --git a/docs/ws/index.md b/docs/ws/index.md index b00bfa5..587fbad 100644 --- a/docs/ws/index.md +++ b/docs/ws/index.md @@ -188,7 +188,7 @@ Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribe | `tableRowOperations` | A `TableRowOperation` for each inserted or deleted row. | | `TableRowOperation` field | Value | -|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `op` | `INSERT` for inserted rows during a [`TransactionUpdate`](#transactionupdate) or rows resident upon applying a subscription; `DELETE` for deleted rows during a [`TransactionUpdate`](#transactionupdate). | | `row` | The altered row, encoded as a BSATN `ProductValue`. | @@ -225,7 +225,7 @@ Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribe | `table_row_operations` | A `TableRowOperation` for each inserted or deleted row. | | `TableRowOperation` field | Value | -|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `op` | `"insert"` for inserted rows during a [`TransactionUpdate`](#transactionupdate) or rows resident upon applying a subscription; `"delete"` for deleted rows during a [`TransactionUpdate`](#transactionupdate). | | `row` | The altered row, encoded as a JSON array. | diff --git a/nav.ts b/nav.ts index 8b21cc9..19e69c7 100644 --- a/nav.ts +++ b/nav.ts @@ -3,7 +3,7 @@ type Nav = { }; type NavItem = NavPage | NavSection; type NavPage = { - type: "page"; + type: 'page'; path: string; slug: string; title: string; @@ -11,71 +11,96 @@ type NavPage = { href?: string; }; type NavSection = { - type: "section"; + type: 'section'; title: string; }; -function page(title: string, slug: string, path: string, props?: { disabled?: boolean; href?: string; description?: string }): NavPage { - return { type: "page", path, slug, title, ...props }; +function page( + title: string, + slug: string, + path: string, + props?: { disabled?: boolean; href?: string; description?: string } +): NavPage { + return { type: 'page', path, slug, title, ...props }; } function section(title: string): NavSection { - return { type: "section", title }; + return { type: 'section', title }; } const nav: Nav = { items: [ - section("Intro"), - 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('Intro'), + 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('Deploying'), + page('Testnet', 'deploying/testnet', 'deploying/testnet.md'), - section("Unity Tutorial - Basic Multiplayer"), - page("Overview", "unity-tutorial", "unity/index.md"), - page("1 - Setup", "unity/part-1", "unity/part-1.md"), - page("2a - Server (Rust)", "unity/part-2a-rust", "unity/part-2a-rust.md"), - page("2b - Server (C#)", "unity/part-2b-c-sharp", "unity/part-2b-c-sharp.md"), - page("3 - Client", "unity/part-3", "unity/part-3.md"), + section('Unity Tutorial - Basic Multiplayer'), + page('Overview', 'unity-tutorial', 'unity/index.md'), + page('1 - Setup', 'unity/part-1', 'unity/part-1.md'), + page('2a - Server (Rust)', 'unity/part-2a-rust', 'unity/part-2a-rust.md'), + page( + '2b - Server (C#)', + 'unity/part-2b-c-sharp', + 'unity/part-2b-c-sharp.md' + ), + page('3 - Client', 'unity/part-3', 'unity/part-3.md'), - section("Unity Tutorial - Advanced"), - page("4 - Resources And Scheduling", "unity/part-4", "unity/part-4.md"), - page("5 - BitCraft Mini", "unity/part-5", "unity/part-5.md"), + section('Unity Tutorial - Advanced'), + page('4 - Resources And Scheduling', 'unity/part-4', 'unity/part-4.md'), + page('5 - BitCraft Mini', 'unity/part-5', 'unity/part-5.md'), - section("Server Module Languages"), - page("Overview", "modules", "modules/index.md"), - page("Rust Quickstart", "modules/rust/quickstart", "modules/rust/quickstart.md"), - page("Rust Reference", "modules/rust", "modules/rust/index.md"), - page("C# Quickstart", "modules/c-sharp/quickstart", "modules/c-sharp/quickstart.md"), - page("C# Reference", "modules/c-sharp", "modules/c-sharp/index.md"), + section('Server Module Languages'), + page('Overview', 'modules', 'modules/index.md'), + page( + 'Rust Quickstart', + 'modules/rust/quickstart', + 'modules/rust/quickstart.md' + ), + page('Rust Reference', 'modules/rust', 'modules/rust/index.md'), + page( + 'C# Quickstart', + 'modules/c-sharp/quickstart', + 'modules/c-sharp/quickstart.md' + ), + 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('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"), + section('WebAssembly ABI'), + page('Module ABI Reference', 'webassembly-abi', 'webassembly-abi/index.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('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('WebSocket API Reference'), + page('WebSocket', 'ws', 'ws/index.md'), - section("Data Format"), - page("SATN", "satn", "satn.md"), - page("BSATN", "bsatn", "bsatn.md"), + section('Data Format'), + page('SATN', 'satn', 'satn.md'), + page('BSATN', 'bsatn', 'bsatn.md'), - section("SQL"), - page("SQL Reference", "sql", "sql/index.md"), + section('SQL'), + page('SQL Reference', 'sql', 'sql/index.md'), ], }; diff --git a/package.json b/package.json index a56ea4e..2c2b944 100644 --- a/package.json +++ b/package.json @@ -12,4 +12,4 @@ }, "author": "Clockwork Labs", "license": "ISC" -} \ No newline at end of file +}