From 60472af604f5eb7d5b5e482005c93c81010a8c45 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Wed, 7 Feb 2024 11:06:09 +0800 Subject: [PATCH 01/24] doc: Onboarding impr, fixes, consistency, cleanup refactor: Whats next cleanup, +unity, -bloat Removed redundant text while there refactor: Unity quickstart fixes, impr, prettify refactor: Unity pt1 fixes, impr, prettify fix(README): Rm "see test edits below" ref * !exists refactor(minor): General onboarding cleanup * Shorter, prettier, consistent fix(sdks/c#): Broken unitypackage url feat(sdks/c#): Add OneTimeQuery api ref --- README.md | 2 +- docs/getting-started.md | 21 +++++---- docs/modules/c-sharp/index.md | 3 ++ docs/modules/c-sharp/quickstart.md | 29 ++++++++++--- docs/modules/rust/quickstart.md | 2 +- docs/sdks/c-sharp/index.md | 20 ++++++--- docs/sdks/c-sharp/quickstart.md | 68 ++++++++++++++++++------------ docs/unity/part-1.md | 55 +++++++++++++++--------- 8 files changed, 131 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index af34b88a..0f9998b0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ To make changes to our docs, you can open a pull request in this repository. You git clone ssh://git@github.com//spacetime-docs ``` -3. Make your edits to the docs that you want to make + test them locally (see Testing Your Edits below) +3. Make your edits to the docs that you want to make + test them locally 4. Commit your changes: ```bash diff --git a/docs/getting-started.md b/docs/getting-started.md index 5a0c6041..372255be 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -2,29 +2,32 @@ To develop SpacetimeDB applications locally, you will need to run the Standalone version of the server. -1. [Install](/install) the SpacetimeDB CLI (Command Line Interface). +1. [Install](/install) the SpacetimeDB CLI (Command Line Interface) 2. Run the start command ```bash spacetime start ``` -The server listens on port `3000` by default. You can change this by using the `--listen-addr` option described below. +The server listens on port `3000` by default, customized via `--listen-addr`. -SSL is not supported in standalone mode. +⚠️ SSL is not supported in standalone mode. ## What's Next? -You are ready to start developing SpacetimeDB modules. We have a quickstart guide for each supported server-side language: +You are ready to start developing SpacetimeDB modules. See below for a quickstart guide for both client and server (module) languages/frameworks. + +### Server (Module) - [Rust](/docs/modules/rust/quickstart) - [C#](/docs/modules/c-sharp/quickstart) -Then you can write your client application. We have a quickstart guide for each supported client-side language: +⚡**Note:** Rust is [roughly 2x faster](https://faun.dev/c/links/faun/c-vs-rust-vs-go-a-performance-benchmarking-in-kubernetes/) than C# + +### Client - [Rust](/docs/sdks/rust/quickstart) -- [C#](/docs/sdks/c-sharp/quickstart) +- [C# (Standalone)](/docs/sdks/c-sharp/quickstart) +- [C# (Unity)](/docs/unity/part-1) - [Typescript](/docs/sdks/typescript/quickstart) -- [Python](/docs/sdks/python/quickstart) - -We also have a [step-by-step tutorial](/docs/unity/part-1) for building a multiplayer game in Unity3d. +- [Python](/docs/sdks/python/quickstart) \ No newline at end of file diff --git a/docs/modules/c-sharp/index.md b/docs/modules/c-sharp/index.md index 36a9618a..31ebd1d4 100644 --- a/docs/modules/c-sharp/index.md +++ b/docs/modules/c-sharp/index.md @@ -42,6 +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. @@ -211,8 +212,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); } diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index fb97c316..c1cfb34d 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -16,7 +16,18 @@ If you haven't already, start by [installing SpacetimeDB](/install). This will i ## Install .NET 8 -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. .NET 8.0 is the earliest to have the `wasi-experimental` workload that we rely on. +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 +``` + +.NET 8.0 is the earliest to have the `wasi-experimental` workload that we rely on, but requires manual activation: + +```bash +dotnet workload install wasi-experimental +``` ## Project structure @@ -35,7 +46,11 @@ spacetime init --lang csharp server ## Declare imports -`spacetime init` should have pre-populated `server/Lib.cs` 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` 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. +3. Clear it out, so we can write a new module that's still pretty simple: a bare-bones chat server. To the top of `server/Lib.cs`, add some imports we'll be using: @@ -235,7 +250,7 @@ public static void OnDisconnect(DbEventArgs dbEventArgs) else { // User does not exist, log warning - Log($"Warning: No user found for disconnected client."); + Log("Warning: No user found for disconnected client."); } } ``` @@ -250,12 +265,16 @@ From the `quickstart-chat` directory, run: spacetime publish --project-path server ``` +```bash +npm i wasm-opt -g +``` + ## Call Reducers You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. ```bash -spacetime call send_message '["Hello, World!"]' +spacetime call send_message "Hello, World!" ``` Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. @@ -288,4 +307,4 @@ spacetime sql "SELECT * FROM Message" You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](/docs/languages/rust/rust-sdk-quickstart-guide), [C#](/docs/languages/csharp/csharp-sdk-quickstart-guide), [TypeScript](/docs/languages/typescript/typescript-sdk-quickstart-guide) or [Python](/docs/languages/python/python-sdk-quickstart-guide). -If you are planning to use SpacetimeDB with the Unity3d game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1) or check out our example game, [BitcraftMini](/docs/unity/part-3). +If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1) or check out our example game, [BitcraftMini](/docs/unity/part-3). diff --git a/docs/modules/rust/quickstart.md b/docs/modules/rust/quickstart.md index e0ff0f5f..e015b881 100644 --- a/docs/modules/rust/quickstart.md +++ b/docs/modules/rust/quickstart.md @@ -269,4 +269,4 @@ You can find the full code for this module [in the SpacetimeDB module examples]( You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quickstart guide for your client language of choice: [Rust](/docs/sdks/rust/quickstart), [C#](/docs/sdks/c-sharp/quickstart), [TypeScript](/docs/sdks/typescript/quickstart) or [Python](/docs/sdks/python/quickstart). -If you are planning to use SpacetimeDB with the Unity3d game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1) or check out our example game, [BitcraftMini](/docs/unity/part-3). +If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1) or check out our example game, [BitcraftMini](/docs/unity/part-3). diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md index 473ca1ba..7c920cf5 100644 --- a/docs/sdks/c-sharp/index.md +++ b/docs/sdks/c-sharp/index.md @@ -17,9 +17,10 @@ The SpacetimeDB client C# for Rust contains all the tools you need to build nati - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect) - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived) - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect) - - [Subscribe to queries](#subscribe-to-queries) + - [Query subscriptions & one-time actions](#subscribe-to-queries) - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe) - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied) + - [Method `SpacetimeDBClient.OneOffQuery`](#event-spacetimedbclientoneoffquery) - [View rows of subscribed tables](#view-rows-of-subscribed-tables) - [Class `{TABLE}`](#class-table) - [Static Method `{TABLE}.Iter`](#static-method-tableiter) @@ -64,13 +65,11 @@ dotnet add package spacetimedbsdk ### Using Unity -To install the SpacetimeDB SDK into a Unity project, download the SpacetimeDB SDK from the following link. +To install the SpacetimeDB SDK into a Unity project, [download the SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest), packaged as a `.unitypackage`. -https://sdk.spacetimedb.com/SpacetimeDBUnitySDK.unitypackage +In Unity navigate to the `Assets > Import Package > Custom Package` menu in the menu bar. Select your `SpacetimeDB.Unity.Comprehensive.Tutorial.unitypackage` file and leave all folders checked. -In Unity navigate to the `Assets > Import Package > Custom Package...` menu in the menu bar. Select your `SpacetimeDBUnitySDK.unitypackage` file and leave all folders checked. - -(See also the [Unity Tutorial](/docs/unity/part-1).) +(See also the [Unity Tutorial](/docs/unity/part-1)) ## Generate module bindings @@ -319,6 +318,15 @@ void Main() } ``` +### Method [`OneTimeQuery`](#method-spacetimedbclientsubscribe) + +You may not want to subscribe to a query, but instead want to run a query once and receive the results immediately via a `Task` result: + +```csharp +// Query all Messages from the sender "bob" +SpacetimeDBClient.instance.OneOffQuery("WHERE sender = \"bob\""); +``` + ## View rows of subscribed tables The SDK maintains a local view of the database called the "client cache". This cache contains whatever rows are selected via a call to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe). These rows are represented in the SpacetimeDB .Net SDK as instances of [`SpacetimeDB.Types.{TABLE}`](#class-table). diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md index f7565019..c143716d 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -1,8 +1,8 @@ # C# Client SDK Quick Start -In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in C#. +In this guide we'll show you how to get up and running with a simple SpacetimeDB app with a client written in C#. -We'll implement a command-line client for the module created in our Rust or C# Module Quickstart guides. Make sure you follow one of these guides before you start on this one. +We'll implement a command-line client for the module created in our [Rust](../../modules/rust/quickstart.md) or [C# Module](../../modules/c-sharp/quickstart.md) Quickstart guides. Ensure you followed one of these guides before continuing. ## Project structure @@ -12,7 +12,7 @@ Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart cd quickstart-chat ``` -Within it, create a new C# console application project called `client` using either Visual Studio or the .NET CLI: +Within it, create a new C# console application project called `client` using either Visual Studio, Rider or the .NET CLI: ```bash dotnet new console -o client @@ -22,7 +22,7 @@ Open the project in your IDE of choice. ## Add the NuGet package for the C# SpacetimeDB SDK -Add the `SpacetimeDB.ClientSDK` [NuGet package](https://www.nuget.org/packages/spacetimedbsdk) using Visual Studio NuGet package manager or via the .NET CLI +Add the `SpacetimeDB.ClientSDK` [NuGet package](https://www.nuget.org/packages/spacetimedbsdk) using Visual Studio or Rider _NuGet Package Manager_ or via the .NET CLI: ```bash dotnet add package SpacetimeDB.ClientSDK @@ -39,7 +39,7 @@ mkdir -p client/module_bindings spacetime generate --lang csharp --out-dir client/module_bindings --project-path server ``` -Take a look inside `client/module_bindings`. The CLI should have generated five files: +Take a look inside `client/module_bindings`. The CLI should have generated five files under `module_bindings`: ``` module_bindings @@ -65,8 +65,10 @@ We will also need to create some global variables that will be explained when we ```csharp // our local client SpacetimeDB identity Identity? local_identity = null; + // declare a thread safe queue to store commands in format (command, args) ConcurrentQueue<(string,string)> input_queue = new ConcurrentQueue<(string, string)>(); + // declare a threadsafe cancel token to cancel the process loop CancellationTokenSource cancel_token = new CancellationTokenSource(); ``` @@ -75,10 +77,10 @@ CancellationTokenSource cancel_token = new CancellationTokenSource(); We'll work outside-in, first defining our `Main` function at a high level, then implementing each behavior it needs. We need `Main` to do several things: -1. Initialize the AuthToken module, which loads and stores our authentication token to/from local storage. -2. Create the SpacetimeDBClient instance. +1. Initialize the `AuthToken` module, which loads and stores our authentication token to/from local storage. +2. Create the `SpacetimeDBClient` instance. 3. Register callbacks on any events we want to handle. These will print to standard output messages received from the database and updates about users' names and online statuses. -4. Start our processing thread, which connects to the SpacetimeDB module, updates the SpacetimeDB client and processes commands that come in from the input loop running in the main thread. +4. Start our processing thread which connects to the SpacetimeDB module, updates the SpacetimeDB client and processes commands that come in from the input loop running in the main thread. 5. Start the input loop, which reads commands from standard input and sends them to the processing thread. 6. When the input loop exits, stop the processing thread and wait for it to exit. @@ -154,7 +156,7 @@ string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()!.S void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) { - if(insertedValue.Online) + if (insertedValue.Online) { Console.WriteLine($"{UserNameOrIdentity(insertedValue)} is online"); } @@ -178,20 +180,21 @@ We'll print an appropriate message in each of these cases. ```csharp void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) { - if(oldValue.Name != newValue.Name) + if (oldValue.Name != newValue.Name) { Console.WriteLine($"{UserNameOrIdentity(oldValue)} renamed to {newValue.Name}"); } - if(oldValue.Online != newValue.Online) + + if (oldValue.Online == newValue.Online) + return; + + if (newValue.Online) { - if(newValue.Online) - { - Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); - } - else - { - Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); - } + Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); + } + else + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); } } ``` @@ -209,7 +212,7 @@ void PrintMessage(Message message) { var sender = User.FilterByIdentity(message.Sender); var senderName = "unknown"; - if(sender != null) + if (sender != null) { senderName = UserNameOrIdentity(sender); } @@ -219,7 +222,7 @@ void PrintMessage(Message message) void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent) { - if(dbEvent != null) + if (dbEvent != null) { PrintMessage(insertedValue); } @@ -254,7 +257,11 @@ We'll test both that our identity matches the sender and that the status is `Fai ```csharp void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) { - if(reducerEvent.Identity == local_identity && reducerEvent.Status == ClientApi.Event.Types.Status.Failed) + bool localIdentityFailedToChangeName = + reducerEvent.Identity == local_identity && + reducerEvent.Status == ClientApi.Event.Types.Status.Failed; + + if (localIdentityFailedToChangeName) { Console.Write($"Failed to change name to {name}"); } @@ -268,7 +275,11 @@ We handle warnings on rejected messages the same way as rejected names, though t ```csharp void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) { - if (reducerEvent.Identity == local_identity && reducerEvent.Status == ClientApi.Event.Types.Status.Failed) + bool localIdentityFailedToSendMessage = + reducerEvent.Identity == local_identity && + reducerEvent.Status == ClientApi.Event.Types.Status.Failed; + + if (localIdentityFailedToSendMessage) { Console.Write($"Failed to send message {text}"); } @@ -282,7 +293,10 @@ Once we are connected, we can send our subscription to the SpacetimeDB module. S ```csharp void OnConnect() { - SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User", "SELECT * FROM Message" }); + SpacetimeDBClient.instance.Subscribe(new List + { + "SELECT * FROM User", "SELECT * FROM Message" + }); } ``` @@ -370,12 +384,12 @@ void InputLoop() while (true) { var input = Console.ReadLine(); - if(input == null) + if (input == null) { break; } - if(input.StartsWith("/name ")) + if (input.StartsWith("/name ")) { input_queue.Enqueue(("name", input.Substring(6))); continue; @@ -421,4 +435,4 @@ dotnet run --project client ## What's next? -Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity3d game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example. +Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example. diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 30bd3137..2105457b 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -12,14 +12,19 @@ This tutorial has been tested against UnityEngine version 2022.3.4f1. This tutor ## Prepare Project Structure -This project is separated into two sub-projects, one for the server (module) code and one for the client code. First we'll create the main directory, this directory name doesn't matter but we'll give you an example: +This project is separated into two sub-projects; + +1. Server (module) code +2. Client code. + +First, we'll create an arbitrarily-named project root directory: ```bash mkdir SpacetimeDBUnityTutorial cd SpacetimeDBUnityTutorial ``` -In the following sections we'll be adding a client directory and a server directory, which will contain the client files and the module (server) files respectively. We'll start by populating the client directory. +We'll start by populating the client directory. ## Setting up the Tutorial Unity Project @@ -31,9 +36,9 @@ Open Unity and create a new project by selecting "New" from the Unity Hub or goi ![UnityHub-NewProject](/images/unity-tutorial/UnityHub-NewProject.JPG) -For Project Name use `client`. For Project Location make sure that you use your `SpacetimeDBUnityTutorial` directory. This is the directory that we created in a previous step. +**⚠️ Ensure `3D (URP)` is selected** to properly render the materials in the scene! -**Important: Ensure that you have selected the 3D (URP) template for this project.** If you forget to do this then Unity won't be able to properly render the materials in the scene! +For Project Name use `client`. For Project Location make sure that you use your `SpacetimeDBUnityTutorial` directory. This is the directory that we created in a previous step. ![UnityHub-3DURP](/images/unity-tutorial/UnityHub-3DURP.JPG) @@ -77,7 +82,9 @@ Now that we have everything set up, let's run the project and see it in action: ![Unity-OpenSceneMain](/images/unity-tutorial/Unity-OpenSceneMain.JPG) -NOTE: When you open the scene you may get a message saying you need to import TMP Essentials. When it appears, click the "Import TMP Essentials" button. +**NOTE:** When you open the scene you may get a message saying you need to import TMP Essentials. When it appears, click the "Import TMP Essentials" button. + +🧹 Clear any false-positive TMPro errors that may show. ![Unity Import TMP Essentials](/images/unity-tutorial/Unity-ImportTMPEssentials.JPG) @@ -105,6 +112,8 @@ At this point you should have the single player game working. In your CLI, your spacetime start ``` +💡Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md) at the cost of 1/2 the speed. + 3. Run the following command to initialize the SpacetimeDB server project with Rust as the language: ```bash @@ -612,6 +621,7 @@ private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo { // Spawn the player object and attach the RemotePlayer component var remotePlayer = Instantiate(PlayerPrefab); + // Lookup and apply the position for this new player var entity = EntityComponent.FilterByEntityId(obj.EntityId); var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); @@ -639,21 +649,26 @@ using SpacetimeDB; private float? lastUpdateTime; private void FixedUpdate() { - if ((lastUpdateTime.HasValue && Time.time - lastUpdateTime.Value > 1.0f / movementUpdateSpeed) || !SpacetimeDBClient.instance.IsConnected()) - { - return; - } - - lastUpdateTime = Time.time; - var p = PlayerMovementController.Local.GetModelPosition(); - Reducer.UpdatePlayerPosition(new StdbVector3 - { - X = p.x, - Y = p.y, - Z = p.z, - }, - PlayerMovementController.Local.GetModelRotation(), - PlayerMovementController.Local.IsMoving()); + bool hasUpdatedRecently = lastUpdateTime.HasValue && + Time.time - lastUpdateTime.Value > 1.0f / movementUpdateSpeed; + bool isConnected = SpacetimeDBClient.instance.IsConnected(); + + if (hasUpdatedRecently || !isConnected) + { + return; + } + + lastUpdateTime = Time.time; + var p = PlayerMovementController.Local.GetModelPosition(); + + Reducer.UpdatePlayerPosition(new StdbVector3 + { + Y = p.y, + Z = p.z, + X = p.x, + }, + PlayerMovementController.Local.GetModelRotation(), + PlayerMovementController.Local.IsMoving()); } ``` From 61629adc73ee92d2fad12735fa8a5daeecaf4c1f Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 8 Feb 2024 08:39:19 +0800 Subject: [PATCH 02/24] doc: Onboarding impr, fixes, consistency, cleanup --- docs/unity/part-1.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 2105457b..22e3947c 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -662,13 +662,13 @@ private void FixedUpdate() var p = PlayerMovementController.Local.GetModelPosition(); Reducer.UpdatePlayerPosition(new StdbVector3 - { - Y = p.y, - Z = p.z, - X = p.x, - }, - PlayerMovementController.Local.GetModelRotation(), - PlayerMovementController.Local.IsMoving()); + { + X = p.x, + Y = p.y, + Z = p.z, + }, + PlayerMovementController.Local.GetModelRotation(), + PlayerMovementController.Local.IsMoving()); } ``` From 45cc2d2ea2f4322352feca61265fddf57233e3c5 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 8 Feb 2024 08:42:29 +0800 Subject: [PATCH 03/24] fix: Rm redundant 'module_bindings' mention --- docs/sdks/c-sharp/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md index c143716d..07aa6cf6 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -39,7 +39,7 @@ mkdir -p client/module_bindings spacetime generate --lang csharp --out-dir client/module_bindings --project-path server ``` -Take a look inside `client/module_bindings`. The CLI should have generated five files under `module_bindings`: +Take a look inside `client/module_bindings`. The CLI should have generated five files: ``` module_bindings From 571a04b41f1b4efa21fcf3de1331fe43a538f730 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 8 Feb 2024 09:04:17 +0800 Subject: [PATCH 04/24] fix: Floating period, "arbitrary", "important": - PR review change requests - Additionally: hasUpdatedRecently fix and reformatting --- docs/unity/part-1.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 22e3947c..235669e2 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -15,9 +15,9 @@ This tutorial has been tested against UnityEngine version 2022.3.4f1. This tutor This project is separated into two sub-projects; 1. Server (module) code -2. Client code. +2. Client code -First, we'll create an arbitrarily-named project root directory: +First, we'll create a project root directory (you can choose the name): ```bash mkdir SpacetimeDBUnityTutorial @@ -36,7 +36,7 @@ Open Unity and create a new project by selecting "New" from the Unity Hub or goi ![UnityHub-NewProject](/images/unity-tutorial/UnityHub-NewProject.JPG) -**⚠️ Ensure `3D (URP)` is selected** to properly render the materials in the scene! +**⚠️ Important: Ensure `3D (URP)` is selected** to properly render the materials in the scene! For Project Name use `client`. For Project Location make sure that you use your `SpacetimeDBUnityTutorial` directory. This is the directory that we created in a previous step. @@ -649,8 +649,8 @@ using SpacetimeDB; private float? lastUpdateTime; private void FixedUpdate() { - bool hasUpdatedRecently = lastUpdateTime.HasValue && - Time.time - lastUpdateTime.Value > 1.0f / movementUpdateSpeed; + float? deltaTime = Time.time - lastUpdateTime; + bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; bool isConnected = SpacetimeDBClient.instance.IsConnected(); if (hasUpdatedRecently || !isConnected) From 47b473bf0eef07b299882c2cde6b1a7bf3551a70 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 8 Feb 2024 12:01:03 +0800 Subject: [PATCH 05/24] fix: Mentioned FilterBy, used FindBy - Used FindBy since that was what the tutorial used, and also looking for a single Identity. - Note: There may be a similar rust discrepancy in the Unity pt1 tutorial. It'll work with Filter, but just simply less consistent. Holding off on that since my Rust syntax knowledge !exists. --- docs/modules/c-sharp/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index c1cfb34d..6a0d340e 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -199,7 +199,7 @@ You could extend the validation in `ValidateMessage` in similar ways to `Validat In C# modules, you can register for `Connect` and `Disconnect` events by using a special `ReducerKind`. We'll use the `Connect` event to create a `User` record for the client if it doesn't yet exist, and to set its online status. -We'll use `User.FilterByIdentity` to look up a `User` row for `dbEvent.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FilterByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. +We'll use `User.FindByIdentity` to look up a `User` row for `dbEvent.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. In `server/Lib.cs`, add the definition of the connect reducer to the `Module` class: From 4833cf71bd5ccdc0a8fcbe6eb1455978c9c7d43a Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 8 Feb 2024 15:42:12 +0800 Subject: [PATCH 06/24] fix(Unity-pt1): Rm copy+paste redundant comments * Duplicate comments found both above and within funcs --- docs/unity/part-1.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 235669e2..3bf5cd34 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -293,7 +293,6 @@ We use the `connect` and `disconnect` reducers to update the logged in state of // Called when the client connects, we update the logged_in state to true #[spacetimedb(connect)] pub fn client_connected(ctx: ReducerContext) { - // called when the client connects, we update the logged_in state to true update_player_login_state(ctx, true); } @@ -301,7 +300,6 @@ pub fn client_connected(ctx: ReducerContext) { // Called when the client disconnects, we update the logged_in state to false #[spacetimedb(disconnect)] pub fn client_disconnected(ctx: ReducerContext) { - // Called when the client disconnects, we update the logged_in state to false update_player_login_state(ctx, false); } From 612a680e4be51eba43d2a3c7c5260ecef7b51678 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 8 Feb 2024 17:32:15 +0800 Subject: [PATCH 07/24] fix(unity): Rm unused using statement +merged info * Removed `System.Runtime.CompilerServices` * SpacetimeDB.Module seems to already include this (merged the info) --- docs/modules/c-sharp/quickstart.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index 6a0d340e..f5f73401 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -60,8 +60,10 @@ using SpacetimeDB.Module; using static SpacetimeDB.Runtime; ``` -- `System.Runtime.CompilerServices` allows us to use the `ModuleInitializer` attribute, which we'll use to register our `OnConnect` and `OnDisconnect` callbacks. -- `SpacetimeDB.Module` contains the special attributes we'll use to define our module. +- `System.Runtime.CompilerServices` +- `SpacetimeDB.Module` + - Contains the special attributes we'll use to define our module. + - Allows us to use the `ModuleInitializer` attribute, which we'll use to register our `OnConnect` and `OnDisconnect` callbacks. - `SpacetimeDB.Runtime` contains the raw API bindings SpacetimeDB uses to communicate with the database. We also need to create our static module class which all of the module code will live in. In `server/Lib.cs`, add: From d03030968f185e34c867f14d2091044aa4398067 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 8 Feb 2024 18:49:12 +0800 Subject: [PATCH 08/24] refactor(minor): Code spacing for grouping/clarity --- docs/unity/part-1.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 3bf5cd34..e7dbb674 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -624,9 +624,11 @@ private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo var entity = EntityComponent.FilterByEntityId(obj.EntityId); var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); remotePlayer.transform.position = position; + var movementController = remotePlayer.GetComponent(); movementController.RemoteTargetPosition = position; movementController.RemoteTargetRotation = entity.Direction; + remotePlayer.AddComponent().EntityId = obj.EntityId; } } @@ -726,13 +728,16 @@ private void OnPlayerComponentChanged(PlayerComponent obj) { // Spawn the player object and attach the RemotePlayer component var remotePlayer = Instantiate(PlayerPrefab); + // Lookup and apply the position for this new player var entity = EntityComponent.FilterByEntityId(obj.EntityId); var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); remotePlayer.transform.position = position; + var movementController = remotePlayer.GetComponent(); movementController.RemoteTargetPosition = position; movementController.RemoteTargetRotation = entity.Direction; + remotePlayer.AddComponent().EntityId = obj.EntityId; } } From 4986db6895f8cfd60f9a81b42ea6d5f7751c54bc Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 8 Feb 2024 19:31:06 +0800 Subject: [PATCH 09/24] feat: 'Standalone mode runs in foreground' memo * At general quickstart for `spacetime start` --- docs/getting-started.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 372255be..177a0d25 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -3,7 +3,7 @@ To develop SpacetimeDB applications locally, you will need to run the Standalone version of the server. 1. [Install](/install) the SpacetimeDB CLI (Command Line Interface) -2. Run the start command +2. Run the start command: ```bash spacetime start @@ -11,6 +11,7 @@ spacetime start The server listens on port `3000` by default, customized via `--listen-addr`. +💡 Standalone mode will run in the foreground. ⚠️ SSL is not supported in standalone mode. ## What's Next? From c12c0c8c878d6d489cd955aef9695bb16da1429a Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Fri, 9 Feb 2024 10:18:18 +0800 Subject: [PATCH 10/24] refactor(unity-pt1): Standalone mode foreground memo * Also, removed the "speed" loss mention of C# --- docs/unity/part-1.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index e7dbb674..9b13224c 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -112,7 +112,8 @@ At this point you should have the single player game working. In your CLI, your spacetime start ``` -💡Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md) at the cost of 1/2 the speed. +💡 Standalone mode will run in the foreground. +💡 Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md). 3. Run the following command to initialize the SpacetimeDB server project with Rust as the language: From 6e9b3c37ceb28f8b41fc4be4d5f98c03ad689e15 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 15 Feb 2024 16:20:23 +0800 Subject: [PATCH 11/24] fix(syntaxErr): Fix err, keep FilterBy, handle null - After a verbose discussion, we will eventually swap to FindBy for single-result queries, but not in this PR. - For now, the syntax err is fixed by making the var nullable and suffixing a LINQ FirstOrDefault(). Approved by Tyler in Discord. - We never *actually* created a player in the tutorial. This creates the player. Approved by Tyler in Discord. --- docs/unity/part-1.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 9b13224c..3eaca9e3 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -561,7 +561,17 @@ public class RemotePlayer : MonoBehaviour canvas.worldCamera = Camera.main; // Get the username from the PlayerComponent for this object and set it in the UI - PlayerComponent playerComp = PlayerComponent.FilterByEntityId(EntityId); + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + if (playerComp is null) + { + string inputUsername = UsernameElement.Text; + Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); + Reducer.CreatePlayer(inputUsername); + + // Try again, optimistically assuming success for simplicity + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + } + Username = playerComp.Username; // Get the last location for this player and set the initial position From d63495d68d086e929170353c6a2115179d334e19 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Fri, 23 Feb 2024 14:02:14 +0800 Subject: [PATCH 12/24] doc!(unity-tutorial): Add C# module parity + split - Why? - Despite being a Unity tutorial (we 100% know the user knows C#), the server example used Rust. - This creates friction when the user is already learning multiple new things: The SpacetimeDB architecture, the CLI, the client SDK and server SDK. If they previously did not know Rust, this could add some weight to the onboarding friction. - The Unity tutorial could use an overview since it's quite lengthy and progressive. - Part1 should be split, anyway - it covers way too much for a single section to handle (especially since it jumps between client and server). Splitting between basic multiplayer + advanced makes things more-manageable and less intimidating. - Before: - UNITY TUTORIAL - Part1 (Client + Rust Server) - Part2 (Resources and Scheduling) - Part3 (BitCraft Mini) - After: - UNITY TUTORIAL - BASIC MULTIPLAYER - Overview - Part1 (Setup) - Part2a (Rust Server) - Part2b (C# Server) - Part3 (Client) - UNITY TUTORIAL - ADVANCED - Part4 (Resources and Scheduling) - Part5 (BitCraft Mini) --- docs/sdks/c-sharp/quickstart.md | 2 +- docs/unity/index.md | 23 + docs/unity/part-1.md | 779 +--------------------------- docs/unity/part-2a-rust.md | 316 +++++++++++ docs/unity/part-2b-c-sharp.md | 348 +++++++++++++ docs/unity/part-3.md | 487 +++++++++++++++-- docs/unity/{part-2.md => part-4.md} | 6 +- docs/unity/part-5.md | 108 ++++ nav.ts | 16 +- 9 files changed, 1250 insertions(+), 835 deletions(-) create mode 100644 docs/unity/index.md create mode 100644 docs/unity/part-2a-rust.md create mode 100644 docs/unity/part-2b-c-sharp.md rename docs/unity/{part-2.md => part-4.md} (97%) create mode 100644 docs/unity/part-5.md diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md index 07aa6cf6..92980f42 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -427,7 +427,7 @@ Finally we just need to add a call to `Main` in `Program.cs`: Main(); ``` -Now we can run the client, by hitting start in Visual Studio or running the following command in the `client` directory: +Now, we can run the client by hitting start in Visual Studio or Rider; or by running the following command in the `client` directory: ```bash dotnet run --project client diff --git a/docs/unity/index.md b/docs/unity/index.md new file mode 100644 index 00000000..2b8e6d67 --- /dev/null +++ b/docs/unity/index.md @@ -0,0 +1,23 @@ +# Unity Tutorial Overview + +Need help with the tutorial or CLI commands? [Join our Discord server](https://discord.gg/spacetimedb)! + +The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, using a command line terminal and coding. + +We'll give you some CLI commands to execute. If you are using Windows, we recommend using Git Bash or PowerShell. For Mac, we recommend Terminal. + +Tested with UnityEngine `2022.3.20f1 LTS` (and may also work on newer versions). + +## Unity Tutorial - Basic Multiplayer +Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](/docs/modules/rust) or [C#](/docs/modules/c-sharp): + +- [Part 1 - Setup](/docs/unity/part-1.md) +- [Part 2a - Server (Rust)](/docs/unity/part-2a-rust.md) +- [Part 2b - Server (C#)](/docs/unity/part-2b-csharp.md) +- [Part 3 - Client](/docs/unity/part-3.md) + +## Unity Tutorial - Advanced +By this point, you should already have a basic understanding of SpacetimeDB client, server and CLI: + +- [Part 4 - Resources & Scheduling](/docs/unity/part-4.md) +- [Part 5 - BitCraft Mini](/docs/unity/part-5.md) diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 3eaca9e3..b8b8c3c0 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -1,15 +1,9 @@ -# Part 1 - Basic Multiplayer +# Unity Tutorial - Basic Multiplayer - Part 1 - Setup ![UnityTutorial-HeroImage](/images/unity-tutorial/UnityTutorial-HeroImage.JPG) Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! -The objective of this tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end of this tutorial you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, using a command line terminal, and coding. - -In this tutorial we'll be giving you some CLI commands to execute. If you are using Windows we recommend using Git Bash or powershell. If you're on mac we recommend you use the Terminal application. If you encouter issues with any of the commands in this guide, please reach out to us through our discord server and we would be happy to help assist you. - -This tutorial has been tested against UnityEngine version 2022.3.4f1. This tutorial may work on newer versions as well. - ## Prepare Project Structure This project is separated into two sub-projects; @@ -115,773 +109,14 @@ spacetime start 💡 Standalone mode will run in the foreground. 💡 Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md). -3. Run the following command to initialize the SpacetimeDB server project with Rust as the language: - -```bash -spacetime init --lang=rust server -``` +### The Entity Component Systems (ECS) -This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. - -### Understanding Entity Component Systems - -Entity Component System (ECS) is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). +Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. -### SpacetimeDB Tables - -In this section we'll be making some edits to the file `server/src/lib.rs`. We recommend you open up this file in an IDE like VSCode or RustRover. - -**Important: Open the `server/src/lib.rs` file and delete its contents. We will be writing it from scratch here.** - -First we need to add some imports at the top of the file. - -**Copy and paste into lib.rs:** - -```rust -use spacetimedb::{spacetimedb, Identity, SpacetimeType, ReducerContext}; -use log; -``` - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our rust module reference. Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.rs:** - -```rust -// We're using this table as a singleton, so there should typically only be one element where the version is 0. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct Config { - #[primarykey] - pub version: u32, - pub message_of_the_day: String, -} -``` - -Next we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `#[derive(SpacetimeType)]` and a `#[spacetimedb(table)]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. So therefore, `StdbVector3` is not itself a table. - -**Append to the bottom of lib.rs:** - -```rust -// This allows us to store 3D points in tables. -#[derive(SpacetimeType, Clone)] -pub struct StdbVector3 { - pub x: f32, - pub y: f32, - pub z: f32, -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```rust -// This stores information related to all entities in our game. In this tutorial -// all entities must at least have an entity_id, a position, a direction and they -// must specify whether or not they are moving. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct EntityComponent { - #[primarykey] - // The autoinc macro here just means every time we insert into this table - // we will receive a new row where this value will be increased by one. This - // allows us to easily get rows where `entity_id` is unique. - #[autoinc] - pub entity_id: u64, - pub position: StdbVector3, - pub direction: f32, - pub moving: bool, -} -``` - -Next we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `entity_id`. You'll see how this works later in the `create_player` reducer. - -**Append to the bottom of lib.rs:** - -```rust -// All players have this component and it associates an entity with the user's -// Identity. It also stores their username and whether or not they're logged in. -#[derive(Clone)] -#[spacetimedb(table)] -pub struct PlayerComponent { - // An entity_id that matches an entity_id in the `EntityComponent` table. - #[primarykey] - pub entity_id: u64, - // The user's identity, which is unique to each player - #[unique] - pub owner_id: Identity, - pub username: String, - pub logged_in: bool, -} -``` - -Next we write our very first reducer, `create_player`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.rs:** - -```rust -// This reducer is called when the user logs in for the first time and -// enters a username -#[spacetimedb(reducer)] -pub fn create_player(ctx: ReducerContext, username: String) -> Result<(), String> { - // Get the Identity of the client who called this reducer - let owner_id = ctx.sender; - - // Make sure we don't already have a player with this identity - if PlayerComponent::filter_by_owner_id(&owner_id).is_some() { - log::info!("Player already exists"); - return Err("Player already exists".to_string()); - } - - // Create a new entity for this player and get a unique `entity_id`. - let entity_id = EntityComponent::insert(EntityComponent - { - entity_id: 0, - position: StdbVector3 { x: 0.0, y: 0.0, z: 0.0 }, - direction: 0.0, - moving: false, - }).expect("Failed to create a unique PlayerComponent.").entity_id; - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - PlayerComponent::insert(PlayerComponent { - entity_id, - owner_id, - username: username.clone(), - logged_in: true, - }).expect("Failed to insert player component."); - - log::info!("Player created: {}({})", username, entity_id); - - Ok(()) -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI or a client SDK or they can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `sender` value of the `ReducerContext`. -- `disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next we are going to write a custom `init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config::filter_by_version(0)`. - -**Append to the bottom of lib.rs:** - -```rust -// Called when the module is initially published -#[spacetimedb(init)] -pub fn init() { - Config::insert(Config { - version: 0, - message_of_the_day: "Hello, World!".to_string(), - }).expect("Failed to insert config."); -} -``` - -We use the `connect` and `disconnect` reducers to update the logged in state of the player. The `update_player_login_state` helper function looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `logged_in` variable and calls the auto-generated `update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.rs:** - -```rust -// Called when the client connects, we update the logged_in state to true -#[spacetimedb(connect)] -pub fn client_connected(ctx: ReducerContext) { - update_player_login_state(ctx, true); -} - - -// Called when the client disconnects, we update the logged_in state to false -#[spacetimedb(disconnect)] -pub fn client_disconnected(ctx: ReducerContext) { - update_player_login_state(ctx, false); -} - -// This helper function gets the PlayerComponent, sets the logged -// in variable and updates the PlayerComponent table row. -pub fn update_player_login_state(ctx: ReducerContext, logged_in: bool) { - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - // We clone the PlayerComponent so we can edit it and pass it back. - let mut player = player.clone(); - player.logged_in = logged_in; - PlayerComponent::update_by_entity_id(&player.entity_id.clone(), player); - } -} -``` - -Our final reducer handles player movement. In `update_player_position` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `create_player` first. - -Using the `entity_id` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `update` function. - -**Append to the bottom of lib.rs:** - -```rust -// Updates the position of a player. This is also called when the player stops moving. -#[spacetimedb(reducer)] -pub fn update_player_position( - ctx: ReducerContext, - position: StdbVector3, - direction: f32, - moving: bool, -) -> Result<(), String> { - // First, look up the player using the sender identity, then use that - // entity_id to retrieve and update the EntityComponent - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - if let Some(mut entity) = EntityComponent::filter_by_entity_id(&player.entity_id) { - entity.position = position; - entity.direction = direction; - entity.moving = moving; - EntityComponent::update_by_entity_id(&player.entity_id, entity); - return Ok(()); - } - } - - // If we can not find the PlayerComponent or EntityComponent for - // this player then something went wrong. - return Err("Player not found".to_string()); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -Now that we've written the code for our server module, 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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the Troubleshooting section at the end of this tutorial. - -## Updating our Unity Project to use SpacetimeDB - -Now we are ready to connect our bitcraft mini project to SpacetimeDB. - -### Import the SDK and Generate Module Files - -1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. - -```bash -https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git -``` - -![Unity-PackageManager](/images/unity-tutorial/Unity-PackageManager.JPG) - -3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. - -```bash -mkdir -p ../client/Assets/module_bindings -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -``` - -### Connect to Your SpacetimeDB Module - -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. - -![Unity-AddNetworkManager](/images/unity-tutorial/Unity-AddNetworkManager.JPG) - -Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: - -**Append to the top of TutorialGameManager.cs** - -```csharp -using SpacetimeDB; -using SpacetimeDB.Types; -using System.Linq; -``` - -At the top of the class definition add the following members: - -**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** - -```csharp -// These are connection variables that are exposed on the GameManager -// inspector. -[SerializeField] private string moduleAddress = "unity-tutorial"; -[SerializeField] private string hostName = "localhost:3000"; - -// This is the identity for this player that is automatically generated -// the first time you log in. We set this variable when the -// onIdentityReceived callback is triggered by the SDK after connecting -private Identity local_identity; -``` - -The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. - -Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. - -**REPLACE the Start() function in TutorialGameManager.cs** - -```csharp -// Start is called before the first frame update -void Start() -{ - instance = this; - - SpacetimeDBClient.instance.onConnect += () => - { - Debug.Log("Connected."); - - // Request all tables - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM *", - }); - }; - - // Called when we have an error connecting to SpacetimeDB - SpacetimeDBClient.instance.onConnectError += (error, message) => - { - Debug.LogError($"Connection error: " + message); - }; - - // Called when we are disconnected from SpacetimeDB - SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => - { - Debug.Log("Disconnected."); - }; - - // Called when we receive the client identity from SpacetimeDB - SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { - AuthToken.SaveToken(token); - local_identity = identity; - }; - - // Called after our local cache is populated from a Subscribe call - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - - // Now that we’ve registered all our callbacks, lets connect to spacetimedb - SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); -} -``` - -In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. - -Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. - ---- - -**Local Client Cache** - -The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. - ---- - -Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. +### Create the Server Module -**Append after the Start() function in TutorialGameManager.cs** - -```csharp -void OnSubscriptionApplied() -{ - // If we don't have any data for our player, then we are creating a - // new one. Let's show the username dialog, which will then call the - // create player reducer - var player = PlayerComponent.FilterByOwnerId(local_identity); - if (player == null) - { - // Show username selection - UIUsernameChooser.instance.Show(); - } - - // Show the Message of the Day in our Config table of the Client Cache - UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); - - // Now that we've done this work we can unregister this callback - SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; -} -``` - -### Adding the Multiplayer Functionality - -Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. - -**Append to the top of UIUsernameChooser.cs** - -```csharp -using SpacetimeDB.Types; -``` - -Then we're doing a modification to the `ButtonPressed()` function: - -**Modify the ButtonPressed function in UIUsernameChooser.cs** - -```csharp -public void ButtonPressed() -{ - CameraController.RemoveDisabler(GetHashCode()); - _panel.SetActive(false); - - // Call the SpacetimeDB CreatePlayer reducer - Reducer.CreatePlayer(_usernameField.text); -} -``` - -We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. - -First append this using to the top of `RemotePlayer.cs` - -**Create file RemotePlayer.cs, then replace its contents:** - -```csharp -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using SpacetimeDB.Types; -using TMPro; - -public class RemotePlayer : MonoBehaviour -{ - public ulong EntityId; - - public TMP_Text UsernameElement; - - public string Username { set { UsernameElement.text = value; } } - - void Start() - { - // Initialize overhead name - UsernameElement = GetComponentInChildren(); - var canvas = GetComponentInChildren(); - canvas.worldCamera = Camera.main; - - // Get the username from the PlayerComponent for this object and set it in the UI - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - if (playerComp is null) - { - string inputUsername = UsernameElement.Text; - Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); - Reducer.CreatePlayer(inputUsername); - - // Try again, optimistically assuming success for simplicity - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - } - - Username = playerComp.Username; - - // Get the last location for this player and set the initial position - EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); - transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - - // Register for a callback that is called when the client gets an - // update for a row in the EntityComponent table - EntityComponent.OnUpdate += EntityComponent_OnUpdate; - } -} -``` - -We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. - -**Append to bottom of RemotePlayer class in RemotePlayer.cs:** - -```csharp -private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) -{ - // If the update was made to this object - if(obj.EntityId == EntityId) - { - var movementController = GetComponent(); - - // Update target position, rotation, etc. - movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); - movementController.RemoteTargetRotation = obj.Direction; - movementController.SetMoving(obj.Moving); - } -} -``` - -Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. - -**Append to bottom of Start() function in TutorialGameManager.cs:** - -```csharp -PlayerComponent.OnInsert += PlayerComponent_OnInsert; -``` - -Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. - -**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** - -```csharp -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } -} -``` - -Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. - -**Append to the top of LocalPlayer.cs** - -```csharp -using SpacetimeDB.Types; -using SpacetimeDB; -``` - -**Append to the bottom of LocalPlayer class in LocalPlayer.cs** - -```csharp -private float? lastUpdateTime; -private void FixedUpdate() -{ - float? deltaTime = Time.time - lastUpdateTime; - bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; - bool isConnected = SpacetimeDBClient.instance.IsConnected(); - - if (hasUpdatedRecently || !isConnected) - { - return; - } - - lastUpdateTime = Time.time; - var p = PlayerMovementController.Local.GetModelPosition(); - - Reducer.UpdatePlayerPosition(new StdbVector3 - { - X = p.x, - Y = p.y, - Z = p.z, - }, - PlayerMovementController.Local.GetModelRotation(), - PlayerMovementController.Local.IsMoving()); -} -``` - -Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. - -![GameManager-Inspector2](/images/unity-tutorial/GameManager-Inspector2.JPG) - -### Play the Game! - -Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. - -![Unity-AddOpenScenes](/images/unity-tutorial/Unity-AddOpenScenes.JPG) - -When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. - -### Implement Player Logout - -So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. - -**Append to the bottom of Start() function in TutorialGameManager.cs** -```csharp -PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; -``` - -We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. - -Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. - -**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** -```csharp -private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(newValue); -} - -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(obj); -} - -private void OnPlayerComponentChanged(PlayerComponent obj) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it - var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); - if (obj.LoggedIn) - { - // Only spawn remote players who aren't already spawned - if (existingPlayer == null) - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } - } - else - { - if (existingPlayer != null) - { - Destroy(existingPlayer.gameObject); - } - } - } -} -``` - -Now you when you play the game you should see remote players disappear when they log out. - -### Finally, Add Chat Support - -The project has a chat window but so far all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.rs``. - -**Append to the bottom of server/src/lib.rs:** - -```rust -#[spacetimedb(table)] -pub struct ChatMessage { - // The primary key for this table will be auto-incremented - #[primarykey] - #[autoinc] - pub message_id: u64, - - // The entity id of the player that sent the message - pub sender_id: u64, - // Message contents - pub text: String, -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.rs:** - -```rust -// Adds a chat entry to the ChatMessage table -#[spacetimedb(reducer)] -pub fn send_chat_message(ctx: ReducerContext, text: String) -> Result<(), String> { - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - // Now that we have the player we can insert the chat message using the player entity id. - ChatMessage::insert(ChatMessage { - // this column auto-increments so we can set it to 0 - message_id: 0, - sender_id: player.entity_id, - text, - }) - .unwrap(); - - return Ok(()); - } - - Err("Player not found".into()) -} -``` - -Before updating the client, let's generate the client files and update publish our module. - -**Execute commands in the server/ directory** -```bash -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -spacetime publish -c unity-tutorial -``` - -On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. - -**Append to the top of UIChatController.cs:** -```csharp -using SpacetimeDB.Types; -``` - -**REPLACE the OnChatButtonPress function in UIChatController.cs:** - -```csharp -public void OnChatButtonPress() -{ - Reducer.SendChatMessage(_chatInput.text); - _chatInput.text = ""; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: - -**Append to the bottom of the Start() function in TutorialGameManager.cs:** -```csharp -Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; -``` - -Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. - -**Append after the Start() function in TutorialGameManager.cs** -```csharp -private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) -{ - var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); - if (player != null) - { - UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); - } -} -``` - -Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. - -## Conclusion - -This concludes the first part of the tutorial. We've learned about the basics of SpacetimeDB and how to use it to create a multiplayer game. In the next part of the tutorial we will add resource nodes to the game and learn about scheduled reducers. - ---- - -### Troubleshooting - -- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` - -- If you get this exception when running the project: - -``` -NullReferenceException: Object reference not set to an instance of an object -TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) -``` - -Check to see if your GameManager object in the Scene has the NetworkManager component attached. - -- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. - -``` -Connection error: Unable to connect to the remote server -``` +From here, the tutorial continues with your favorite server module language of choice: + - [Rust](part-2a-rust.md) + - [C#](part-2b-csharp.md) diff --git a/docs/unity/part-2a-rust.md b/docs/unity/part-2a-rust.md new file mode 100644 index 00000000..aa8f68d4 --- /dev/null +++ b/docs/unity/part-2a-rust.md @@ -0,0 +1,316 @@ +# Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (Rust) + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 1 Tutorial](/docs/unity/part-1.md) + +## Create a Server Module + +Run the following command to initialize the SpacetimeDB server module project with Rust as the language: + +```bash +spacetime init --lang=rust server +``` + +This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. + +### SpacetimeDB Tables + +In this section we'll be making some edits to the file `server/src/lib.rs`. We recommend you open up this file in an IDE like VSCode or RustRover. + +**Important: Open the `server/src/lib.rs` file and delete its contents. We will be writing it from scratch here.** + +First we need to add some imports at the top of the file. + +**Copy and paste into lib.rs:** + +```rust +use spacetimedb::{spacetimedb, Identity, SpacetimeType, ReducerContext}; +use log; +``` + +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [Rust module reference](/docs/modules/rust). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. + +**Append to the bottom of lib.rs:** + +```rust +// We're using this table as a singleton, so there should typically only be one element where the version is 0. +#[spacetimedb(table)] +#[derive(Clone)] +pub struct Config { + #[primarykey] + pub version: u32, + pub message_of_the_day: String, +} +``` + +Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `#[derive(SpacetimeType)]` and a `#[spacetimedb(table)]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. + +**Append to the bottom of lib.rs:** + +```rust +// This allows us to store 3D points in tables. +#[derive(SpacetimeType, Clone)] +pub struct StdbVector3 { + pub x: f32, + pub y: f32, + pub z: f32, +} +``` + +Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. + +```rust +// This stores information related to all entities in our game. In this tutorial +// all entities must at least have an entity_id, a position, a direction and they +// must specify whether or not they are moving. +#[spacetimedb(table)] +#[derive(Clone)] +pub struct EntityComponent { + #[primarykey] + // The autoinc macro here just means every time we insert into this table + // we will receive a new row where this value will be increased by one. This + // allows us to easily get rows where `entity_id` is unique. + #[autoinc] + pub entity_id: u64, + pub position: StdbVector3, + pub direction: f32, + pub moving: bool, +} +``` + +Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `entity_id`. You'll see how this works later in the `create_player` reducer. + +**Append to the bottom of lib.rs:** + +```rust +// All players have this component and it associates an entity with the user's +// Identity. It also stores their username and whether or not they're logged in. +#[derive(Clone)] +#[spacetimedb(table)] +pub struct PlayerComponent { + // An entity_id that matches an entity_id in the `EntityComponent` table. + #[primarykey] + pub entity_id: u64, + + // The user's identity, which is unique to each player + #[unique] + pub owner_id: Identity, + pub username: String, + pub logged_in: bool, +} +``` + +Next, we write our very first reducer, `create_player`. From the client we will call this reducer when we create a new player: + +**Append to the bottom of lib.rs:** + +```rust +// This reducer is called when the user logs in for the first time and +// enters a username +#[spacetimedb(reducer)] +pub fn create_player(ctx: ReducerContext, username: String) -> Result<(), String> { + // Get the Identity of the client who called this reducer + let owner_id = ctx.sender; + + // Make sure we don't already have a player with this identity + if PlayerComponent::filter_by_owner_id(&owner_id).is_some() { + log::info!("Player already exists"); + return Err("Player already exists".to_string()); + } + + // Create a new entity for this player and get a unique `entity_id`. + let entity_id = EntityComponent::insert(EntityComponent + { + entity_id: 0, + position: StdbVector3 { x: 0.0, y: 0.0, z: 0.0 }, + direction: 0.0, + moving: false, + }).expect("Failed to create a unique PlayerComponent.").entity_id; + + // The PlayerComponent uses the same entity_id and stores the identity of + // the owner, username, and whether or not they are logged in. + PlayerComponent::insert(PlayerComponent { + entity_id, + owner_id, + username: username.clone(), + logged_in: true, + }).expect("Failed to insert player component."); + + log::info!("Player created: {}({})", username, entity_id); + + Ok(()) +} +``` + +--- + +**SpacetimeDB Reducers** + +"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. + +--- + +SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. + +- `init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. +- `connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `sender` value of the `ReducerContext`. +- `disconnect` - Called when a user disconnects from the SpacetimeDB module. + +Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. + +**Append to the bottom of lib.rs:** + +```rust +// Called when the module is initially published +#[spacetimedb(init)] +pub fn init() { + Config::insert(Config { + version: 0, + message_of_the_day: "Hello, World!".to_string(), + }).expect("Failed to insert config."); +} +``` + +We use the `connect` and `disconnect` reducers to update the logged in state of the player. The `update_player_login_state` helper function looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `logged_in` variable and calls the auto-generated `update` function on `PlayerComponent` to update the row. + +**Append to the bottom of lib.rs:** + +```rust +// Called when the client connects, we update the logged_in state to true +#[spacetimedb(connect)] +pub fn client_connected(ctx: ReducerContext) { + update_player_login_state(ctx, true); +} +``` +```rust +// Called when the client disconnects, we update the logged_in state to false +#[spacetimedb(disconnect)] +pub fn client_disconnected(ctx: ReducerContext) { + update_player_login_state(ctx, false); +} +``` +```rust +// This helper function gets the PlayerComponent, sets the logged +// in variable and updates the PlayerComponent table row. +pub fn update_player_login_state(ctx: ReducerContext, logged_in: bool) { + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + // We clone the PlayerComponent so we can edit it and pass it back. + let mut player = player.clone(); + player.logged_in = logged_in; + PlayerComponent::update_by_entity_id(&player.entity_id.clone(), player); + } +} +``` + +Our final reducer handles player movement. In `update_player_position` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `create_player` first. + +Using the `entity_id` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `update` function. + +**Append to the bottom of lib.rs:** + +```rust +// Updates the position of a player. This is also called when the player stops moving. +#[spacetimedb(reducer)] +pub fn update_player_position( + ctx: ReducerContext, + position: StdbVector3, + direction: f32, + moving: bool, +) -> Result<(), String> { + // First, look up the player using the sender identity, then use that + // entity_id to retrieve and update the EntityComponent + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + if let Some(mut entity) = EntityComponent::filter_by_entity_id(&player.entity_id) { + entity.position = position; + entity.direction = direction; + entity.moving = moving; + EntityComponent::update_by_entity_id(&player.entity_id, entity); + return Ok(()); + } + } + + // If we can not find the PlayerComponent or EntityComponent for + // this player then something went wrong. + return Err("Player not found".to_string()); +} +``` + +--- + +**Server Validation** + +In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. + +--- + +### Publishing a Module to SpacetimeDB + +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. + +```bash +cd server +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. + +### Finally, Add Chat Support + +The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. + +First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.rs``. + +**Append to the bottom of server/src/lib.rs:** + +```rust +#[spacetimedb(table)] +pub struct ChatMessage { + // The primary key for this table will be auto-incremented + #[primarykey] + #[autoinc] + pub message_id: u64, + + // The entity id of the player that sent the message + pub sender_id: u64, + // Message contents + pub text: String, +} +``` + +Now we need to add a reducer to handle inserting new chat messages. + +**Append to the bottom of server/src/lib.rs:** + +```rust +// Adds a chat entry to the ChatMessage table +#[spacetimedb(reducer)] +pub fn send_chat_message(ctx: ReducerContext, text: String) -> Result<(), String> { + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + // Now that we have the player we can insert the chat message using the player entity id. + ChatMessage::insert(ChatMessage { + // this column auto-increments so we can set it to 0 + message_id: 0, + sender_id: player.entity_id, + text, + }) + .unwrap(); + + return Ok(()); + } + + Err("Player not found".into()) +} +``` + +## Wrapping Up + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. + +From here, the tutorial continues with more-advanced topics. The [next tutorial](/docs/unity/part-4.md) introduces Resources & Scheduling. diff --git a/docs/unity/part-2b-c-sharp.md b/docs/unity/part-2b-c-sharp.md new file mode 100644 index 00000000..faafe86c --- /dev/null +++ b/docs/unity/part-2b-c-sharp.md @@ -0,0 +1,348 @@ +# Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (C#) + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 1 Tutorial](/docs/unity/part-1.md) + +## Create a Server Module + +Run the following command to initialize the SpacetimeDB server module project with Rust as the language: + +```bash +spacetime init --lang=rust server +``` + +This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. + +### SpacetimeDB Tables + +In this section we'll be making some edits to the file `server/src/lib.cs`. We recommend you open up this file in an IDE like VSCode or RustRover. + +**Important: Open the `server/src/lib.cs` file and delete its contents. We will be writing it from scratch here.** + +First we need to add some imports at the top of the file. + +**Copy and paste into lib.cs:** + +```csharp +// using SpacetimeDB; // Uncomment to omit `SpacetimeDB` attribute prefixes +using SpacetimeDB.Module; +using static SpacetimeDB.Runtime; +``` + +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](/docs/modules/c-sharp). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. + +**Append to the bottom of lib.cs:** + +```csharp +/// We're using this table as a singleton, +/// so there should typically only be one element where the version is 0. +[SpacetimeDB.Table] +public partial class Config +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public Identity Version; + public string? MessageOfTheDay; +} +``` + +Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. + +**Append to the bottom of lib.cs:** + +```csharp +/// This allows us to store 3D points in tables. +[SpacetimeDB.Type] +public partial class StdbVector3 +{ + public float X; + public float Y; + public float Z; +} +``` + +Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. + +```csharp +/// This stores information related to all entities in our game. In this tutorial +/// all entities must at least have an entity_id, a position, a direction and they +/// must specify whether or not they are moving. +[SpacetimeDB.Table] +public partial class EntityComponent +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong EntityId; + public StdbVector3 Position; + public float Direction; + public bool Moving; +} +``` + +Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. + +**Append to the bottom of lib.cs:** + +```csharp +/// All players have this component and it associates an entity with the user's +/// Identity. It also stores their username and whether or not they're logged in. +[SpacetimeDB.Table] +public partial class PlayerComponent +{ + // An EntityId that matches an EntityId in the `EntityComponent` table. + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + // The user's identity, which is unique to each player + [SpacetimeDB.Column(ColumnAttrs.Unique)] + public Identity Identity; + public string? Username; + public bool LoggedIn; +} +``` + +Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: + +**Append to the bottom of lib.cs:** + +```csharp +/// This reducer is called when the user logs in for the first time and +/// enters a username. +[SpacetimeDB.Reducer] +public static void CreatePlayer(DbEventArgs dbEvent, string username) +{ + // Get the Identity of the client who called this reducer + Identity sender = dbEvent.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"); + } + + // Create a new entity for this player + try + { + new EntityComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, + Direction = 0, + Moving = false, + }.Insert(); + } + catch + { + Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); + Throw; + } + + // The PlayerComponent uses the same entity_id and stores the identity of + // the owner, username, and whether or not they are logged in. + try + { + new PlayerComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Identity = dbEvent.Sender, + Username = username, + LoggedIn = true, + }.Insert(); + } + catch + { + Log("Error: Failed to insert PlayerComponent", LogLevel.Error); + throw; + } + Log($"Player created: {username}"); +} +``` + +--- + +**SpacetimeDB Reducers** + +"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. + +--- + +SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. + +- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. +- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. +- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. + +Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the module is initially published +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void OnInit() +{ + try + { + new Config + { + Version = 0, + MessageOfTheDay = "Hello, World!", + }.Insert(); + } + catch + { + Log("Error: Failed to insert Config", LogLevel.Error); + throw; + } +} +``` + +We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the client connects, we update the LoggedIn state to true +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void ClientConnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:true); +``` +```csharp +/// Called when the client disconnects, we update the logged_in state to false +[SpacetimeDB.Reducer(ReducerKind.Disconnect)] +public static void ClientDisonnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:false); +``` +```csharp +/// This helper function gets the PlayerComponent, sets the LoggedIn +/// variable and updates the PlayerComponent table row. +private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) +{ + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + player.LoggedIn = loggedIn; + PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); +} +``` + +Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. + +Using the `EntityId` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. + +**Append to the bottom of lib.cs:** + +```csharp +/// Updates the position of a player. This is also called when the player stops moving. +[SpacetimeDB.Reducer] +private static void UpdatePlayerPosition( + DbEventArgs dbEvent, + StdbVector3 position, + float direction, + bool moving) +{ + // First, look up the player using the sender identity + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + // Use the Player's EntityId to retrieve and update the EntityComponent + ulong playerEntityId = player.EntityId; + EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); + if (entity is null) + { + throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); + } + + entity.Position = position; + entity.Direction = direction; + entity.Moving = moving; + EntityComponent.UpdateByEntityId(playerEntityId, entity); +} +``` + +--- + +**Server Validation** + +In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. + +--- + +### Publishing a Module to SpacetimeDB + +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. + +```bash +cd server +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. + +### Finally, Add Chat Support + +The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. + +First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +[SpacetimeDB.Table] +public partial class ChatMessage +{ + // The primary key for this table will be auto-incremented + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + + // The entity id of the player that sent the message + public ulong SenderId; + + // Message contents + public string? Text; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +/// Adds a chat entry to the ChatMessage table +[SpacetimeDB.Reducer] +public static void SendChatMessage(DbEventArgs dbEvent, string text) +{ + // Get the player's entity id + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + + // Insert the chat message + new ChatMessage + { + SenderId = player.EntityId, + Text = text, + }.Insert(); +} +``` + +## Wrapping Up + +💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. + +From here, the tutorial continues with more-advanced topics. The [next tutorial](/docs/unity/part-4.md) introduces Resources & Scheduling. diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index b49b5a5d..98cfff0a 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -1,104 +1,479 @@ -# Part 3 - BitCraft Mini +# Unity Tutorial - Basic Multiplayer - Part 3 - Client -**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! -BitCraft Mini is a game that we developed which extends the code you've already developed in this tutorial. It is inspired by our game [BitCraft](https://bitcraftonline.com) and illustrates how you could build a more complex game from just the components we've discussed. Right now you can walk around, mine ore, and manage your inventory. +This progressive tutorial is continued from one of the Part 2 tutorials: +- [Rust Server Module](/docs/unity/part-2a-rust.md) +- [C# Server Module](/docs/unity/part-2b-c-sharp.md) -## 1. Download +## Updating our Unity Project Client to use SpacetimeDB -You can git-clone BitCraftMini from here: +Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. -```plaintext -git clone ssh://git@github.com/clockworklabs/BitCraftMini +### Import the SDK and Generate Module Files + +1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. + +```bash +https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git ``` -Once you have downloaded BitCraftMini, you will need to compile the spacetime module. +![Unity-PackageManager](/images/unity-tutorial/Unity-PackageManager.JPG) -## 2. Compile the Spacetime Module +3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. -In order to compile the BitCraftMini module, you will need to install cargo. You can install cargo from here: +```bash +mkdir -p ../client/Assets/module_bindings +spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp +``` -> https://www.rust-lang.org/tools/install +### Connect to Your SpacetimeDB Module -Once you have cargo installed, you can compile and publish the module with these commands: +The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. -```bash -cd BitCraftMini/Server -spacetime publish +![Unity-AddNetworkManager](/images/unity-tutorial/Unity-AddNetworkManager.JPG) + +Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: + +**Append to the top of TutorialGameManager.cs** + +```csharp +using SpacetimeDB; +using SpacetimeDB.Types; +using System.Linq; ``` -`spacetime publish` will output an address where your module has been deployed to. You will want to copy/save this address because you will need it in step 3. Here is an example of what it should look like: +At the top of the class definition add the following members: -```plaintext -$ spacetime publish -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - Finished release [optimized] target(s) in 0.03s -Publish finished successfully. -Created new database with address: c91c17ecdcea8a05302be2bad9dd59b3 +**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** + +```csharp +// These are connection variables that are exposed on the GameManager +// inspector. +[SerializeField] private string moduleAddress = "unity-tutorial"; +[SerializeField] private string hostName = "localhost:3000"; + +// This is the identity for this player that is automatically generated +// the first time you log in. We set this variable when the +// onIdentityReceived callback is triggered by the SDK after connecting +private Identity local_identity; ``` -Optionally, you can specify a name when you publish the module: +The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. + +Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. + +**REPLACE the Start() function in TutorialGameManager.cs** + +```csharp +// Start is called before the first frame update +void Start() +{ + instance = this; + + SpacetimeDBClient.instance.onConnect += () => + { + Debug.Log("Connected."); + + // Request all tables + SpacetimeDBClient.instance.Subscribe(new List() + { + "SELECT * FROM *", + }); + }; + + // Called when we have an error connecting to SpacetimeDB + SpacetimeDBClient.instance.onConnectError += (error, message) => + { + Debug.LogError($"Connection error: " + message); + }; + + // Called when we are disconnected from SpacetimeDB + SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => + { + Debug.Log("Disconnected."); + }; + + // Called when we receive the client identity from SpacetimeDB + SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { + AuthToken.SaveToken(token); + local_identity = identity; + }; + + // Called after our local cache is populated from a Subscribe call + SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + + // Now that we’ve registered all our callbacks, lets connect to spacetimedb + SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); +} +``` -```bash -spacetime publish "unique-module-name" +In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. + +Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. + +--- + +**Local Client Cache** + +The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. + +--- + +Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. + +**Append after the Start() function in TutorialGameManager.cs** + +```csharp +void OnSubscriptionApplied() +{ + // If we don't have any data for our player, then we are creating a + // new one. Let's show the username dialog, which will then call the + // create player reducer + var player = PlayerComponent.FilterByOwnerId(local_identity); + if (player == null) + { + // Show username selection + UIUsernameChooser.instance.Show(); + } + + // Show the Message of the Day in our Config table of the Client Cache + UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); + + // Now that we've done this work we can unregister this callback + SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; +} ``` -Currently, all the named modules exist in the same namespace so if you get a message saying that database is not owned by you, it means that someone else has already published a module with that name. You can either choose a different name or you can use the address instead. If you specify a name when you publish, you can use that name in place of the autogenerated address in both the CLI and in the Unity client. +### Adding the Multiplayer Functionality -In the BitCraftMini module we have a function called `initialize()`. This function should be called immediately after publishing the module to spacetimedb. This function is in charge of generating some initial settings that are required for the server to operate. You can call this function like so: +Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. -```bash -spacetime call "" "initialize" "[]" +**Append to the top of UIUsernameChooser.cs** + +```csharp +using SpacetimeDB.Types; ``` -Here we are telling spacetime to invoke the `initialize()` function on our module "bitcraftmini". If the function had some arguments, we would json encode them and put them into the "[]". Since `initialize()` requires no parameters, we just leave it empty. +Then we're doing a modification to the `ButtonPressed()` function: -After you have called `initialize()` on the spacetime module you shouldgenerate the client files: +**Modify the ButtonPressed function in UIUsernameChooser.cs** -```bash -spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs +```csharp +public void ButtonPressed() +{ + CameraController.RemoveDisabler(GetHashCode()); + _panel.SetActive(false); + + // Call the SpacetimeDB CreatePlayer reducer + Reducer.CreatePlayer(_usernameField.text); +} +``` + +We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. + +First append this using to the top of `RemotePlayer.cs` + +**Create file RemotePlayer.cs, then replace its contents:** + +```csharp +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using SpacetimeDB.Types; +using TMPro; + +public class RemotePlayer : MonoBehaviour +{ + public ulong EntityId; + + public TMP_Text UsernameElement; + + public string Username { set { UsernameElement.text = value; } } + + void Start() + { + // Initialize overhead name + UsernameElement = GetComponentInChildren(); + var canvas = GetComponentInChildren(); + canvas.worldCamera = Camera.main; + + // Get the username from the PlayerComponent for this object and set it in the UI + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + if (playerComp is null) + { + string inputUsername = UsernameElement.Text; + Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); + Reducer.CreatePlayer(inputUsername); + + // Try again, optimistically assuming success for simplicity + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + } + + Username = playerComp.Username; + + // Get the last location for this player and set the initial position + EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); + transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + + // Register for a callback that is called when the client gets an + // update for a row in the EntityComponent table + EntityComponent.OnUpdate += EntityComponent_OnUpdate; + } +} +``` + +We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. + +**Append to bottom of RemotePlayer class in RemotePlayer.cs:** + +```csharp +private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) +{ + // If the update was made to this object + if(obj.EntityId == EntityId) + { + var movementController = GetComponent(); + + // Update target position, rotation, etc. + movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); + movementController.RemoteTargetRotation = obj.Direction; + movementController.SetMoving(obj.Moving); + } +} +``` + +Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. + +**Append to bottom of Start() function in TutorialGameManager.cs:** + +```csharp +PlayerComponent.OnInsert += PlayerComponent_OnInsert; +``` + +Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. + +**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** + +```csharp +private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) +{ + // If the identity of the PlayerComponent matches our user identity then this is the local player + if(obj.OwnerId == local_identity) + { + // Now that we have our initial position we can start the game + StartGame(); + } + else + { + // Spawn the player object and attach the RemotePlayer component + var remotePlayer = Instantiate(PlayerPrefab); + + // Lookup and apply the position for this new player + var entity = EntityComponent.FilterByEntityId(obj.EntityId); + var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + remotePlayer.transform.position = position; + + var movementController = remotePlayer.GetComponent(); + movementController.RemoteTargetPosition = position; + movementController.RemoteTargetRotation = entity.Direction; + + remotePlayer.AddComponent().EntityId = obj.EntityId; + } +} ``` -Here is some sample output: +Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. -```plaintext -$ spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang cs -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - Finished release [optimized] target(s) in 0.03s -compilation took 234.613518ms -Generate finished successfully. +**Append to the top of LocalPlayer.cs** + +```csharp +using SpacetimeDB.Types; +using SpacetimeDB; ``` -If you've gotten this message then everything should be working properly so far. +**Append to the bottom of LocalPlayer class in LocalPlayer.cs** + +```csharp +private float? lastUpdateTime; +private void FixedUpdate() +{ + float? deltaTime = Time.time - lastUpdateTime; + bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; + bool isConnected = SpacetimeDBClient.instance.IsConnected(); + + if (hasUpdatedRecently || !isConnected) + { + return; + } + + lastUpdateTime = Time.time; + var p = PlayerMovementController.Local.GetModelPosition(); + + Reducer.UpdatePlayerPosition(new StdbVector3 + { + X = p.x, + Y = p.y, + Z = p.z, + }, + PlayerMovementController.Local.GetModelRotation(), + PlayerMovementController.Local.IsMoving()); +} +``` -## 3. Replace address in BitCraftMiniGameManager +Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. -The following settings are exposed in the `BitCraftMiniGameManager` inspector: Module Address, Host Name, and SSL Enabled. +![GameManager-Inspector2](/images/unity-tutorial/GameManager-Inspector2.JPG) -Open the Main scene in Unity and click on the `GameManager` object in the heirarchy. The inspector window will look like this: +### Play the Game! -![GameManager-Inspector](/images/unity-tutorial/GameManager-Inspector.JPG) +Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. -Update the module address with the address you got from the `spacetime publish` command. If you are using SpacetimeDB Cloud `testnet`, the host name should be `testnet.spacetimedb.com` and SSL Enabled should be checked. If you are running SpacetimeDB Standalone locally, the host name should be `localhost:3000` and SSL Enabled should be unchecked. For instructions on how to deploy to these environments, see the [Deployment Section](/docs/deploying/testnet) +![Unity-AddOpenScenes](/images/unity-tutorial/Unity-AddOpenScenes.JPG) -## 4. Play Mode +When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. -You should now be able to enter play mode and walk around! You can mine some rocks, cut down some trees and if you connect more clients you can trade with other players. +### Implement Player Logout -## 5. Editing the Module +So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. -If you want to make further updates to the module, make sure to use this publish command instead: +**Append to the bottom of Start() function in TutorialGameManager.cs** +```csharp +PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; +``` -```bash -spacetime publish +We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. + +Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. + +**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** +```csharp +private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) +{ + OnPlayerComponentChanged(newValue); +} + +private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) +{ + OnPlayerComponentChanged(obj); +} + +private void OnPlayerComponentChanged(PlayerComponent obj) +{ + // If the identity of the PlayerComponent matches our user identity then this is the local player + if(obj.OwnerId == local_identity) + { + // Now that we have our initial position we can start the game + StartGame(); + } + else + { + // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it + var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); + if (obj.LoggedIn) + { + // Only spawn remote players who aren't already spawned + if (existingPlayer == null) + { + // Spawn the player object and attach the RemotePlayer component + var remotePlayer = Instantiate(PlayerPrefab); + + // Lookup and apply the position for this new player + var entity = EntityComponent.FilterByEntityId(obj.EntityId); + var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + remotePlayer.transform.position = position; + + var movementController = remotePlayer.GetComponent(); + movementController.RemoteTargetPosition = position; + movementController.RemoteTargetRotation = entity.Direction; + + remotePlayer.AddComponent().EntityId = obj.EntityId; + } + } + else + { + if (existingPlayer != null) + { + Destroy(existingPlayer.gameObject); + } + } + } +} ``` -Where `` is your own address. If you do this instead then you won't have to change the address inside of `BitCraftMiniGameManager.cs` +Now you when you play the game you should see remote players disappear when they log out. -When you change the server module you should also regenerate the client files as well: +Before updating the client, let's generate the client files and update publish our module. +**Execute commands in the server/ directory** ```bash -spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs +spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp +spacetime publish -c unity-tutorial +``` + +On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. + +**Append to the top of UIChatController.cs:** +```csharp +using SpacetimeDB.Types; +``` + +**REPLACE the OnChatButtonPress function in UIChatController.cs:** + +```csharp +public void OnChatButtonPress() +{ + Reducer.SendChatMessage(_chatInput.text); + _chatInput.text = ""; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: + +**Append to the bottom of the Start() function in TutorialGameManager.cs:** +```csharp +Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; ``` -You may want to consider putting these 2 commands into a simple shell script to make the process a bit cleaner. +Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. + +**Append after the Start() function in TutorialGameManager.cs** +```csharp +private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) +{ + var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); + if (player != null) + { + UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); + } +} +``` + +Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. + +## Conclusion + +This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: + +**Next Unity Tutorial:** [Resources & Scheduling](/docs/unity/part-4.md) + +--- + +### Troubleshooting + +- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` + +- If you get this exception when running the project: + +``` +NullReferenceException: Object reference not set to an instance of an object +TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) +``` + +Check to see if your GameManager object in the Scene has the NetworkManager component attached. + +- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. + +``` +Connection error: Unable to connect to the remote server +``` diff --git a/docs/unity/part-2.md b/docs/unity/part-4.md similarity index 97% rename from docs/unity/part-2.md rename to docs/unity/part-4.md index 537edd44..a87f27a2 100644 --- a/docs/unity/part-2.md +++ b/docs/unity/part-4.md @@ -1,4 +1,8 @@ -# Part 2 - Resources and Scheduling +# Unity Tutorial - Advanced - Part 4 - Resources and Scheduling + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 3](/docs/unity/part-3.md) Tutorial. **Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** diff --git a/docs/unity/part-5.md b/docs/unity/part-5.md new file mode 100644 index 00000000..6ebce1c0 --- /dev/null +++ b/docs/unity/part-5.md @@ -0,0 +1,108 @@ +# Unity Tutorial - Advanced - Part 5 - BitCraft Mini + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 4](/docs/unity/part-3.md) Tutorial. + +**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** + +BitCraft Mini is a game that we developed which extends the code you've already developed in this tutorial. It is inspired by our game [BitCraft](https://bitcraftonline.com) and illustrates how you could build a more complex game from just the components we've discussed. Right now you can walk around, mine ore, and manage your inventory. + +## 1. Download + +You can git-clone BitCraftMini from here: + +```plaintext +git clone ssh://git@github.com/clockworklabs/BitCraftMini +``` + +Once you have downloaded BitCraftMini, you will need to compile the spacetime module. + +## 2. Compile the Spacetime Module + +In order to compile the BitCraftMini module, you will need to install cargo. You can install cargo from here: + +> https://www.rust-lang.org/tools/install + +Once you have cargo installed, you can compile and publish the module with these commands: + +```bash +cd BitCraftMini/Server +spacetime publish +``` + +`spacetime publish` will output an address where your module has been deployed to. You will want to copy/save this address because you will need it in step 3. Here is an example of what it should look like: + +```plaintext +$ spacetime publish +info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date + Finished release [optimized] target(s) in 0.03s +Publish finished successfully. +Created new database with address: c91c17ecdcea8a05302be2bad9dd59b3 +``` + +Optionally, you can specify a name when you publish the module: + +```bash +spacetime publish "unique-module-name" +``` + +Currently, all the named modules exist in the same namespace so if you get a message saying that database is not owned by you, it means that someone else has already published a module with that name. You can either choose a different name or you can use the address instead. If you specify a name when you publish, you can use that name in place of the autogenerated address in both the CLI and in the Unity client. + +In the BitCraftMini module we have a function called `initialize()`. This function should be called immediately after publishing the module to spacetimedb. This function is in charge of generating some initial settings that are required for the server to operate. You can call this function like so: + +```bash +spacetime call "" "initialize" "[]" +``` + +Here we are telling spacetime to invoke the `initialize()` function on our module "bitcraftmini". If the function had some arguments, we would json encode them and put them into the "[]". Since `initialize()` requires no parameters, we just leave it empty. + +After you have called `initialize()` on the spacetime module you shouldgenerate the client files: + +```bash +spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs +``` + +Here is some sample output: + +```plaintext +$ spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang cs +info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date + Finished release [optimized] target(s) in 0.03s +compilation took 234.613518ms +Generate finished successfully. +``` + +If you've gotten this message then everything should be working properly so far. + +## 3. Replace address in BitCraftMiniGameManager + +The following settings are exposed in the `BitCraftMiniGameManager` inspector: Module Address, Host Name, and SSL Enabled. + +Open the Main scene in Unity and click on the `GameManager` object in the heirarchy. The inspector window will look like this: + +![GameManager-Inspector](/images/unity-tutorial/GameManager-Inspector.JPG) + +Update the module address with the address you got from the `spacetime publish` command. If you are using SpacetimeDB Cloud `testnet`, the host name should be `testnet.spacetimedb.com` and SSL Enabled should be checked. If you are running SpacetimeDB Standalone locally, the host name should be `localhost:3000` and SSL Enabled should be unchecked. For instructions on how to deploy to these environments, see the [Deployment Section](/docs/deploying/testnet) + +## 4. Play Mode + +You should now be able to enter play mode and walk around! You can mine some rocks, cut down some trees and if you connect more clients you can trade with other players. + +## 5. Editing the Module + +If you want to make further updates to the module, make sure to use this publish command instead: + +```bash +spacetime publish +``` + +Where `` is your own address. If you do this instead then you won't have to change the address inside of `BitCraftMiniGameManager.cs` + +When you change the server module you should also regenerate the client files as well: + +```bash +spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs +``` + +You may want to consider putting these 2 commands into a simple shell script to make the process a bit cleaner. diff --git a/nav.ts b/nav.ts index 6d5a304b..8f463ad7 100644 --- a/nav.ts +++ b/nav.ts @@ -25,16 +25,22 @@ function section(title: string): NavSection { const nav: Nav = { items: [ section("Intro"), - page("Overview", "index", "index.md"), + 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"), - page("Part 1 - Basic Multiplayer", "unity/part-1", "unity/part-1.md"), - page("Part 2 - Resources And Scheduling", "unity/part-2", "unity/part-2.md"), - page("Part 3 - BitCraft Mini", "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-2a-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"), From 752b32818936ea9ea5c817861ff6e0abdac729a0 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Mon, 26 Feb 2024 17:05:29 +0800 Subject: [PATCH 13/24] doc(deploy): Split Deploy section, impr mentions - Deploy section split: - BEFORE: [ Testnet ] - AFTER: [ Overview, Hosted, Self-Hosted ] - In root doc overview, added a name to the publish command (to prevent confusion later) - Added instructions how to add back `local` and `testnet` servers via CLI after repeatedly experiencing a bug that wipes the servers. --- docs/deploying/hosted.md | 74 +++++++++++++++++++++++++++++++++++ docs/deploying/index.md | 48 +++++++++++++++++++++++ docs/deploying/self-hosted.md | 60 ++++++++++++++++++++++++++++ docs/deploying/testnet.md | 34 ---------------- docs/index.md | 10 ++--- docs/nav.js | 19 ++++++--- nav.ts | 34 ++++++++++------ 7 files changed, 220 insertions(+), 59 deletions(-) create mode 100644 docs/deploying/hosted.md create mode 100644 docs/deploying/index.md create mode 100644 docs/deploying/self-hosted.md delete mode 100644 docs/deploying/testnet.md diff --git a/docs/deploying/hosted.md b/docs/deploying/hosted.md new file mode 100644 index 00000000..0898a267 --- /dev/null +++ b/docs/deploying/hosted.md @@ -0,0 +1,74 @@ +# Deploying - Hosted + +This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI. Via CLI, we will then: + +1. Ensure our hosted server named `testnet` exists as the default. +1. Create an `Identity`. +1. `Publish` your app. + +💡 This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI and that you already have `testnet` server added (exists by default). If you accidentally removed `testnet`, add it back via CLI: + +```bash +spacetime server add "https://testnet.spacetimedb.com" testnet +``` + +## SpacetimeDB Cloud (Hosted) Deployment + +Currently, for hosted deployment, only the `testnet` server is available for SpacetimeDB cloud, which is subject to wipes. + +📢 Stay tuned (such as [via Discord](https://discord.com/invite/SpacetimeDB)) for `mainnet` coming soon! + +## Set the Server Default + +To make CLI commands easier so that we don't need to keep specifying `testnet` as the target server, let's set it as default: + +```bash +spacetime server set-default testnet +``` + +## Creating an Identity + +By default, there are no identities created. Let's create a new one via CLI: +```bash +spacetime identity new --name {Nickname} --email {Email} +``` + +💡If you already created an identity but forgot to attach an email, add it via CLI: +```bash +spacetime identity set-email {Email} +``` + +## Create and Publish a Module + +Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: + +```bash +cd ~ +spacetime init --lang rust HelloSpacetimeDB +cd HelloSpacetimeDB +spacetime publish HelloSpacetimeDB +``` + +## Hosted Web Dashboard + +By earlier associating an email with your CLI identity, you can now view your published modules on the web dashboard. For multiple identities, first list them and copy the hash you want to use: + +```bash +spacetime identity list +``` + +1. Open the SpacetimeDB [login page](https://spacetimedb.com/login) using the same email above. +1. Choose your identity from the dropdown menu. + - \[For multiple identities\] `CTRL+F` to highlight the correct identity you copied earlier. +1. Check your email for a validation link. + +You should now be able to see your published modules on the web dashboard! + +--- + +## Summary + +- We ensured the hosted `testnet` server existed, then set it as the default. +- We added an `identity` to bind with our hosted `testnet` server, ensuring it contained both a Nickname and Email. +- We then logged in the web dashboard via an email `one-time password (OTP)` and were then able to view our published apps. +- With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment. diff --git a/docs/deploying/index.md b/docs/deploying/index.md new file mode 100644 index 00000000..723281a9 --- /dev/null +++ b/docs/deploying/index.md @@ -0,0 +1,48 @@ +# Deploying Overview + +SpacetimeDB supports both hosted and self-hosted publishing in multiple ways. Below, we will: + +1. Generally introduce Identities. +1. Generally introduce Servers. +1Choose to proceed with either a [Hosted](/docs/deploying/hosted.md) or [Self-Hosted](/docs/deploying/self-hosted.md) deployment. + +💡 This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI. + +## About Identities + +An `Identity` is a hash attached to a `Nickname` and `Email`, allowing you to manage your app (such as `Publishing` your app). + +Each `Identity` is bound to one, single `Server`: Unlike GitHub, for example, you would require one identity per server. + +By default, there are no identities created. While the next tutorial will go more in-depth, you may create a new one via CLI: +```bash +spacetime identity new --name {Nickname} --email {Email} +``` + +See the verbose [overview identity explanation](https://spacetimedb.com/docs#identities), [API reference](/docs/http/identity.md) or CLI help (below) for further managing `Identities`: +```bash +spacetime identity --help +``` + +## About Servers + +You `publish` your app to a target `Server` database: While we recommend to **host** your SpacetimeDB app with us for simplicity and scalability, you may also **self-host** (such as locally). + +By default, there are already two default servers added ([testnet](/docs/deploying/hosted.md) and [local](/docs/deploying/self-hosted.md)). While the next tutorial will go more in-depth, you may list your servers via CLI: +```bash +spacetime server list +``` + +See the [API reference](/docs/http/database.md) or CLI help (below) for further managing `Servers`: +```bash +spacetime server --help +``` + +--- + +## Deploying via CLI + +Choose a server for your hosting tutorial path to set a server as default, create an identity, and deploy (`publish`) your app: + +1. [testnet](/docs/deploying/hosted.md) (hosted) +2. [local](/docs/deploying/self-hosted.md) (self-hosted) diff --git a/docs/deploying/self-hosted.md b/docs/deploying/self-hosted.md new file mode 100644 index 00000000..2886b5b8 --- /dev/null +++ b/docs/deploying/self-hosted.md @@ -0,0 +1,60 @@ +# Deploying - Self-Hosted + +This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI. Via CLI, we will then: + +1. Ensure our localhost server named `local` exists as the default. +1. Start our localhost server in a separate terminal window. +1. Create an `Identity` with at least a Nickname. +1. `Publish` your app. + +💡 This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI and that you already have `local` server added (exists by default). If you accidentally removed `local`, add it back via CLI with the `--no-fingerprint` flag (since our server is not yet running): + +```bash +spacetime server add "http://127.0.0.1:3000" local --no-fingerprint +``` + +## Set the Server Default + +To make CLI commands easier so that we don't need to keep specifying `local` as the target server, let's set it as default: + +```bash +spacetime server set-default local +``` + +## Start the Local Server + +In a **separate** terminal window, start the local listen server in the foreground: +```bash +spacetime start +``` + +## Creating an Identity + +By default, there are no identities created. Let's create a new one via CLI: +```bash +spacetime identity new --name {Nickname} +``` + +💡We could optionally add `--email {Email}` to the above command, but is currently unnecessary for local deployment since there's no web dashboard. If you already created an identity but forgot to attach a Nickname, add it via CLI to easier identify your modules: +```bash +spacetime identity set-name {Nickname} +``` + +## Create and Publish a Module + +Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: + +```bash +cd ~ +spacetime init --lang rust HelloSpacetimeDB +cd HelloSpacetimeDB +spacetime publish HelloSpacetimeDB +``` + +--- + +## Summary + +- We ensured the self-hosted `local` server existed, then set it as the default. +- We then opened a separate terminal to run the self-hosted `local` server in the foreground. +- We added an `identity` to bind with our self-hosted `local` server set to default, ensuring it contained a Nickname. diff --git a/docs/deploying/testnet.md b/docs/deploying/testnet.md deleted file mode 100644 index ce648043..00000000 --- a/docs/deploying/testnet.md +++ /dev/null @@ -1,34 +0,0 @@ -# SpacetimeDB Cloud Deployment - -The SpacetimeDB Cloud is a managed cloud service that provides developers an easy way to deploy their SpacetimeDB apps to the cloud. - -Currently only the `testnet` is available for SpacetimeDB cloud which is subject to wipes. The `mainnet` will be available soon. - -## Deploy via CLI - -1. [Install](/install) the SpacetimeDB CLI. -1. Configure your CLI to use the SpacetimeDB Cloud. To do this, run the `spacetime server` command: - -```bash -spacetime server add --default "https://testnet.spacetimedb.com" testnet -``` - -## Connecting your Identity to the Web Dashboard - -By associating an email with your CLI identity, you can view your published modules on the web dashboard. - -1. Get your identity using the `spacetime identity list` command. Copy it to your clipboard. -1. Connect your email address to your identity using the `spacetime identity set-email` command: - -```bash -spacetime identity set-email -``` - -1. Open the SpacetimeDB website and log in using your email address. -1. Choose your identity from the dropdown menu. -1. Validate your email address by clicking the link in the email you receive. -1. You should now be able to see your published modules on the web dashboard. - ---- - -With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment. diff --git a/docs/index.md b/docs/index.md index 7a95f4f8..904abeb6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -100,23 +100,21 @@ SpacetimeDB was designed first and foremost as the backend for multiplayer Unity 1. How do I get/install SpacetimeDB? Just install our command line tool and then upload your application to the cloud. -1. How do I create a new database with SpacetimeDB? +4. How do I create a new database with SpacetimeDB? Follow our [Quick Start](/docs/getting-started) guide! -TL;DR in an empty directory: +TL;DR in an empty directory, init and publish a barebones app named HelloWorld. ```bash spacetime init --lang=rust -spacetime publish +spacetime publish HelloWorld ``` 5. How do I create a Unity game with SpacetimeDB? Follow our [Unity Project](/docs/unity-project) guide! -TL;DR in an empty directory: +TL;DR after already initializing and publishing (see FAQ #5), generate the SDK: ```bash -spacetime init --lang=rust -spacetime publish spacetime generate --out-dir --lang=csharp ``` diff --git a/docs/nav.js b/docs/nav.js index cb8d22f1..f013f783 100644 --- a/docs/nav.js +++ b/docs/nav.js @@ -9,14 +9,21 @@ function section(title) { const nav = { items: [ section("Intro"), - page("Overview", "index", "index.md"), + 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"), - page("Part 1 - Basic Multiplayer", "unity/part-1", "unity/part-1.md"), - page("Part 2 - Resources And Scheduling", "unity/part-2", "unity/part-2.md"), - page("Part 3 - BitCraft Mini", "unity/part-3", "unity/part-3.md"), + page("Overview", "deploying", "deploying/index.md"), + page("Hosted", "deploying/hosted", "deploying/hosted.md"), + page("Self-Hosted", "deploying/hosted", "deploying/self-hosted.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-2a-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"), diff --git a/nav.ts b/nav.ts index 6d5a304b..b7af65ad 100644 --- a/nav.ts +++ b/nav.ts @@ -25,16 +25,24 @@ function section(title: string): NavSection { const nav: Nav = { items: [ section("Intro"), - page("Overview", "index", "index.md"), - page("Getting Started", "getting-started", "getting-started.md"), + 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"), + page("Overview", "deploying", "deploying/index.md"), + page("Hosted", "deploying/hosted", "deploying/hosted.md"), + page("Self-Hosted", "deploying/hosted", "deploying/self-hosted.md"), - section("Unity Tutorial"), - page("Part 1 - Basic Multiplayer", "unity/part-1", "unity/part-1.md"), - page("Part 2 - Resources And Scheduling", "unity/part-2", "unity/part-2.md"), - page("Part 3 - BitCraft Mini", "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-2a-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"), @@ -44,7 +52,7 @@ const nav: Nav = { page("C# Reference", "modules/c-sharp", "modules/c-sharp/index.md"), section("Client SDK Languages"), - page("Overview", "sdks", "sdks/index.md"), + 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"), @@ -55,7 +63,7 @@ const nav: Nav = { page("C# Reference", "sdks/c-sharp", "sdks/c-sharp/index.md"), section("WebAssembly ABI"), - page("Module ABI Reference", "webassembly-abi", "webassembly-abi/index.md"), + page("Module ABI Reference", "webassembly-abi", "webassembly-abi/index.md"), section("HTTP API"), page("HTTP", "http", "http/index.md"), @@ -64,14 +72,14 @@ const nav: Nav = { page("`/energy`", "http/energy", "http/energy.md"), section("WebSocket API Reference"), - page("WebSocket", "ws", "ws/index.md"), + page("WebSocket", "ws", "ws/index.md"), section("Data Format"), - page("SATN", "satn", "satn.md"), - page("BSATN", "bsatn", "bsatn.md"), + page("SATN", "satn", "satn.md"), + page("BSATN", "bsatn", "bsatn.md"), section("SQL"), - page("SQL Reference", "sql", "sql/index.md"), + page("SQL Reference", "sql", "sql/index.md"), ], }; From 5b055a6477c7b36393c4dad0fbcef9cb2d6900a1 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Mon, 25 Mar 2024 18:01:57 +0800 Subject: [PATCH 14/24] doc: Homeless, part-1 revamp, wip pt2 --- docs/unity/homeless.md | 13 ++++ docs/unity/part-1.md | 130 +++++++++------------------------- docs/unity/part-2b-c-sharp.md | 14 ++++ 3 files changed, 60 insertions(+), 97 deletions(-) create mode 100644 docs/unity/homeless.md diff --git a/docs/unity/homeless.md b/docs/unity/homeless.md new file mode 100644 index 00000000..cba27d87 --- /dev/null +++ b/docs/unity/homeless.md @@ -0,0 +1,13 @@ +### Create the Module + +1. It is important that you already have the SpacetimeDB CLI tool [installed](/install). + +2. Run SpacetimeDB locally using the installed CLI. In a **new** terminal or command window, run the following command: + +```bash +spacetime start +``` + +💡 Standalone mode will run in the foreground. +💡 Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md). + diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index b8b8c3c0..9b76fce8 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -1,122 +1,58 @@ -# Unity Tutorial - Basic Multiplayer - Part 1 - Setup +# Unity Multiplayer Tutorial -![UnityTutorial-HeroImage](/images/unity-tutorial/UnityTutorial-HeroImage.JPG) +## Part 1 of 3: Setup -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! +This tutorial will guide you through setting up a multiplayer game project using Unity and SpacetimeDB. We will start by cloning the project, connecting it to SpacetimeDB and running the project. -## Prepare Project Structure +💡 Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! -This project is separated into two sub-projects; +> [!IMPORTANT] +> TODO: This draft may link to WIP repos or docs - be sure to replace with final links after prerequisite PRs are approved (that are not yet approved upon writing this) -1. Server (module) code -2. Client code - -First, we'll create a project root directory (you can choose the name): +## 1. Clone the Project +Let's name it `SpacetimeDBUnityTutorial` for reference: ```bash -mkdir SpacetimeDBUnityTutorial -cd SpacetimeDBUnityTutorial +git clone https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade SpacetimeDBUnityTutorial ``` -We'll start by populating the client directory. - -## Setting up the Tutorial Unity Project - -In this section, we will guide you through the process of setting up a Unity Project that will serve as the starting point for our tutorial. By the end of this section, you will have a basic Unity project and be ready to implement the server functionality. - -### Step 1: Create a Blank Unity Project - -Open Unity and create a new project by selecting "New" from the Unity Hub or going to **File -> New Project**. - -![UnityHub-NewProject](/images/unity-tutorial/UnityHub-NewProject.JPG) - -**⚠️ Important: Ensure `3D (URP)` is selected** to properly render the materials in the scene! - -For Project Name use `client`. For Project Location make sure that you use your `SpacetimeDBUnityTutorial` directory. This is the directory that we created in a previous step. - -![UnityHub-3DURP](/images/unity-tutorial/UnityHub-3DURP.JPG) - -Click "Create" to generate the blank project. - -### Step 2: Adding Required Packages - -To work with SpacetimeDB and ensure compatibility, we need to add some essential packages to our Unity project. Follow these steps: - -1. Open the Unity Package Manager by going to **Window -> Package Manager**. -2. In the Package Manager window, select the "Unity Registry" tab to view unity packages. -3. Search for and install the following package: - - **Input System**: Enables the use of Unity's new Input system used by this project. - -![PackageManager-InputSystem](/images/unity-tutorial/PackageManager-InputSystem.JPG) - -4. You may need to restart the Unity Editor to switch to the new Input system. - -![PackageManager-Restart](/images/unity-tutorial/PackageManager-Restart.JPG) +This project repo is separated into two sub-projects: -### Step 3: Importing the Tutorial Package +1. [Server](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp) (STDB Module) +1. [Client](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Client) (Unity project) -In this step, we will import the provided Unity tutorial package that contains the basic single-player game setup. Follow these instructions: +> [!TIP] +> You may optionally _update_ the [SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk) via the Package Manager in Unity -1. Download the tutorial package from the releases page on GitHub: [https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest) -2. In Unity, go to **Assets -> Import Package -> Custom Package**. +## 2. Publishing the Project -![Unity-ImportCustomPackageB](/images/unity-tutorial/Unity-ImportCustomPackageB.JPG) +From Unity, you don't need CLI commands for common functionality: -3. Browse and select the downloaded tutorial package file. -4. Unity will prompt you with an import settings dialog. Ensure that all the files are selected and click "Import" to import the package into your project. -5. At this point in the project, you shouldn't have any errors. +1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) +1. Create an identity -> Select `testnet` for the server +1. Browse to your repo root `Server-Csharp` dir -> **Publish** -> **Generate** Unity files -![Unity-ImportCustomPackage2](/images/unity-tutorial/Unity-ImportCustomPackage2.JPG) +💡For the next section, we'll use the selected `Server` and publish result `Host` -### Step 4: Running the Project +![Unity Publisher Tool](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-publisher.jpg) -Now that we have everything set up, let's run the project and see it in action: +## 3. Connecting the Project -1. Open the scene named "Main" in the Scenes folder provided in the project hierarchy by double-clicking it. +1. Open `Scenes/Main` in Unity -> select the `GameManager` GameObject in the inspector. +1. Matching the earlier Publish setup: + 1. For the GameManager `Db Name or Address`, input `testnet` + 1. For the GameManager `Host`, input `https://testnet.spacetimedb.com +1. Save your scene -![Unity-OpenSceneMain](/images/unity-tutorial/Unity-OpenSceneMain.JPG) - -**NOTE:** When you open the scene you may get a message saying you need to import TMP Essentials. When it appears, click the "Import TMP Essentials" button. - -🧹 Clear any false-positive TMPro errors that may show. - -![Unity Import TMP Essentials](/images/unity-tutorial/Unity-ImportTMPEssentials.JPG) - -2. Press the **Play** button located at the top of the Unity Editor. - -![Unity-Play](/images/unity-tutorial/Unity-Play.JPG) - -3. Enter any name and click "Continue" - -4. You should see a character loaded in the scene, and you can use the keyboard or mouse controls to move the character around. - -Congratulations! You have successfully set up the basic single-player game project. In the next section, we will start integrating SpacetimeDB functionality to enable multiplayer features. - -## Writing our SpacetimeDB Server Module - -At this point you should have the single player game working. In your CLI, your current working directory should be within your `SpacetimeDBUnityTutorial` directory that we created in a previous step. - -### Create the Module - -1. It is important that you already have the SpacetimeDB CLI tool [installed](/install). - -2. Run SpacetimeDB locally using the installed CLI. In a **new** terminal or command window, run the following command: - -```bash -spacetime start -``` +## 4. Running the Project -💡 Standalone mode will run in the foreground. -💡 Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md). +With the same `Main` scene open, press play! -### The Entity Component Systems (ECS) +![Gameplay Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-action.jpg) -Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). +![UI Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-ui.jpg) -We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. +You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. -### Create the Server Module +Congratulations! You have successfully set up your multiplayer game project. In the next section, we will start integrating SpacetimeDB functionality to enable multiplayer features. -From here, the tutorial continues with your favorite server module language of choice: - - [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 faafe86c..07d45e7a 100644 --- a/docs/unity/part-2b-c-sharp.md +++ b/docs/unity/part-2b-c-sharp.md @@ -1,3 +1,17 @@ +# Unity Multiplayer Tutorial + +## Part 2 of 3: Inspecting the C# Server Module + +In this part of the tutorial, we will create a SpacetimeDB (STDB) server module using C# for the Unity multiplayer game. The server module will handle the game logic and data management for the game. + +💡 Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +## The Entity Component Systems (ECS) + +Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). + +We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. + # Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (C#) Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! From e705f0532b84e4e1f029074e33a1a9ecf000ec39 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Tue, 26 Mar 2024 18:07:07 +0800 Subject: [PATCH 15/24] doc!: Part 2 revamp, with part1 tweaks - Some good stuff moved to 'homeless', for now, to possibly be used later in an advanced section --- Writerside/c.list | 6 + Writerside/topics/bsatn.md | 115 ++ .../topics/deploying/deploying_index.md | 48 + Writerside/topics/deploying/hosted.md | 74 ++ Writerside/topics/deploying/self-hosted.md | 60 + Writerside/topics/getting-started.md | 34 + Writerside/topics/http/database.md | 589 ++++++++ Writerside/topics/http/energy.md | 76 ++ Writerside/topics/http/http_index.md | 51 + Writerside/topics/http/identity.md | 160 +++ Writerside/topics/index.md | 120 ++ .../topics/modules/c-sharp/c-sharp_index.md | 307 +++++ .../topics/modules/c-sharp/quickstart.md | 312 +++++ Writerside/topics/modules/modules_index.md | 30 + Writerside/topics/modules/rust/rust_index.md | 454 +++++++ .../topics/modules/rust/rust_quickstart.md | 272 ++++ Writerside/topics/satn.md | 163 +++ .../topics/sdks/c-sharp/c-sharp_quickstart.md | 438 ++++++ .../topics/sdks/c-sharp/sdks_c-sharp_index.md | 959 +++++++++++++ Writerside/topics/sdks/python/python_index.md | 552 ++++++++ .../topics/sdks/python/python_quickstart.md | 379 ++++++ .../topics/sdks/rust/sdks_rust_index.md | 1183 +++++++++++++++++ .../topics/sdks/rust/sdks_rust_quickstart.md | 487 +++++++ Writerside/topics/sdks/sdks_index.md | 74 ++ .../sdks/typescript/typescript_index.md | 942 +++++++++++++ .../sdks/typescript/typescript_quickstart.md | 502 +++++++ Writerside/topics/sql/sql_index.md | 407 ++++++ .../topics/unity/homeless.md | 31 +- Writerside/topics/unity/part-1.md | 57 + Writerside/topics/unity/part-2.md | 483 +++++++ Writerside/topics/unity/part-2a-rust.md | 316 +++++ Writerside/topics/unity/part-3.md | 479 +++++++ Writerside/topics/unity/part-4.md | 261 ++++ Writerside/topics/unity/part-5.md | 108 ++ Writerside/topics/unity/unity_index.md | 24 + .../webassembly-abi/webassembly-abi_index.md | 499 +++++++ Writerside/topics/ws/ws_index.md | 318 +++++ Writerside/v.list | 5 + Writerside/writerside.cfg | 7 + Writerside2/c.list | 6 + Writerside2/cfg/buildprofiles.xml | 13 + Writerside2/s.tree | 72 + Writerside2/topics/bsatn.md | 115 ++ .../topics/deploying/deploying_index.md | 48 + Writerside2/topics/deploying/hosted.md | 74 ++ Writerside2/topics/deploying/self-hosted.md | 60 + Writerside2/topics/getting-started.md | 34 + Writerside2/topics/http/database.md | 589 ++++++++ Writerside2/topics/http/energy.md | 76 ++ Writerside2/topics/http/http_index.md | 51 + Writerside2/topics/http/identity.md | 160 +++ Writerside2/topics/index.md | 120 ++ .../topics/modules/c-sharp/c-sharp_index.md | 307 +++++ .../topics/modules/c-sharp/quickstart.md | 312 +++++ Writerside2/topics/modules/modules_index.md | 30 + Writerside2/topics/modules/rust/rust_index.md | 454 +++++++ .../topics/modules/rust/rust_quickstart.md | 272 ++++ Writerside2/topics/satn.md | 163 +++ .../topics/sdks/c-sharp/c-sharp_quickstart.md | 438 ++++++ .../topics/sdks/c-sharp/sdks_c-sharp_index.md | 959 +++++++++++++ .../topics/sdks/python/python_index.md | 552 ++++++++ .../topics/sdks/python/python_quickstart.md | 379 ++++++ .../topics/sdks/rust/sdks_rust_index.md | 1183 +++++++++++++++++ .../topics/sdks/rust/sdks_rust_quickstart.md | 487 +++++++ Writerside2/topics/sdks/sdks_index.md | 74 ++ .../sdks/typescript/typescript_index.md | 942 +++++++++++++ .../sdks/typescript/typescript_quickstart.md | 502 +++++++ Writerside2/topics/sql/sql_index.md | 407 ++++++ Writerside2/topics/unity/homeless.md | 355 +++++ Writerside2/topics/unity/part-1.md | 57 + Writerside2/topics/unity/part-2.md | 489 +++++++ Writerside2/topics/unity/part-2a-rust.md | 316 +++++ Writerside2/topics/unity/part-3.md | 479 +++++++ Writerside2/topics/unity/part-4.md | 261 ++++ Writerside2/topics/unity/part-5.md | 108 ++ Writerside2/topics/unity/unity_index.md | 24 + .../webassembly-abi/webassembly-abi_index.md | 499 +++++++ Writerside2/topics/ws/ws_index.md | 318 +++++ Writerside2/v.list | 5 + Writerside2/writerside.cfg | 8 + docs/nav.js | 2 +- docs/unity/homeless.md | 342 +++++ docs/unity/index.md | 13 +- docs/unity/part-1.md | 18 +- docs/unity/part-2.md | 278 ++++ docs/unity/part-3.md | 2 +- nav.ts | 8 +- 87 files changed, 23806 insertions(+), 37 deletions(-) create mode 100644 Writerside/c.list create mode 100644 Writerside/topics/bsatn.md create mode 100644 Writerside/topics/deploying/deploying_index.md create mode 100644 Writerside/topics/deploying/hosted.md create mode 100644 Writerside/topics/deploying/self-hosted.md create mode 100644 Writerside/topics/getting-started.md create mode 100644 Writerside/topics/http/database.md create mode 100644 Writerside/topics/http/energy.md create mode 100644 Writerside/topics/http/http_index.md create mode 100644 Writerside/topics/http/identity.md create mode 100644 Writerside/topics/index.md create mode 100644 Writerside/topics/modules/c-sharp/c-sharp_index.md create mode 100644 Writerside/topics/modules/c-sharp/quickstart.md create mode 100644 Writerside/topics/modules/modules_index.md create mode 100644 Writerside/topics/modules/rust/rust_index.md create mode 100644 Writerside/topics/modules/rust/rust_quickstart.md create mode 100644 Writerside/topics/satn.md create mode 100644 Writerside/topics/sdks/c-sharp/c-sharp_quickstart.md create mode 100644 Writerside/topics/sdks/c-sharp/sdks_c-sharp_index.md create mode 100644 Writerside/topics/sdks/python/python_index.md create mode 100644 Writerside/topics/sdks/python/python_quickstart.md create mode 100644 Writerside/topics/sdks/rust/sdks_rust_index.md create mode 100644 Writerside/topics/sdks/rust/sdks_rust_quickstart.md create mode 100644 Writerside/topics/sdks/sdks_index.md create mode 100644 Writerside/topics/sdks/typescript/typescript_index.md create mode 100644 Writerside/topics/sdks/typescript/typescript_quickstart.md create mode 100644 Writerside/topics/sql/sql_index.md rename docs/unity/part-2b-c-sharp.md => Writerside/topics/unity/homeless.md (88%) create mode 100644 Writerside/topics/unity/part-1.md create mode 100644 Writerside/topics/unity/part-2.md create mode 100644 Writerside/topics/unity/part-2a-rust.md create mode 100644 Writerside/topics/unity/part-3.md create mode 100644 Writerside/topics/unity/part-4.md create mode 100644 Writerside/topics/unity/part-5.md create mode 100644 Writerside/topics/unity/unity_index.md create mode 100644 Writerside/topics/webassembly-abi/webassembly-abi_index.md create mode 100644 Writerside/topics/ws/ws_index.md create mode 100644 Writerside/v.list create mode 100644 Writerside/writerside.cfg create mode 100644 Writerside2/c.list create mode 100644 Writerside2/cfg/buildprofiles.xml create mode 100644 Writerside2/s.tree create mode 100644 Writerside2/topics/bsatn.md create mode 100644 Writerside2/topics/deploying/deploying_index.md create mode 100644 Writerside2/topics/deploying/hosted.md create mode 100644 Writerside2/topics/deploying/self-hosted.md create mode 100644 Writerside2/topics/getting-started.md create mode 100644 Writerside2/topics/http/database.md create mode 100644 Writerside2/topics/http/energy.md create mode 100644 Writerside2/topics/http/http_index.md create mode 100644 Writerside2/topics/http/identity.md create mode 100644 Writerside2/topics/index.md create mode 100644 Writerside2/topics/modules/c-sharp/c-sharp_index.md create mode 100644 Writerside2/topics/modules/c-sharp/quickstart.md create mode 100644 Writerside2/topics/modules/modules_index.md create mode 100644 Writerside2/topics/modules/rust/rust_index.md create mode 100644 Writerside2/topics/modules/rust/rust_quickstart.md create mode 100644 Writerside2/topics/satn.md create mode 100644 Writerside2/topics/sdks/c-sharp/c-sharp_quickstart.md create mode 100644 Writerside2/topics/sdks/c-sharp/sdks_c-sharp_index.md create mode 100644 Writerside2/topics/sdks/python/python_index.md create mode 100644 Writerside2/topics/sdks/python/python_quickstart.md create mode 100644 Writerside2/topics/sdks/rust/sdks_rust_index.md create mode 100644 Writerside2/topics/sdks/rust/sdks_rust_quickstart.md create mode 100644 Writerside2/topics/sdks/sdks_index.md create mode 100644 Writerside2/topics/sdks/typescript/typescript_index.md create mode 100644 Writerside2/topics/sdks/typescript/typescript_quickstart.md create mode 100644 Writerside2/topics/sql/sql_index.md create mode 100644 Writerside2/topics/unity/homeless.md create mode 100644 Writerside2/topics/unity/part-1.md create mode 100644 Writerside2/topics/unity/part-2.md create mode 100644 Writerside2/topics/unity/part-2a-rust.md create mode 100644 Writerside2/topics/unity/part-3.md create mode 100644 Writerside2/topics/unity/part-4.md create mode 100644 Writerside2/topics/unity/part-5.md create mode 100644 Writerside2/topics/unity/unity_index.md create mode 100644 Writerside2/topics/webassembly-abi/webassembly-abi_index.md create mode 100644 Writerside2/topics/ws/ws_index.md create mode 100644 Writerside2/v.list create mode 100644 Writerside2/writerside.cfg create mode 100644 docs/unity/part-2.md diff --git a/Writerside/c.list b/Writerside/c.list new file mode 100644 index 00000000..c4c77a29 --- /dev/null +++ b/Writerside/c.list @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Writerside/topics/bsatn.md b/Writerside/topics/bsatn.md new file mode 100644 index 00000000..f8aeca7f --- /dev/null +++ b/Writerside/topics/bsatn.md @@ -0,0 +1,115 @@ +# SATN Binary Format (BSATN) + +The Spacetime Algebraic Type Notation binary (BSATN) format defines +how Spacetime `AlgebraicValue`s and friends are encoded as byte strings. + +Algebraic values and product values are BSATN-encoded for e.g., +module-host communication and for storing row data in the database. + +## Notes on notation + +In this reference, we give a formal definition of the format. +To do this, we use inductive definitions, and define the following notation: + +- `bsatn(x)` denotes a function converting some value `x` to a list of bytes. +- `a: B` means that `a` is of type `B`. +- `Foo(x)` denotes extracting `x` out of some variant or type `Foo`. +- `a ++ b` denotes concatenating two byte lists `a` and `b`. +- `bsatn(A) = bsatn(B) | ... | bsatn(Z)` where `B` to `Z` are variants of `A` + means that `bsatn(A)` is defined as e.g., + `bsatn(B)`, `bsatn(C)`, .., `bsatn(Z)` depending on what variant of `A` it was. +- `[]` denotes the empty list of bytes. + +## Values + +### At a glance + +| Type | Description | +| ---------------- | ---------------------------------------------------------------- | +| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype.). | +| `SumValue` | A value whose type is a [`SumType`](#sumtype.). | +| `ProductValue` | A value whose type is a [`ProductType`](#producttype.). | +| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype.). | + +### `AlgebraicValue` + +The BSATN encoding of an `AlgebraicValue` defers to the encoding of each variant: + +```fsharp +bsatn(AlgebraicValue) = bsatn(SumValue) | bsatn(ProductValue) | bsatn(BuiltinValue) +``` + +### `SumValue` + +An instance of a [`SumType`](#sumtype.). +`SumValue`s are binary-encoded as `bsatn(tag) ++ bsatn(variant_data)` +where `tag: u8` is an index into the [`SumType.variants`](#sumtype.) +array of the value's [`SumType`](#sumtype.), +and where `variant_data` is the data of the variant. +For variants holding no data, i.e., of some zero sized type, +`bsatn(variant_data) = []`. + +### `ProductValue` + +An instance of a [`ProductType`](#producttype.). +`ProductValue`s are binary encoded as: + +```fsharp +bsatn(elems) = bsatn(elem_0) ++ .. ++ bsatn(elem_n) +``` + +Field names are not encoded. + +### `BuiltinValue` + +An instance of a [`BuiltinType`](#builtintype.). +The BSATN encoding of `BuiltinValue`s defers to the encoding of each variant: + +```fsharp +bsatn(BuiltinValue) + = bsatn(Bool) + | bsatn(U8) | bsatn(U16) | bsatn(U32) | bsatn(U64) | bsatn(U128) + | bsatn(I8) | bsatn(I16) | bsatn(I32) | bsatn(I64) | bsatn(I128) + | bsatn(F32) | bsatn(F64) + | bsatn(String) + | bsatn(Array) + | bsatn(Map) + +bsatn(Bool(b)) = bsatn(b as u8) +bsatn(U8(x)) = [x] +bsatn(U16(x: u16)) = to_little_endian_bytes(x) +bsatn(U32(x: u32)) = to_little_endian_bytes(x) +bsatn(U64(x: u64)) = to_little_endian_bytes(x) +bsatn(U128(x: u128)) = to_little_endian_bytes(x) +bsatn(I8(x: i8)) = to_little_endian_bytes(x) +bsatn(I16(x: i16)) = to_little_endian_bytes(x) +bsatn(I32(x: i32)) = to_little_endian_bytes(x) +bsatn(I64(x: i64)) = to_little_endian_bytes(x) +bsatn(I128(x: i128)) = to_little_endian_bytes(x) +bsatn(F32(x: f32)) = bsatn(f32_to_raw_bits(x)) // lossless conversion +bsatn(F64(x: f64)) = bsatn(f64_to_raw_bits(x)) // lossless conversion +bsatn(String(s)) = bsatn(len(s) as u32) ++ bsatn(bytes(s)) +bsatn(Array(a)) = bsatn(len(a) as u32) + ++ bsatn(normalize(a)_0) ++ .. ++ bsatn(normalize(a)_n) +bsatn(Map(map)) = bsatn(len(m) as u32) + ++ bsatn(key(map_0)) ++ bsatn(value(map_0)) + .. + ++ bsatn(key(map_n)) ++ bsatn(value(map_n)) +``` + +Where + +- `f32_to_raw_bits(x)` is the raw transmute of `x: f32` to `u32` +- `f64_to_raw_bits(x)` is the raw transmute of `x: f64` to `u64` +- `normalize(a)` for `a: ArrayValue` converts `a` to a list of `AlgebraicValue`s +- `key(map_i)` extracts the key of the `i`th entry of `map` +- `value(map_i)` extracts the value of the `i`th entry of `map` + +## Types + +All SATS types are BSATN-encoded by converting them to an `AlgebraicValue`, +then BSATN-encoding that meta-value. + +See [the SATN JSON Format](satn-reference-json-format.) +for more details of the conversion to meta values. +Note that these meta values are converted to BSATN and _not JSON_. diff --git a/Writerside/topics/deploying/deploying_index.md b/Writerside/topics/deploying/deploying_index.md new file mode 100644 index 00000000..658df48d --- /dev/null +++ b/Writerside/topics/deploying/deploying_index.md @@ -0,0 +1,48 @@ +# Deploying Overview + +SpacetimeDB supports both hosted and self-hosted publishing in multiple ways. Below, we will: + +1. Generally introduce Identities. +1. Generally introduce Servers. +1Choose to proceed with either a [Hosted](hosted1.md) or [Self-Hosted](self-hosted1.md) deployment. + +💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. + +## About Identities + +An `Identity` is a hash attached to a `Nickname` and `Email`, allowing you to manage your app (such as `Publishing` your app). + +Each `Identity` is bound to one, single `Server`: Unlike GitHub, for example, you would require one identity per server. + +By default, there are no identities created. While the next tutorial will go more in-depth, you may create a new one via CLI: +```bash +spacetime identity new --name {Nickname} --email {Email} +``` + +See the verbose [overview identity explanation](https://spacetimedb.com/docs#identities), [API reference](identity1.md) or CLI help (below) for further managing `Identities`: +```bash +spacetime identity --help +``` + +## About Servers + +You `publish` your app to a target `Server` database: While we recommend to **host** your SpacetimeDB app with us for simplicity and scalability, you may also **self-host** (such as locally). + +By default, there are already two default servers added ([testnet](hosted1.md) and [local](self-hosted1.md)). While the next tutorial will go more in-depth, you may list your servers via CLI: +```bash +spacetime server list +``` + +See the [API reference](database1.md) or CLI help (below) for further managing `Servers`: +```bash +spacetime server --help +``` + +--- + +## Deploying via CLI + +Choose a server for your hosting tutorial path to set a server as default, create an identity, and deploy (`publish`) your app: + +1. [testnet](hosted1.md) (hosted) +2. [local](self-hosted1.md) (self-hosted) diff --git a/Writerside/topics/deploying/hosted.md b/Writerside/topics/deploying/hosted.md new file mode 100644 index 00000000..187eec4c --- /dev/null +++ b/Writerside/topics/deploying/hosted.md @@ -0,0 +1,74 @@ +# Deploying - Hosted + +This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. Via CLI, we will then: + +1. Ensure our hosted server named `testnet` exists as the default. +1. Create an `Identity`. +1. `Publish` your app. + +💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI and that you already have `testnet` server added (exists by default). If you accidentally removed `testnet`, add it back via CLI: + +```bash +spacetime server add "https://testnet.spacetimedb.com" testnet +``` + +## SpacetimeDB Cloud (Hosted) Deployment + +Currently, for hosted deployment, only the `testnet` server is available for SpacetimeDB cloud, which is subject to wipes. + +📢 Stay tuned (such as [via Discord](https://discord.com/invite/SpacetimeDB)) for `mainnet` coming soon! + +## Set the Server Default + +To make CLI commands easier so that we don't need to keep specifying `testnet` as the target server, let's set it as default: + +```bash +spacetime server set-default testnet +``` + +## Creating an Identity + +By default, there are no identities created. Let's create a new one via CLI: +```bash +spacetime identity new --name {Nickname} --email {Email} +``` + +💡If you already created an identity but forgot to attach an email, add it via CLI: +```bash +spacetime identity set-email {Email} +``` + +## Create and Publish a Module + +Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: + +```bash +cd ~ +spacetime init --lang rust HelloSpacetimeDB +cd HelloSpacetimeDB +spacetime publish HelloSpacetimeDB +``` + +## Hosted Web Dashboard + +By earlier associating an email with your CLI identity, you can now view your published modules on the web dashboard. For multiple identities, first list them and copy the hash you want to use: + +```bash +spacetime identity list +``` + +1. Open the SpacetimeDB [login page](https://spacetimedb.com/login) using the same email above. +1. Choose your identity from the dropdown menu. + - \[For multiple identities\] `CTRL+F` to highlight the correct identity you copied earlier. +1. Check your email for a validation link. + +You should now be able to see your published modules on the web dashboard! + +--- + +## Summary + +- We ensured the hosted `testnet` server existed, then set it as the default. +- We added an `identity` to bind with our hosted `testnet` server, ensuring it contained both a Nickname and Email. +- We then logged in the web dashboard via an email `one-time password (OTP)` and were then able to view our published apps. +- With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment. diff --git a/Writerside/topics/deploying/self-hosted.md b/Writerside/topics/deploying/self-hosted.md new file mode 100644 index 00000000..9c47282f --- /dev/null +++ b/Writerside/topics/deploying/self-hosted.md @@ -0,0 +1,60 @@ +# Deploying - Self-Hosted + +This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. Via CLI, we will then: + +1. Ensure our localhost server named `local` exists as the default. +1. Start our localhost server in a separate terminal window. +1. Create an `Identity` with at least a Nickname. +1. `Publish` your app. + +💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI and that you already have `local` server added (exists by default). If you accidentally removed `local`, add it back via CLI with the `--no-fingerprint` flag (since our server is not yet running): + +```bash +spacetime server add "http://127.0.0.1:3000" local --no-fingerprint +``` + +## Set the Server Default + +To make CLI commands easier so that we don't need to keep specifying `local` as the target server, let's set it as default: + +```bash +spacetime server set-default local +``` + +## Start the Local Server + +In a **separate** terminal window, start the local listen server in the foreground: +```bash +spacetime start +``` + +## Creating an Identity + +By default, there are no identities created. Let's create a new one via CLI: +```bash +spacetime identity new --name {Nickname} +``` + +💡We could optionally add `--email {Email}` to the above command, but is currently unnecessary for local deployment since there's no web dashboard. If you already created an identity but forgot to attach a Nickname, add it via CLI to easier identify your modules: +```bash +spacetime identity set-name {Nickname} +``` + +## Create and Publish a Module + +Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: + +```bash +cd ~ +spacetime init --lang rust HelloSpacetimeDB +cd HelloSpacetimeDB +spacetime publish HelloSpacetimeDB +``` + +--- + +## Summary + +- We ensured the self-hosted `local` server existed, then set it as the default. +- We then opened a separate terminal to run the self-hosted `local` server in the foreground. +- We added an `identity` to bind with our self-hosted `local` server set to default, ensuring it contained a Nickname. diff --git a/Writerside/topics/getting-started.md b/Writerside/topics/getting-started.md new file mode 100644 index 00000000..31e2fc90 --- /dev/null +++ b/Writerside/topics/getting-started.md @@ -0,0 +1,34 @@ +# Getting Started + +To develop SpacetimeDB applications locally, you will need to run the Standalone version of the server. + +1. [Install](install.) the SpacetimeDB CLI (Command Line Interface) +2. Run the start command: + +```bash +spacetime start +``` + +The server listens on port `3000` by default, customized via `--listen-addr`. + +💡 Standalone mode will run in the foreground. +⚠️ SSL is not supported in standalone mode. + +## What's Next? + +You are ready to start developing SpacetimeDB modules. See below for a quickstart guide for both client and server (module) languages/frameworks. + +### Server (Module) + +- [Rust](quickstart.) +- [C#](quickstart1.) + +⚡**Note:** Rust is [roughly 2x faster](https://faun.dev/c/links/faun/c-vs-rust-vs-go-a-performance-benchmarking-in-kubernetes/) than C# + +### Client + +- [Rust](quickstart2.) +- [C# (Standalone)](quickstart3.) +- [C# (Unity)](part-1.) +- [Typescript](quickstart4.) +- [Python](quickstart5.) \ No newline at end of file diff --git a/Writerside/topics/http/database.md b/Writerside/topics/http/database.md new file mode 100644 index 00000000..0e7fbe89 --- /dev/null +++ b/Writerside/topics/http/database.md @@ -0,0 +1,589 @@ +# `/database` HTTP API + +The HTTP endpoints in `/database` allow clients to interact with Spacetime databases in a variety of ways, including retrieving information, creating and deleting databases, invoking reducers and evaluating SQL queries. + +## At a glance + +| Route | Description | +| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| [`/database/dns/:name GET`](#databasednsname-get.) | Look up a database's address by its name. | +| [`/database/reverse_dns/:address GET`](#databasereverse_dnsaddress-get.) | Look up a database's name by its address. | +| [`/database/set_name GET`](#databaseset_name-get.) | Set a database's name, given its address. | +| [`/database/ping GET`](#databaseping-get.) | No-op. Used to determine whether a client can connect. | +| [`/database/register_tld GET`](#databaseregister_tld-get.) | Register a top-level domain. | +| [`/database/request_recovery_code GET`](#databaserequest_recovery_code-get.) | Request a recovery code to the email associated with an identity. | +| [`/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](ws.). | +| [`/database/call/:name_or_address/:reducer POST`](#databasecallname_or_addressreducer-post.) | Invoke a reducer in a database. | +| [`/database/schema/:name_or_address GET`](#databaseschemaname_or_address-get.) | Get the schema for a database. | +| [`/database/schema/:name_or_address/:entity_type/:entity GET`](#databaseschemaname_or_addressentity_typeentity-get.) | Get a schema for a particular table or reducer. | +| [`/database/info/:name_or_address GET`](#databaseinfoname_or_address-get.) | Get a JSON description of a database. | +| [`/database/logs/:name_or_address GET`](#databaselogsname_or_address-get.) | Retrieve logs from a database. | +| [`/database/sql/:name_or_address POST`](#databasesqlname_or_address-post.) | Run a SQL query against a database. | + +## `/database/dns/:name GET` + +Look up a database's address by its name. + +Accessible through the CLI as `spacetime dns lookup `. + +#### Parameters + +| Name | Value | +| ------- | ------------------------- | +| `:name` | The name of the database. | + +#### Returns + +If a database with that name exists, returns JSON in the form: + +```typescript +{ "Success": { + "domain": string, + "address": string +} } +``` + +If no database with that name exists, returns JSON in the form: + +```typescript +{ "Failure": { + "domain": string +} } +``` + +## `/database/reverse_dns/:address GET` + +Look up a database's name by its address. + +Accessible through the CLI as `spacetime dns reverse-lookup
`. + +#### Parameters + +| Name | Value | +| ---------- | ---------------------------- | +| `:address` | The address of the database. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ "names": array } +``` + +where `` is a JSON array of strings, each of which is a name which refers to the database. + +## `/database/set_name GET` + +Set the name associated with a database. + +Accessible through the CLI as `spacetime dns set-name
`. + +#### Query Parameters + +| Name | Value | +| -------------- | ------------------------------------------------------------------------- | +| `address` | The address of the database to be named. | +| `domain` | The name to register. | +| `register_tld` | A boolean; whether to register the name as a TLD. Should usually be true. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +If the name was successfully set, returns JSON in the form: + +```typescript +{ "Success": { + "domain": string, + "address": string +} } +``` + +If the top-level domain is not registered, and `register_tld` was not specified, returns JSON in the form: + +```typescript +{ "TldNotRegistered": { + "domain": string +} } +``` + +If the top-level domain is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form: + +```typescript +{ "PermissionDenied": { + "domain": string +} } +``` + +> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. + +## `/database/ping GET` + +Does nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB. + +## `/database/register_tld GET` + +Register a new Spacetime top-level domain. A TLD is the part of a database name before the first `/`. For example, in the name `tyler/bitcraft`, the TLD is `tyler`. Each top-level domain is owned by at most one identity, and only the owner can publish databases with that TLD. + +> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. + +Accessible through the CLI as `spacetime dns register-tld `. + +#### Query Parameters + +| Name | Value | +| ----- | -------------------------------------- | +| `tld` | New top-level domain name to register. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +If the domain is successfully registered, returns JSON in the form: + +```typescript +{ "Success": { + "domain": string +} } +``` + +If the domain is already registered to the caller, returns JSON in the form: + +```typescript +{ "AlreadyRegistered": { + "domain": string +} } +``` + +If the domain is already registered to another identity, returns JSON in the form: + +```typescript +{ "Unauthorized": { + "domain": string +} } +``` + +## `/database/request_recovery_code GET` + +Request a recovery code or link via email, in order to recover the token associated with an identity. + +Accessible through the CLI as `spacetime identity recover `. + +#### Query Parameters + +| 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`](identity#identity-post.) or afterwards via [`/identity/:identity/set-email`](identity#identityidentityset_email-post.). | +| `link` | A boolean; whether to send a clickable link rather than a recovery code. | + +## `/database/confirm_recovery_code GET` + +Confirm a recovery code received via email following a [`/database/request_recovery_code GET`](#-database-request_recovery_code-get.) request, and retrieve the identity's token. + +Accessible through the CLI as `spacetime identity recover `. + +#### Query Parameters + +| Name | Value | +| ---------- | --------------------------------------------- | +| `identity` | The identity whose token should be recovered. | +| `email` | The email which received the recovery code. | +| `code` | The recovery code received via email. | + +On success, returns JSON in the form: + +```typescript +{ + "identity": string, + "token": string +} +``` + +## `/database/publish POST` + +Publish a database. + +Accessible through the CLI as `spacetime publish`. + +#### Query Parameters + +| Name | Value | +| ----------------- | ------------------------------------------------------------------------------------------------ | +| `host_type` | Optional; a SpacetimeDB module host type. Currently, only `"wasmer"` is supported. | +| `clear` | A boolean; whether to clear any existing data when updating an existing database. | +| `name_or_address` | The name of the database to publish or update, or the address of an existing database to update. | +| `register_tld` | A boolean; whether to register the database's top-level domain. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Data + +A WebAssembly module in the [binary format](https://webassembly.github.io/spec/core/binary/index.html). + +#### Returns + +If the database was successfully published, returns JSON in the form: + +```typescript +{ "Success": { + "domain": null | string, + "address": string, + "op": "created" | "updated" +} } +``` + +If the top-level domain for the requested name is not registered, returns JSON in the form: + +```typescript +{ "TldNotRegistered": { + "domain": string +} } +``` + +If the top-level domain for the requested name is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form: + +```typescript +{ "PermissionDenied": { + "domain": string +} } +``` + +> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. + +## `/database/delete/:address POST` + +Delete a database. + +Accessible through the CLI as `spacetime delete
`. + +#### Parameters + +| Name | Address | +| ---------- | ---------------------------- | +| `:address` | The address of the database. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +## `/database/subscribe/:name_or_address GET` + +Begin a [WebSocket connection](ws.) with a database. + +#### Parameters + +| Name | Value | +| ------------------ | ---------------------------- | +| `:name_or_address` | The address of the database. | + +#### Required Headers + +For more information about WebSocket headers, see [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). + +| Name | Value | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `Sec-WebSocket-Protocol` | [`v1.bin.spacetimedb`](ws#binary-protocol.) or [`v1.text.spacetimedb`](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. | + +#### Optional Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +## `/database/call/:name_or_address/:reducer POST` + +Invoke a reducer in a database. + +#### Parameters + +| Name | Value | +| ------------------ | ------------------------------------ | +| `:name_or_address` | The name or address of the database. | +| `:reducer` | The name of the reducer. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Data + +A JSON array of arguments to the reducer. + +## `/database/schema/:name_or_address GET` + +Get a schema for a database. + +Accessible through the CLI as `spacetime describe `. + +#### Parameters + +| Name | Value | +| ------------------ | ------------------------------------ | +| `:name_or_address` | The name or address of the database. | + +#### Query Parameters + +| Name | Value | +| -------- | ----------------------------------------------------------- | +| `expand` | A boolean; whether to include full schemas for each entity. | + +#### Returns + +Returns a JSON object with two properties, `"entities"` and `"typespace"`. For example, on the default module generated by `spacetime init` with `expand=true`, returns: + +```typescript +{ + "entities": { + "Person": { + "arity": 1, + "schema": { + "elements": [ + { + "algebraic_type": { + "Builtin": { + "String": [] + } + }, + "name": { + "some": "name" + } + } + ] + }, + "type": "table" + }, + "__init__": { + "arity": 0, + "schema": { + "elements": [], + "name": "__init__" + }, + "type": "reducer" + }, + "add": { + "arity": 1, + "schema": { + "elements": [ + { + "algebraic_type": { + "Builtin": { + "String": [] + } + }, + "name": { + "some": "name" + } + } + ], + "name": "add" + }, + "type": "reducer" + }, + "say_hello": { + "arity": 0, + "schema": { + "elements": [], + "name": "say_hello" + }, + "type": "reducer" + } + }, + "typespace": [ + { + "Product": { + "elements": [ + { + "algebraic_type": { + "Builtin": { + "String": [] + } + }, + "name": { + "some": "name" + } + } + ] + } + } + ] +} +``` + +The `"entities"` will be an object whose keys are table and reducer names, and whose values are objects of the form: + +```typescript +{ + "arity": number, + "type": "table" | "reducer", + "schema"?: ProductType +} +``` + +| Entity field | Value | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `arity` | For tables, the number of colums; for reducers, the number of arguments. | +| `type` | For tables, `"table"`; for reducers, `"reducer"`. | +| `schema` | A [JSON-encoded `ProductType`](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](satn.) referenced by the module. This can be used to resolve `Ref` types within the schema; the type `{ "Ref": n }` refers to `response["typespace"][n]`. + +## `/database/schema/:name_or_address/:entity_type/:entity GET` + +Get a schema for a particular table or reducer in a database. + +Accessible through the CLI as `spacetime describe `. + +#### Parameters + +| Name | Value | +| ------------------ | ---------------------------------------------------------------- | +| `:name_or_address` | The name or address of the database. | +| `:entity_type` | `reducer` to describe a reducer, or `table` to describe a table. | +| `:entity` | The name of the reducer or table. | + +#### Query Parameters + +| Name | Value | +| -------- | ------------------------------------------------------------- | +| `expand` | A boolean; whether to include the full schema for the entity. | + +#### Returns + +Returns a single entity in the same format as in the `"entities"` returned by [the `/database/schema/:name_or_address GET` endpoint](#databaseschemaname_or_address-get.): + +```typescript +{ + "arity": number, + "type": "table" | "reducer", + "schema"?: ProductType, +} +``` + +| Field | Value | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `arity` | For tables, the number of colums; for reducers, the number of arguments. | +| `type` | For tables, `"table"`; for reducers, `"reducer"`. | +| `schema` | A [JSON-encoded `ProductType`](satn.); for tables, the table schema; for reducers, the argument schema. Only present if `expand` is supplied and true. | + +## `/database/info/:name_or_address GET` + +Get a database's address, owner identity, host type, number of replicas and a hash of its WASM module. + +#### Parameters + +| Name | Value | +| ------------------ | ------------------------------------ | +| `:name_or_address` | The name or address of the database. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "address": string, + "identity": string, + "host_type": "wasmer", + "num_replicas": number, + "program_bytes_address": string +} +``` + +| Field | Type | Meaning | +| ------------------------- | ------ | ----------------------------------------------------------- | +| `"address"` | String | The address of the database. | +| `"identity"` | String | The Spacetime identity of the database's owner. | +| `"host_type"` | String | The module host type; currently always `"wasmer"`. | +| `"num_replicas"` | Number | The number of replicas of the database. Currently always 1. | +| `"program_bytes_address"` | String | Hash of the WASM module for the database. | + +## `/database/logs/:name_or_address GET` + +Retrieve logs from a database. + +Accessible through the CLI as `spacetime logs `. + +#### Parameters + +| Name | Value | +| ------------------ | ------------------------------------ | +| `:name_or_address` | The name or address of the database. | + +#### Query Parameters + +| Name | Value | +| ----------- | --------------------------------------------------------------- | +| `num_lines` | Number of most-recent log lines to retrieve. | +| `follow` | A boolean; whether to continue receiving new logs via a stream. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +Text, or streaming text if `follow` is supplied, containing log lines. + +## `/database/sql/:name_or_address POST` + +Run a SQL query against a database. + +Accessible through the CLI as `spacetime sql `. + +#### Parameters + +| Name | Value | +| ------------------ | --------------------------------------------- | +| `:name_or_address` | The name or address of the database to query. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Data + +SQL queries, separated by `;`. + +#### Returns + +Returns a JSON array of statement results, each of which takes the form: + +```typescript +{ + "schema": ProductType, + "rows": array +} +``` + +The `schema` will be a [JSON-encoded `ProductType`](satn.) describing the type of the returned rows. + +The `rows` will be an array of [JSON-encoded `ProductValue`s](satn.), each of which conforms to the `schema`. diff --git a/Writerside/topics/http/energy.md b/Writerside/topics/http/energy.md new file mode 100644 index 00000000..fabecc30 --- /dev/null +++ b/Writerside/topics/http/energy.md @@ -0,0 +1,76 @@ +# `/energy` HTTP API + +The HTTP endpoints in `/energy` allow clients to query identities' energy balances. Spacetime databases expend energy from their owners' balances while executing reducers. + +## At a glance + +| Route | Description | +| ------------------------------------------------ | --------------------------------------------------------- | +| [`/energy/:identity GET`](#energyidentity-get.) | Get the remaining energy balance for the user `identity`. | +| [`/energy/:identity POST`](#energyidentity-post.) | Set the energy balance for the user `identity`. | + +## `/energy/:identity GET` + +Get the energy balance of an identity. + +Accessible through the CLI as `spacetime energy status `. + +#### Parameters + +| Name | Value | +| ----------- | ----------------------- | +| `:identity` | The Spacetime identity. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "balance": string +} +``` + +| Field | Value | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `balance` | The identity's energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. | + +## `/energy/:identity POST` + +Set the energy balance for an identity. + +Note that in the SpacetimeDB 0.6 Testnet, this endpoint always returns code 401, `UNAUTHORIZED`. Testnet energy balances cannot be refilled. + +Accessible through the CLI as `spacetime energy set-balance `. + +#### Parameters + +| Name | Value | +| ----------- | ----------------------- | +| `:identity` | The Spacetime identity. | + +#### Query Parameters + +| Name | Value | +| --------- | ------------------------------------------ | +| `balance` | A decimal integer; the new balance to set. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "balance": number +} +``` + +| Field | Value | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `balance` | The identity's new energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. | diff --git a/Writerside/topics/http/http_index.md b/Writerside/topics/http/http_index.md new file mode 100644 index 00000000..e9a3d21e --- /dev/null +++ b/Writerside/topics/http/http_index.md @@ -0,0 +1,51 @@ +# SpacetimeDB HTTP Authorization + +Rather than a password, each Spacetime identity is associated with a private token. These tokens are generated by SpacetimeDB when the corresponding identity is created, and cannot be changed. + +> Do not share your SpacetimeDB token with anyone, ever. + +### Generating identities and tokens + +Clients can request a new identity and token via [the `/identity POST` HTTP endpoint](identity#identity-post.). + +Alternately, a new identity and token will be generated during an anonymous connection via the [WebSocket API](ws.), and passed to the client as [an `IdentityToken` message](ws#identitytoken.). + +### Encoding `Authorization` headers + +Many SpacetimeDB HTTP endpoints either require or optionally accept a token in the `Authorization` header. SpacetimeDB authorization headers use `Basic` authorization with the username `token` and the token as the password. Because Spacetime tokens are not passwords, and SpacetimeDB Cloud uses TLS, usual security concerns about HTTP `Basic` authorization do not apply. + +To construct an appropriate `Authorization` header value for a `token`: + +1. Prepend the string `token:`. +2. Base64-encode. +3. Prepend the string `Basic `. + +#### Python + +```python +def auth_header_value(token): + username_and_password = f"token:{token}".encode("utf-8") + base64_encoded = base64.b64encode(username_and_password).decode("utf-8") + return f"Basic {base64_encoded}" +``` + +#### Rust + +```rust +fn auth_header_value(token: &str) -> String { + let username_and_password = format!("token:{}", token); + let base64_encoded = base64::prelude::BASE64_STANDARD.encode(username_and_password); + format!("Basic {}", encoded) +} +``` + +#### C# + +```csharp +public string AuthHeaderValue(string token) +{ + var username_and_password = Encoding.UTF8.GetBytes($"token:{auth}"); + var base64_encoded = Convert.ToBase64String(username_and_password); + return "Basic " + base64_encoded; +} +``` diff --git a/Writerside/topics/http/identity.md b/Writerside/topics/http/identity.md new file mode 100644 index 00000000..544d5d11 --- /dev/null +++ b/Writerside/topics/http/identity.md @@ -0,0 +1,160 @@ +# `/identity` HTTP API + +The HTTP endpoints in `/identity` allow clients to generate and manage Spacetime public identities and private tokens. + +## At a glance + +| Route | Description | +| ----------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [`/identity GET`](#identity-get.) | Look up an identity by email. | +| [`/identity POST`](#identity-post.) | Generate a new identity and token. | +| [`/identity/websocket_token POST`](#identitywebsocket_token-post.) | Generate a short-lived access token for use in untrusted contexts. | +| [`/identity/:identity/set-email POST`](#identityidentityset-email-post.) | Set the email for an identity. | +| [`/identity/:identity/databases GET`](#identityidentitydatabases-get.) | List databases owned by an identity. | +| [`/identity/:identity/verify GET`](#identityidentityverify-get.) | Verify an identity and token. | + +## `/identity GET` + +Look up Spacetime identities associated with an email. + +Accessible through the CLI as `spacetime identity find `. + +#### Query Parameters + +| Name | Value | +| ------- | ------------------------------- | +| `email` | An email address to search for. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "identities": [ + { + "identity": string, + "email": string + } + ] +} +``` + +The `identities` value is an array of zero or more objects, each of which has an `identity` and an `email`. Each `email` will be the same as the email passed as a query parameter. + +## `/identity POST` + +Create a new identity. + +Accessible through the CLI as `spacetime identity new`. + +#### Query Parameters + +| Name | Value | +| ------- | ----------------------------------------------------------------------------------------------------------------------- | +| `email` | An email address to associate with the new identity. If unsupplied, the new identity will not have an associated email. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "identity": string, + "token": string +} +``` + +## `/identity/websocket_token POST` + +Generate a short-lived access token which can be used in untrusted contexts, e.g. embedded in URLs. + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "token": string +} +``` + +The `token` value is a short-lived [JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519). + +## `/identity/:identity/set-email POST` + +Associate an email with a Spacetime identity. + +Accessible through the CLI as `spacetime identity set-email `. + +#### Parameters + +| Name | Value | +| ----------- | ----------------------------------------- | +| `:identity` | The identity to associate with the email. | + +#### Query Parameters + +| Name | Value | +| ------- | ----------------- | +| `email` | An email address. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +## `/identity/:identity/databases GET` + +List all databases owned by an identity. + +#### Parameters + +| Name | Value | +| ----------- | --------------------- | +| `:identity` | A Spacetime identity. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "addresses": array +} +``` + +The `addresses` value is an array of zero or more strings, each of which is the address of a database owned by the identity passed as a parameter. + +## `/identity/:identity/verify GET` + +Verify the validity of an identity/token pair. + +#### Parameters + +| Name | Value | +| ----------- | ----------------------- | +| `:identity` | The identity to verify. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +Returns no data. + +If the token is valid and matches the identity, returns `204 No Content`. + +If the token is valid but does not match the identity, returns `400 Bad Request`. + +If the token is invalid, or no `Authorization` header is included in the request, returns `401 Unauthorized`. diff --git a/Writerside/topics/index.md b/Writerside/topics/index.md new file mode 100644 index 00000000..8426e256 --- /dev/null +++ b/Writerside/topics/index.md @@ -0,0 +1,120 @@ +# SpacetimeDB Documentation + +## Installation + +You can run SpacetimeDB as a standalone database server via the `spacetime` CLI tool. + +You can find the instructions to install the CLI tool for your platform [here](install.). + + + +To get started running your own standalone instance of SpacetimeDB check out our [Getting Started Guide](getting-started.). + + + +## What is SpacetimeDB? + +You can think of SpacetimeDB as a database that is also a server. + +It is a relational database system that lets you upload your application logic directly into the database by way of very fancy stored procedures called "modules". + +Instead of deploying a web or game server that sits in between your clients and your database, your clients connect directly to the database and execute your application logic inside the database itself. You can write all of your permission and authorization logic right inside your module just as you would in a normal server. + +This means that you can write your entire application in a single language, Rust, and deploy it as a single binary. No more microservices, no more containers, no more Kubernetes, no more Docker, no more VMs, no more DevOps, no more infrastructure, no more ops, no more servers. + +
+ SpacetimeDB Architecture +
+ SpacetimeDB application architecture + (elements in white are provided by SpacetimeDB) +
+
+ +It's actually similar to the idea of smart contracts, except that SpacetimeDB is a database, has nothing to do with blockchain, and it's a lot faster than any smart contract system. + +So fast, in fact, that the entire backend our MMORPG [BitCraft Online](https://bitcraftonline.com) is just a SpacetimeDB module. We don't have any other servers or services running, which means that everything in the game, all of the chat messages, items, resources, terrain, and even the locations of the players are stored and processed by the database before being synchronized out to all of the clients in real-time. + +SpacetimeDB is optimized for maximum speed and minimum latency rather than batch processing or OLAP workloads. It is designed to be used for real-time applications like games, chat, and collaboration tools. + +This speed and latency is achieved by holding all of application state in memory, while persisting the data in a write-ahead-log (WAL) which is used to recover application state. + +## State Synchronization + +SpacetimeDB syncs client and server state for you so that you can just write your application as though you're accessing the database locally. No more messing with sockets for a week before actually writing your game. + +## Identities + +A SpacetimeDB `Identity` is a unique identifier that is used to authenticate and authorize access to the database. Importantly, while it represents who someone is, does NOT represent what they can do. Your application's logic will determine what a given identity is able to do by allowing or disallowing a transaction based on the caller's `Identity` along with any module-defined data and logic. + +SpacetimeDB associates each user with a 256-bit (32-byte) integer `Identity`. These identities are usually formatted as 64-digit hexadecimal strings. Identities are public information, and applications can use them to identify users. Identities are a global resource, so a user can use the same identity with multiple applications, so long as they're hosted by the same SpacetimeDB instance. + +Each identity has a corresponding authentication token. The authentication token is private, and should never be shared with anyone. Specifically, authentication tokens are [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519) signed by a secret unique to the SpacetimeDB instance. + +Additionally, each database has an owner `Identity`. Many database maintenance operations, like publishing a new version or evaluating arbitrary SQL queries, are restricted to only authenticated connections by the owner. + +SpacetimeDB provides tools in the CLI and the [client SDKs](sdks.) for managing credentials. + +## Addresses + +A SpacetimeDB `Address` is an opaque identifier for a database or a client connection. An `Address` is a 128-bit integer, usually formatted as a 32-character (16-byte) hexadecimal string. + +Each SpacetimeDB database has an `Address`, generated by the SpacetimeDB host, which can be used to connect to the database or to request information about it. Databases may also have human-readable names, which are mapped to addresses internally. + +Each client connection has an `Address`. These addresses are opaque, and do not correspond to any metadata about the client. They are notably not IP addresses or device identifiers. A client connection can be uniquely identified by its `(Identity, Address)` pair, but client addresses may not be globally unique; it is possible for multiple connections with the same `Address` but different identities to co-exist. SpacetimeDB modules should treat `Identity` as differentiating users, and `Address` as differentiating connections by the same user. + +## Language Support + +### Server-side Libraries + +Currently, Rust is the best-supported language for writing SpacetimeDB modules. Support for lots of other languages is in the works! + +- [Rust](rust.) - [(Quickstart)](quickstart.) +- [C#](c-sharp.) - [(Quickstart)](quickstart1.) +- Python (Coming soon) +- C# (Coming soon) +- Typescript (Coming soon) +- C++ (Planned) +- Lua (Planned) + +### Client-side SDKs + +- [Rust](rust1.) - [(Quickstart)](quickstart2.) +- [C#](c-sharp1.) - [(Quickstart)](quickstart3.) +- [TypeScript](typescript.) - [(Quickstart)](quickstart4.) +- [Python](python.) - [(Quickstart)](quickstart5.) +- C++ (Planned) +- Lua (Planned) + +### Unity + +SpacetimeDB was designed first and foremost as the backend for multiplayer Unity games. To learn more about using SpacetimeDB with Unity, jump on over to the [SpacetimeDB Unity Tutorial](part-1.). + +## FAQ + +1. What is SpacetimeDB? + It's a whole cloud platform within a database that's fast enough to run real-time games. + +1. How do I use SpacetimeDB? + Install the `spacetime` command line tool, choose your favorite language, import the SpacetimeDB library, write your application, compile it to WebAssembly, and upload it to the SpacetimeDB cloud platform. Once it's uploaded you can call functions directly on your application and subscribe to changes in application state. + +1. How do I get/install SpacetimeDB? + Just install our command line tool and then upload your application to the cloud. + +4. How do I create a new database with SpacetimeDB? + Follow our [Quick Start](getting-started.) guide! + +TL;DR in an empty directory, init and publish a barebones app named HelloWorld. + +```bash +spacetime init --lang=rust +spacetime publish HelloWorld +``` + +5. How do I create a Unity game with SpacetimeDB? + Follow our [Unity Project](unity-project.) guide! + +TL;DR after already initializing and publishing (see FAQ #5), generate the SDK: + +```bash +spacetime generate --out-dir --lang=csharp +``` diff --git a/Writerside/topics/modules/c-sharp/c-sharp_index.md b/Writerside/topics/modules/c-sharp/c-sharp_index.md new file mode 100644 index 00000000..31ebd1d4 --- /dev/null +++ b/Writerside/topics/modules/c-sharp/c-sharp_index.md @@ -0,0 +1,307 @@ +# SpacetimeDB C# Modules + +You can use the [C# SpacetimeDB library](https://github.com/clockworklabs/SpacetimeDBLibCSharp) to write modules in C# which interact with the SpacetimeDB database. + +It uses [Roslyn incremental generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) to add extra static methods to types, tables and reducers marked with special attributes and registers them with the database runtime. + +## Example + +Let's start with a heavily commented version of the default example from the landing page: + +```csharp +// These imports bring into the scope common APIs you'll need to expose items from your module and to interact with the database runtime. +using SpacetimeDB.Module; +using static SpacetimeDB.Runtime; + +// Roslyn generators are statically generating extra code as-if they were part of the source tree, so, +// in order to inject new methods, types they operate on as well as their parents have to be marked as `partial`. +// +// We start with the top-level `Module` class for the module itself. +static partial class Module +{ + // `[SpacetimeDB.Table]` registers a struct or a class as a SpacetimeDB table. + // + // It generates methods to insert, filter, update, and delete rows of the given type in the table. + [SpacetimeDB.Table] + public partial struct Person + { + // `[SpacetimeDB.Column]` allows to specify column attributes / constraints such as + // "this field should be unique" or "this field should get automatically assigned auto-incremented value". + [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)] + public int Id; + public string Name; + public int Age; + } + + // `[SpacetimeDB.Reducer]` marks a static method as a SpacetimeDB reducer. + // + // Reducers are functions that can be invoked from the database runtime. + // They can't return values, but can throw errors that will be caught and reported back to the runtime. + [SpacetimeDB.Reducer] + public static void Add(string name, int age) + { + // We can skip (or explicitly set to zero) auto-incremented fields when creating new rows. + var person = new Person { Name = name, Age = age }; + + // `Insert()` method is auto-generated and will insert the given row into the table. + person.Insert(); + // After insertion, the auto-incremented fields will be populated with their actual values. + // + // `Log()` function is provided by the runtime and will print the message to the database log. + // It should be used instead of `Console.WriteLine()` or similar functions. + Log($"Inserted {person.Name} under #{person.Id}"); + } + + [SpacetimeDB.Reducer] + public static void SayHello() + { + // Each table type gets a static Iter() method that can be used to iterate over the entire table. + foreach (var person in Person.Iter()) + { + Log($"Hello, {person.Name}!"); + } + Log("Hello, World!"); + } +} +``` + +## API reference + +Now we'll get into details on all the APIs SpacetimeDB provides for writing modules in C#. + +### Logging + +First of all, logging as we're likely going to use it a lot for debugging and reporting errors. + +`SpacetimeDB.Runtime` provides a `Log` function that will print the given message to the database log, along with the source location and a log level it was provided. + +Supported log levels are provided by the `LogLevel` enum: + +```csharp +public enum LogLevel +{ + Error, + Warn, + Info, + Debug, + Trace, + Panic +} +``` + +If omitted, the log level will default to `Info`, so these two forms are equivalent: + +```csharp +Log("Hello, World!"); +Log("Hello, World!", LogLevel.Info); +``` + +### Supported types + +#### Built-in types + +The following types are supported out of the box and can be stored in the database tables directly or as part of more complex types: + +- `bool` +- `byte`, `sbyte` +- `short`, `ushort` +- `int`, `uint` +- `long`, `ulong` +- `float`, `double` +- `string` +- [`Int128`](https://learn.microsoft.com/en-us/dotnet/api/system.int128), [`UInt128`](https://learn.microsoft.com/en-us/dotnet/api/system.uint128) +- `T[]` - arrays of supported values. +- [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1) +- [`Dictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2) + +And a couple of special custom types: + +- `SpacetimeDB.SATS.Unit` - semantically equivalent to an empty struct, sometimes useful in generic contexts where C# doesn't permit `void`. +- `Identity` (`SpacetimeDB.Runtime.Identity`) - a unique identifier for each user; internally a byte blob but can be printed, hashed and compared for equality. +- `Address` (`SpacetimeDB.Runtime.Address`) - an identifier which disamgibuates connections by the same `Identity`; internally a byte blob but can be printed, hashed and compared for equality. + + +#### 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. + +Any `struct` or `class` marked with this attribute, as well as their respective parents, must be `partial`, as the code generator will add methods to them. + +```csharp +[SpacetimeDB.Type] +public partial struct Point +{ + public int x; + public int y; +} +``` + +`enum`s marked with this attribute must not use custom discriminants, as the runtime expects them to be always consecutive starting from zero. Unlike structs and classes, they don't use `partial` as C# doesn't allow to add methods to `enum`s. + +```csharp +[SpacetimeDB.Type] +public enum Color +{ + Red, + Green, + Blue, +} +``` + +#### Tagged enums + +SpacetimeDB has support for tagged enums which can be found in languages like Rust, but not C#. + +To bridge the gap, a special marker interface `SpacetimeDB.TaggedEnum` can be used on any `SpacetimeDB.Type`-marked `struct` or `class` to mark it as a SpacetimeDB tagged enum. It accepts a tuple of 2 or more named items and will generate methods to check which variant is currently active, as well as accessors for each variant. + +It is expected that you will use the `Is*` methods to check which variant is active before accessing the corresponding field, as the accessor will throw an exception on a state mismatch. + +```csharp +// Example declaration: +[SpacetimeDB.Type] +partial struct Option : SpacetimeDB.TaggedEnum<(T Some, Unit None)> { } + +// Usage: +var option = new Option { Some = 42 }; +if (option.IsSome) +{ + Log($"Value: {option.Some}"); +} +``` + +### Tables + +`[SpacetimeDB.Table]` attribute can be used on any `struct` or `class` to mark it as a SpacetimeDB table. It will register a table in the database with the given name and fields as well as will generate C# methods to insert, filter, update, and delete rows of the given type. + +It implies `[SpacetimeDB.Type]`, so you must not specify both attributes on the same type. + +```csharp +[SpacetimeDB.Table] +public partial struct Person +{ + [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)] + public int Id; + public string Name; + public int Age; +} +``` + +The example above will generate the following extra methods: + +```csharp +public partial struct Person +{ + // Inserts current instance as a new row into the table. + public void Insert(); + + // Returns an iterator over all rows in the table, e.g.: + // `for (var person in Person.Iter()) { ... }` + public static IEnumerable Iter(); + + // Returns an iterator over all rows in the table that match the given filter, e.g.: + // `for (var person in Person.Query(p => p.Age >= 18)) { ... }` + public static IEnumerable Query(Expression> filter); + + // Generated for each column: + + // Returns an iterator over all rows in the table that have the given value in the `Name` column. + public static IEnumerable FilterByName(string name); + public static IEnumerable FilterByAge(int age); + + // Generated for each unique column: + + // 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); +} +``` + +#### Column attributes + +Attribute `[SpacetimeDB.Column]` can be used on any field of a `SpacetimeDB.Table`-marked `struct` or `class` to customize column attributes as seen above. + +The supported column attributes are: + +- `ColumnAttrs.AutoInc` - this column should be auto-incremented. +- `ColumnAttrs.Unique` - this column should be unique. +- `ColumnAttrs.PrimaryKey` - this column should be a primary key, it implies `ColumnAttrs.Unique` but also allows clients to subscribe to updates via `OnUpdate` which will use this field to match the old and the new version of the row with each other. + +These attributes are bitflags and can be combined together, but you can also use some predefined shortcut aliases: + +- `ColumnAttrs.Identity` - same as `ColumnAttrs.Unique | ColumnAttrs.AutoInc`. +- `ColumnAttrs.PrimaryKeyAuto` - same as `ColumnAttrs.PrimaryKey | ColumnAttrs.AutoInc`. + +### Reducers + +Attribute `[SpacetimeDB.Reducer]` can be used on any `static void` method to register it as a SpacetimeDB reducer. The method must accept only supported types as arguments. If it throws an exception, those will be caught and reported back to the database runtime. + +```csharp +[SpacetimeDB.Reducer] +public static void Add(string name, int age) +{ + var person = new Person { Name = name, Age = age }; + person.Insert(); + Log($"Inserted {person.Name} under #{person.Id}"); +} +``` + +If a reducer has an argument with a type `DbEventArgs` (`SpacetimeDB.Runtime.DbEventArgs`), it will be provided with event details such as the sender identity (`SpacetimeDB.Runtime.Identity`), sender address (`SpacetimeDB.Runtime.Address?`) and the time (`DateTimeOffset`) of the invocation: + +```csharp +[SpacetimeDB.Reducer] +public static void PrintInfo(DbEventArgs e) +{ + Log($"Sender identity: {e.Sender}"); + Log($"Sender address: {e.Address}"); + Log($"Time: {e.Time}"); +} +``` + +`[SpacetimeDB.Reducer]` also generates a function to schedule the given reducer in the future. + +Since it's not possible to generate extension methods on existing methods, the codegen will instead add a `Schedule`-prefixed method colocated in the same namespace as the original method instead. The generated method will accept `DateTimeOffset` argument for the time when the reducer should be invoked, followed by all the arguments of the reducer itself, except those that have type `DbEventArgs`. + +```csharp +// Example reducer: +[SpacetimeDB.Reducer] +public static void Add(string name, int age) { ... } + +// Auto-generated by the codegen: +public static void ScheduleAdd(DateTimeOffset time, string name, int age) { ... } + +// Usage from another reducer: +[SpacetimeDB.Reducer] +public static void AddIn5Minutes(DbEventArgs e, string name, int age) +{ + // Note that we're using `e.Time` instead of `DateTimeOffset.Now` which is not allowed in modules. + var scheduleToken = ScheduleAdd(e.Time.AddMinutes(5), name, age); + + // We can cancel the scheduled reducer by calling `Cancel()` on the returned token. + scheduleToken.Cancel(); +} +``` + +#### Special reducers + +These are two special kinds of reducers that can be used to respond to module lifecycle events. They're stored in the `SpacetimeDB.Module.ReducerKind` class and can be used as an argument to the `[SpacetimeDB.Reducer]` attribute: + +- `ReducerKind.Init` - this reducer will be invoked when the module is first published. +- `ReducerKind.Update` - this reducer will be invoked when the module is updated. +- `ReducerKind.Connect` - this reducer will be invoked when a client connects to the database. +- `ReducerKind.Disconnect` - this reducer will be invoked when a client disconnects from the database. + + +Example: + +```csharp +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void Init() +{ + Log("...and we're live!"); +} +``` diff --git a/Writerside/topics/modules/c-sharp/quickstart.md b/Writerside/topics/modules/c-sharp/quickstart.md new file mode 100644 index 00000000..fedd7851 --- /dev/null +++ b/Writerside/topics/modules/c-sharp/quickstart.md @@ -0,0 +1,312 @@ +# C# Module Quickstart + +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. + +Each SpacetimeDB module defines a set of tables and a set of reducers. + +Each table is defined as a C# `class` annotated with `[SpacetimeDB.Table]`, where an instance represents a row, and each field represents a column. + +A reducer is a function which traverses and updates the database. Each reducer call runs in its own transaction, and its updates to the database are only committed if the reducer returns successfully. In C#, reducers are defined as functions annotated with `[SpacetimeDB.Reducer]`. If an exception is thrown, the reducer call fails, the database is not updated, and a failed message is reported to the client. + +## Install SpacetimeDB + +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. + +## Install .NET 8 + +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 +``` + +.NET 8.0 is the earliest to have the `wasi-experimental` workload that we rely on, but requires manual activation: + +```bash +dotnet workload install wasi-experimental +``` + +## Project structure + +Create and enter a directory `quickstart-chat`: + +```bash +mkdir quickstart-chat +cd quickstart-chat +``` + +Now create `server`, our module, which runs in the database: + +```bash +spacetime init --lang csharp server +``` + +## Declare imports + +`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. +3. Clear it out, so we can write a new module that's still pretty simple: a bare-bones chat server. + +To the top of `server/Lib.cs`, add some imports we'll be using: + +```csharp +using System.Runtime.CompilerServices; +using SpacetimeDB.Module; +using static SpacetimeDB.Runtime; +``` + +- `System.Runtime.CompilerServices` +- `SpacetimeDB.Module` + - Contains the special attributes we'll use to define our module. + - Allows us to use the `ModuleInitializer` attribute, which we'll use to register our `OnConnect` and `OnDisconnect` callbacks. +- `SpacetimeDB.Runtime` contains the raw API bindings SpacetimeDB uses to communicate with the database. + +We also need to create our static module class which all of the module code will live in. In `server/Lib.cs`, add: + +```csharp +static partial class Module +{ +} +``` + +## Define tables + +To get our chat server running, we'll need to store two kinds of data: information about each user, and records of all the messages that have been sent. + +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 +[SpacetimeDB.Table] +public partial class User +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public Identity Identity; + public string? Name; + public bool Online; +} +``` + +For each `Message`, we'll store the `Identity` of the user who sent it, the `Timestamp` when it was sent, and the text of the message. + +In `server/Lib.cs`, add the definition of the table `Message` to the `Module` class: + +```csharp +[SpacetimeDB.Table] +public partial class Message +{ + public Identity Sender; + public long Sent; + public string Text = ""; +} +``` + +## Set users' names + +We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `SetName` which clients can invoke to set their `User.Name`. It will validate the caller's chosen name, using a function `ValidateName` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail. + +Each reducer may accept as its first argument a `DbEventArgs`, 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`, `dbEvent.Sender`. + +It's also possible to call `SetName` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though. + +In `server/Lib.cs`, add to the `Module` class: + +```csharp +[SpacetimeDB.Reducer] +public static void SetName(DbEventArgs dbEvent, string name) +{ + name = ValidateName(name); + + var user = User.FindByIdentity(dbEvent.Sender); + if (user is not null) + { + user.Name = name; + User.UpdateByIdentity(dbEvent.Sender, user); + } +} +``` + +For now, we'll just do a bare minimum of validation, rejecting the empty name. You could extend this in various ways, like: + +- Comparing against a blacklist for moderation purposes. +- Unicode-normalizing names. +- Rejecting names that contain non-printable characters, or removing characters or replacing them with a placeholder. +- Rejecting or truncating long names. +- Rejecting duplicate names. + +In `server/Lib.cs`, add to the `Module` class: + +```csharp +/// Takes a name and checks if it's acceptable as a user's name. +public static string ValidateName(string name) +{ + if (string.IsNullOrEmpty(name)) + { + throw new Exception("Names must not be empty"); + } + return name; +} +``` + +## Send messages + +We define a reducer `SendMessage`, 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 `Time` timestamp taken from the `DbEventArgs`. + +In `server/Lib.cs`, add to the `Module` class: + +```csharp +[SpacetimeDB.Reducer] +public static void SendMessage(DbEventArgs dbEvent, string text) +{ + text = ValidateMessage(text); + Log(text); + new Message + { + Sender = dbEvent.Sender, + Text = text, + Sent = dbEvent.Time.ToUnixTimeMilliseconds(), + }.Insert(); +} +``` + +We'll want to validate messages' texts in much the same way we validate users' chosen names. As above, we'll do the bare minimum, rejecting only empty messages. + +In `server/Lib.cs`, add to the `Module` class: + +```csharp +/// Takes a message's text and checks if it's acceptable to send. +public static string ValidateMessage(string text) +{ + if (string.IsNullOrEmpty(text)) + { + throw new ArgumentException("Messages must not be empty"); + } + return text; +} +``` + +You could extend the validation in `ValidateMessage` in similar ways to `ValidateName`, or add additional checks to `SendMessage`, like: + +- Rejecting messages from senders who haven't set their names. +- Rate-limiting users so they can't send new messages too quickly. + +## Set users' online status + +In C# modules, you can register for `Connect` and `Disconnect` events by using a special `ReducerKind`. We'll use the `Connect` event to create a `User` record for the client if it doesn't yet exist, and to set its online status. + +We'll use `User.FindByIdentity` to look up a `User` row for `dbEvent.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. + +In `server/Lib.cs`, add the definition of the connect reducer to the `Module` class: + +```csharp +[SpacetimeDB.Reducer(ReducerKind.Connect)] +public static void OnConnect(DbEventArgs dbEventArgs) +{ + Log($"Connect {dbEventArgs.Sender}"); + var user = User.FindByIdentity(dbEventArgs.Sender); + + if (user is not null) + { + // If this is a returning user, i.e., we already have a `User` with this `Identity`, + // set `Online: true`, but leave `Name` and `Identity` unchanged. + user.Online = true; + User.UpdateByIdentity(dbEventArgs.Sender, user); + } + else + { + // If this is a new user, create a `User` object for the `Identity`, + // which is online, but hasn't set a name. + new User + { + Name = null, + Identity = dbEventArgs.Sender, + Online = true, + }.Insert(); + } +} +``` + +Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.Disconnect`. We'll use it to un-set the `Online` status of the `User` for the disconnected client. + +Add the following code after the `OnConnect` lambda: + +```csharp +[SpacetimeDB.Reducer(ReducerKind.Disconnect)] +public static void OnDisconnect(DbEventArgs dbEventArgs) +{ + var user = User.FindByIdentity(dbEventArgs.Sender); + + if (user is not null) + { + // This user should exist, so set `Online: false`. + user.Online = false; + User.UpdateByIdentity(dbEventArgs.Sender, user); + } + else + { + // User does not exist, log warning + Log("Warning: No user found for disconnected client."); + } +} +``` + +## Publish the module + +And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more pleasant. Come up with a unique name, and fill it in where we've written ``. + +From the `quickstart-chat` directory, run: + +```bash +spacetime publish --project-path server +``` + +```bash +npm i wasm-opt -g +``` + +## Call Reducers + +You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. + +```bash +spacetime call send_message "Hello, World!" +``` + +Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. + +```bash +spacetime logs +``` + +You should now see the output that your module printed in the database. + +```bash +info: Hello, World! +``` + +## SQL Queries + +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" +``` + +```bash + text +--------- + "Hello, World!" +``` + +## What's next? + +You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](rust-sdk-quickstart-guide.), [C#](csharp-sdk-quickstart-guide.), [TypeScript](typescript-sdk-quickstart-guide.) or [Python](python-sdk-quickstart-guide.). + +If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](part-1.) or check out our example game, [BitcraftMini](part-3.). diff --git a/Writerside/topics/modules/modules_index.md b/Writerside/topics/modules/modules_index.md new file mode 100644 index 00000000..fd1a7e62 --- /dev/null +++ b/Writerside/topics/modules/modules_index.md @@ -0,0 +1,30 @@ +# Server Module Overview + +Server modules are the core of a SpacetimeDB application. They define the structure of the database and the server-side logic that processes and handles client requests. These functions are called reducers and are transactional, meaning they ensure data consistency and integrity. Reducers can perform operations such as inserting, updating, and deleting data in the database. + +In the following sections, we'll cover the basics of server modules and how to create and deploy them. + +## Supported Languages + +### Rust + +As of SpacetimeDB 0.6, Rust is the only fully supported language for server modules. Rust is a great option for server modules because it is fast, safe, and has a small runtime. + +- [Rust Module Reference](rust.) +- [Rust Module Quickstart Guide](quickstart.) + +### C# + +We have C# support available in experimental status. C# can be a good choice for developers who are already using Unity or .net for their client applications. + +- [C# Module Reference](c-sharp.) +- [C# Module Quickstart Guide](quickstart1.) + +### Coming Soon + +We have plans to support additional languages in the future. + +- Python +- Typescript +- C++ +- Lua diff --git a/Writerside/topics/modules/rust/rust_index.md b/Writerside/topics/modules/rust/rust_index.md new file mode 100644 index 00000000..05d62bdc --- /dev/null +++ b/Writerside/topics/modules/rust/rust_index.md @@ -0,0 +1,454 @@ +# SpacetimeDB Rust Modules + +Rust clients of SpacetimeDB use the [Rust SpacetimeDB module library][module library] to write modules which interact with the SpacetimeDB database. + +First, the `spacetimedb` library provides a number of macros for creating tables and Rust `struct`s corresponding to rows in those tables. + +Then the client API allows interacting with the database inside special functions called reducers. + +This guide assumes you are familiar with some basics of Rust. At the very least, you should be familiar with the idea of using attribute macros. An extremely common example is `derive` macros. + +Derive macros look at the type they are attached to and generate some related code. In this example, `#[derive(Debug)]` generates the formatting code needed to print out a `Location` for debugging purposes. + +```rust +#[derive(Debug)] +struct Location { + x: u32, + y: u32, +} +``` + +## SpacetimeDB Macro basics + +Let's start with a highly commented example, straight from the [demo]. This Rust package defines a SpacetimeDB module, with types we can operate on and functions we can run. + +```rust +// In this small example, we have two rust imports: +// |spacetimedb::spacetimedb| is the most important attribute we'll be using. +// |spacetimedb::println| is like regular old |println|, but outputting to the module's logs. +use spacetimedb::{spacetimedb, println}; + +// This macro lets us interact with a SpacetimeDB table of Person rows. +// We can insert and delete into, and query, this table by the collection +// of functions generated by the macro. +#[spacetimedb(table)] +pub struct Person { + name: String, +} + +// This is the other key macro we will be using. A reducer is a +// stored procedure that lives in the database, and which can +// be invoked remotely. +#[spacetimedb(reducer)] +pub fn add(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) +} + +// 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 +// functions, not methods. +#[spacetimedb(reducer)] +pub fn say_hello() { + // 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() { + // Reducers run in a very constrained and sandboxed environment, + // and in particular, can't do most I/O from the Rust standard library. + // We provide an alternative |spacetimedb::println| which is just like + // the std version, excepted it is redirected out to the module's logs. + println!("Hello, {}!", person.name); + } + println!("Hello, World!"); +} + +// Reducers can't return values, but can return errors. To do so, +// the reducer must have a return type of `Result<(), T>`, for any `T` that +// implements `Debug`. Such errors returned from reducers will be formatted and +// printed out to logs. +#[spacetimedb(reducer)] +pub fn add_person(name: String) -> Result<(), String> { + if name.is_empty() { + return Err("Name cannot be empty"); + } + + Person::insert(Person { name }) +} +``` + +## Macro API + +Now we'll get into details on all the macro APIs SpacetimeDB provides, starting with all the variants of the `spacetimedb` attribute. + +### Defining tables + +`#[spacetimedb(table)]` takes no further arguments, and is applied to a Rust struct with named fields: + +```rust +#[spacetimedb(table)] +struct Table { + field1: String, + field2: u32, +} +``` + +This attribute is applied to Rust structs in order to create corresponding tables in SpacetimeDB. Fields of the Rust struct correspond to columns of the database table. + +The fields of the struct have to be types that spacetimedb knows how to encode into the database. This is captured in Rust by the `SpacetimeType` trait. + +This is automatically defined for built in numeric types: + +- `bool` +- `u8`, `u16`, `u32`, `u64`, `u128` +- `i8`, `i16`, `i32`, `i64`, `i128` +- `f32`, `f64` + +And common data structures: + +- `String` and `&str`, utf-8 string data +- `()`, the unit type +- `Option where T: SpacetimeType` +- `Vec where T: SpacetimeType` + +All `#[spacetimedb(table)]` types are `SpacetimeType`s, and accordingly, all of their fields have to be. + +```rust +#[spacetimedb(table)] +struct AnotherTable { + // Fine, some builtin types. + id: u64, + name: Option, + + // Fine, another table type. + table: Table, + + // Fine, another type we explicitly make serializable. + serial: Serial, +} +``` + +If you want to have a field that is not one of the above primitive types, and not a table of its own, you can derive the `SpacetimeType` attribute on it. + +We can derive `SpacetimeType` on `struct`s and `enum`s with members that are themselves `SpacetimeType`s. + +```rust +#[derive(SpacetimeType)] +enum Serial { + Builtin(f64), + Compound { + s: String, + bs: Vec, + } +} +``` + +Once the table is created via the macro, other attributes described below can control more aspects of the table. For instance, a particular column can be indexed, or take on values of an automatically incremented counter. These are described in detail below. + +```rust +#[spacetimedb(table)] +struct Person { + #[unique] + id: u64, + + name: String, + address: String, +} +``` + +### Defining reducers + +`#[spacetimedb(reducer)]` optionally takes a single argument, which is a frequency at which the reducer will be automatically called by the database. + +`#[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>`. + +```rust +#[spacetimedb(reducer)] +fn give_player_item(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::filter_by_item_id(id).ok_or(GameErr::InvalidId)?; + item.owner = Some(player_id); + Item::update_by_id(id, item); + Ok(()) +} + +struct Item { + #[unique] + item_id: u64, + + owner: Option, +} +``` + +Note that reducers can call non-reducer functions, including standard library functions. + +Reducers that are called periodically take an additional macro argument specifying the frequency at which they will be invoked. Durations are parsed according to https://docs.rs/humantime/latest/humantime/fn.parse_duration.html and will usually be a number of milliseconds or seconds. + +Both of these examples are invoked every second. + +```rust +#[spacetimedb(reducer, repeat = 1s)] +fn every_second() {} + +#[spacetimedb(reducer, repeat = 1000ms)] +fn every_thousand_milliseconds() {} +``` + +Finally, reducers can also receive a ReducerContext object, or the Timestamp at which they are invoked, just by taking parameters of those types first. + +```rust +#[spacetimedb(reducer, repeat = 1s)] +fn tick_timestamp(time: Timestamp) { + println!("tick at {time}"); +} + +#[spacetimedb(reducer, repeat = 500ms)] +fn tick_ctx(ctx: ReducerContext) { + println!("tick at {}", ctx.timestamp) +} +``` + +Note that each distinct time a repeating reducer is invoked, a seperate schedule is created for that reducer. So invoking `every_second` three times from the spacetimedb cli will result in the reducer being called times times each second. + +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. + +#[SpacetimeType] + +#[sats] + +## Client API + +Besides the macros for creating tables and reducers, there's two other parts of the Rust SpacetimeDB library. One is a collection of macros for logging, and the other is all the automatically generated functions for operating on those tables. + +### `println!` and friends + +Because reducers run in a WASM sandbox, they don't have access to general purpose I/O from the Rust standard library. There's no filesystem or network access, and no input or output. This means no access to things like `std::println!`, which prints to standard output. + +SpacetimeDB modules have access to logging output. These are exposed as macros, just like their `std` equivalents. The names, and all the Rust formatting machinery, work the same; just the location of the output is different. + +Logs for a module can be viewed with the `spacetime logs` command from the CLI. + +```rust +use spacetimedb::{ + println, + print, + eprintln, + eprint, + dbg, +}; + +#[spacetimedb(reducer)] +fn output(i: i32) { + // These will be logged at log::Level::Info. + println!("an int with a trailing newline: {i}"); + print!("some more text...\n"); + + // These log at log::Level::Error. + eprint!("Oops..."); + eprintln!(", we hit an error"); + + // Just like std::dbg!, this prints its argument and returns the value, + // as a drop-in way to print expressions. So this will print out |i| + // before passing the value of |i| along to the calling function. + // + // The output is logged log::Level::Debug. + OutputtedNumbers::insert(dbg!(i)); +} +``` + +### Generated functions on a SpacetimeDB table + +We'll work off these structs to see what functions SpacetimeDB generates: + +This table has a plain old column. + +```rust +#[spacetimedb(table)] +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. + +```rust +#[spacetimedb(table)] +struct Unique { + // A unique column: + #[unique] + unique_field: u64, +} +``` + +This table has an automatically incrementing column. SpacetimeDB automatically provides an incrementing sequence of values for this field, and sets the field to that value when you insert the row. + +Only integer types can be `#[unique]`: `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64` and `i128`. + +```rust +#[spacetimedb(table)] +struct Autoinc { + #[autoinc] + autoinc_field: u64, +} +``` + +These attributes can be combined, to create an automatically assigned ID usable for filtering. + +```rust +#[spacetimedb(table)] +struct Identity { + #[autoinc] + #[unique] + id_field: u64, +} +``` + +### Insertion + +We'll talk about insertion first, as there a couple of special semantics to know about. + +When we define |Ordinary| as a spacetimedb table, we get the ability to insert into it with the generated `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) { + let ordinary = Ordinary { ordinary_field: value }; + let result = Ordinary::insert(ordinary); + assert_eq!(ordinary.ordinary_field, result.ordinary_field); +} +``` + +When there is a unique column constraint on the table, insertion can fail if a uniqueness constraint is violated. + +If we insert two rows which have the same value of a unique column, the second will fail. + +```rust +#[spacetimedb(reducer)] +fn insert_unique(value: u64) { + let result = Ordinary::insert(Unique { unique_field: value }); + assert!(result.is_ok()); + + let result = Ordinary::insert(Unique { unique_field: value }); + assert!(result.is_err()); +} +``` + +When inserting a table with an `#[autoinc]` column, the database will automatically overwrite whatever we give it with an atomically increasing value. + +The returned row has the `autoinc` column set to the value that was actually written into the database. + +```rust +#[spacetimedb(reducer)] +fn insert_autoinc() { + 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 }) + assert_eq!(actual.autoinc_field, i); + } +} + +#[spacetimedb(reducer)] +fn insert_id() { + 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 }) + } +} +``` + +### Iterating + +Given a table, we can iterate over all the rows in it. + +```rust +#[spacetimedb(table)] +struct Person { + #[unique] + id: u64, + + age: u32, + name: String, + address: String, +} +``` + +// Every table structure an iter function, like: + +```rust +fn MyTable::iter() -> TableIter +``` + +`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() { + let mut addresses = HashSet::new(); + + for person in Person::iter() { + addresses.insert(person.address); + } + + for address in addresses.iter() { + println!("{address}"); + } +} +``` + +### Filtering + +Often, we don't need to look at the entire table, and instead are looking for rows with specific values in certain columns. + +Our `Person` table has a unique id column, so we can filter for a row matching that ID. Since it is unique, we will find either 0 or 1 matching rows in the database. This gets represented naturally as an `Option` in Rust. SpacetimeDB automatically creates and uses indexes for filtering on unique columns, so it is very efficient. + +The name of the filter method just corresponds to the column name. + +```rust +#[spacetimedb(reducer)] +fn filtering(id: u64) { + match Person::filter_by_id(&id) { + Some(person) => println!("Found {person}"), + None => println!("No person with id {id}"), + } +} +``` + +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::filter_by_age(&21) { + println!("{person} has turned 21"); + } +} +``` + +### Deleting + +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) +} +``` + +[macro library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/bindings-macro +[module library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/lib +[demo]: /#demo diff --git a/Writerside/topics/modules/rust/rust_quickstart.md b/Writerside/topics/modules/rust/rust_quickstart.md new file mode 100644 index 00000000..baa62a0d --- /dev/null +++ b/Writerside/topics/modules/rust/rust_quickstart.md @@ -0,0 +1,272 @@ +# Rust Module Quickstart + +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. + +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. + +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. + +## 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. + +## 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: + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +If you're on Windows, go [here](https://learn.microsoft.com/en-us/windows/dev-environment/rust/setup). + +## Project structure + +Create and enter a directory `quickstart-chat`: + +```bash +mkdir quickstart-chat +cd quickstart-chat +``` + +Now create `server`, our module, which runs in the database: + +```bash +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. + +To the top of `server/src/lib.rs`, add some imports we'll be using: + +```rust +use spacetimedb::{spacetimedb, ReducerContext, Identity, Timestamp}; +``` + +From `spacetimedb`, we import: + +- `spacetimedb`, an attribute macro we'll use to define tables and reducers. +- `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. + +## Define tables + +To get our chat server running, we'll need to store two kinds of data: information about each user, and records of all the messages that have been sent. + +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. + +To `server/src/lib.rs`, add the definition of the table `User`: + +```rust +#[spacetimedb(table)] +pub struct User { + #[primarykey] + identity: Identity, + name: Option, + online: bool, +} +``` + +For each `Message`, we'll store the `Identity` of the user who sent it, the `Timestamp` when it was sent, and the text of the message. + +To `server/src/lib.rs`, add the definition of the table `Message`: + +```rust +#[spacetimedb(table)] +pub struct Message { + sender: Identity, + sent: Timestamp, + text: String, +} +``` + +## Set users' names + +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`. + +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> { + 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 }); + Ok(()) + } else { + Err("Cannot set name for unknown user".to_string()) + } +} +``` + +For now, we'll just do a bare minimum of validation, rejecting the empty name. You could extend this in various ways, like: + +- Comparing against a blacklist for moderation purposes. +- Unicode-normalizing names. +- Rejecting names that contain non-printable characters, or removing characters or replacing them with a placeholder. +- Rejecting or truncating long names. +- Rejecting duplicate names. + +To `server/src/lib.rs`, add: + +```rust +/// Takes a name and checks if it's acceptable as a user's name. +fn validate_name(name: String) -> Result { + if name.is_empty() { + Err("Names must not be empty".to_string()) + } else { + Ok(name) + } +} +``` + +## 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`. + +To `server/src/lib.rs`, add: + +```rust +#[spacetimedb(reducer)] +/// Clients invoke this reducer to send messages. +pub fn send_message(ctx: ReducerContext, text: String) -> Result<(), String> { + let text = validate_message(text)?; + log::info!("{}", text); + Message::insert(Message { + sender: ctx.sender, + text, + sent: ctx.timestamp, + }); + Ok(()) +} +``` + +We'll want to validate messages' texts in much the same way we validate users' chosen names. As above, we'll do the bare minimum, rejecting only empty messages. + +To `server/src/lib.rs`, add: + +```rust +/// Takes a message's text and checks if it's acceptable to send. +fn validate_message(text: String) -> Result { + if text.is_empty() { + Err("Messages must not be empty".to_string()) + } else { + Ok(text) + } +} +``` + +You could extend the validation in `validate_message` in similar ways to `validate_name`, or add additional checks to `send_message`, like: + +- Rejecting messages from senders who haven't set their names. +- Rate-limiting users so they can't send new messages too quickly. + +## 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. + +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)]` attribute, 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`. + +To `server/src/lib.rs`, add the definition of the connect reducer: + +```rust +#[spacetimedb(connect)] +// Called when a client connects to the SpacetimeDB +pub fn identity_connected(ctx: ReducerContext) { + if let Some(user) = User::filter_by_identity(&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 }); + } 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 { + 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. + +```rust +#[spacetimedb(disconnect)] +// 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 }); + } else { + // This branch should be unreachable, + // as it doesn't make sense for a client to disconnect without connecting first. + log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender); + } +} +``` + +## 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 ``. + +From the `quickstart-chat` directory, run: + +```bash +spacetime publish --project-path server +``` + +## Call Reducers + +You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. + +```bash +spacetime call send_message 'Hello, World!' +``` + +Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. + +```bash +spacetime logs +``` + +You should now see the output that your module printed in the database. + +```bash +info: Hello, World! +``` + +## SQL Queries + +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" +``` + +```bash + text +--------- + "Hello, World!" +``` + +## What's next? + +You can find the full code for this module [in the SpacetimeDB module examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/modules/quickstart-chat). + +You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quickstart guide for your client language of choice: [Rust](quickstart2.), [C#](quickstart3.), [TypeScript](quickstart4.) or [Python](quickstart5.). + +If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](part-1.) or check out our example game, [BitcraftMini](part-3.). diff --git a/Writerside/topics/satn.md b/Writerside/topics/satn.md new file mode 100644 index 00000000..774ff1b3 --- /dev/null +++ b/Writerside/topics/satn.md @@ -0,0 +1,163 @@ +# SATN JSON Format + +The Spacetime Algebraic Type Notation JSON format defines how Spacetime `AlgebraicType`s and `AlgebraicValue`s are encoded as JSON. Algebraic types and values are JSON-encoded for transport via the [HTTP Databases API](database.) and the [WebSocket text protocol](ws#text-protocol.). + +## Values + +### At a glance + +| Type | Description | +| ---------------- | ---------------------------------------------------------------- | +| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype.). | +| `SumValue` | A value whose type is a [`SumType`](#sumtype.). | +| `ProductValue` | A value whose type is a [`ProductType`](#producttype.). | +| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype.). | +| | | + +### `AlgebraicValue` + +```json +SumValue | ProductValue | BuiltinValue +``` + +### `SumValue` + +An instance of a [`SumType`](#sumtype.). `SumValue`s are encoded as a JSON object with a single key, a non-negative integer tag which identifies the variant. The value associated with this key is the variant data. Variants which hold no data will have an empty array as their value. + +The tag is an index into the [`SumType.variants`](#sumtype.) array of the value's [`SumType`](#sumtype.). + +```json +{ + "": AlgebraicValue +} +``` + +### `ProductValue` + +An instance of a [`ProductType`](#producttype.). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#productype.) array of the value's [`ProductType`](#producttype.). + +```json +array +``` + +### `BuiltinValue` + +An instance of a [`BuiltinType`](#builtintype.). `BuiltinValue`s are encoded as JSON values of corresponding types. + +```json +boolean | number | string | array | map +``` + +| [`BuiltinType`](#builtintype.) | JSON type | +| ----------------------------- | ------------------------------------- | +| `Bool` | `boolean` | +| Integer types | `number` | +| Float types | `number` | +| `String` | `string` | +| Array types | `array` | +| Map types | `map` | + +All SATS integer types are encoded as JSON `number`s, so values of 64-bit and 128-bit integer types may lose precision when encoding values larger than 2⁵². + +## Types + +All SATS types are JSON-encoded by converting them to an `AlgebraicValue`, then JSON-encoding that meta-value. + +### At a glance + +| Type | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------ | +| [`AlgebraicType`](#algebraictype.) | Any SATS type. | +| [`SumType`](#sumtype.) | Sum types, i.e. tagged unions. | +| [`ProductType`](#productype.) | Product types, i.e. structures. | +| [`BuiltinType`](#builtintype.) | Built-in and primitive types, including booleans, numbers, strings, arrays and maps. | +| [`AlgebraicTypeRef`](#algebraictyperef.) | An indirect reference to a type, used to implement recursive types. | + +#### `AlgebraicType` + +`AlgebraicType` is the most general meta-type in the Spacetime Algebraic Type System. Any SATS type can be represented as an `AlgebraicType`. `AlgebraicType` is encoded as a tagged union, with variants for [`SumType`](#sumtype.), [`ProductType`](#producttype.), [`BuiltinType`](#builtintype.) and [`AlgebraicTypeRef`](#algebraictyperef.). + +```json +{ "Sum": SumType } +| { "Product": ProductType } +| { "Builtin": BuiltinType } +| { "Ref": AlgebraicTypeRef } +``` + +#### `SumType` + +The meta-type `SumType` represents sum types, also called tagged unions or Rust `enum`s. A sum type has some number of variants, each of which has an `AlgebraicType` of variant data, and an optional string discriminant. For each instance, exactly one variant will be active. The instance will contain only that variant's data. + +A `SumType` with zero variants is called an empty type or never type because it is impossible to construct an instance. + +Instances of `SumType`s are [`SumValue`s](#sumvalue.), and store a tag which identifies the active variant. + +```json +// SumType: +{ + "variants": array, +} + +// SumTypeVariant: +{ + "algebraic_type": AlgebraicType, + "name": { "some": string } | { "none": [] } +} +``` + +### `ProductType` + +The meta-type `ProductType` represents product types, also called structs or tuples. A product type has some number of fields, each of which has an `AlgebraicType` of field data, and an optional string field name. Each instance will contain data for all of the product type's fields. + +A `ProductType` with zero fields is called a unit type because it has a single instance, the unit, which is empty. + +Instances of `ProductType`s are [`ProductValue`s](#productvalue.), and store an array of field data. + +```json +// ProductType: +{ + "elements": array, +} + +// ProductTypeElement: +{ + "algebraic_type": AlgebraicType, + "name": { "some": string } | { "none": [] } +} +``` + +### `BuiltinType` + +The meta-type `BuiltinType` represents SATS primitive types: booleans, integers, floating-point numbers, strings, arrays and maps. `BuiltinType` is encoded as a tagged union, with a variant for each SATS primitive type. + +SATS integer types are identified by their signedness and width in bits. SATS supports the same set of integer types as Rust, i.e. 8, 16, 32, 64 and 128-bit signed and unsigned integers. + +SATS floating-point number types are identified by their width in bits. SATS supports 32 and 64-bit floats, which correspond to [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) single- and double-precision binary floats, respectively. + +SATS array and map types are homogeneous, meaning that each array has a single element type to which all its elements must conform, and each map has a key type and a value type to which all of its keys and values must conform. + +```json +{ "Bool": [] } +| { "I8": [] } +| { "U8": [] } +| { "I16": [] } +| { "U16": [] } +| { "I32": [] } +| { "U32": [] } +| { "I64": [] } +| { "U64": [] } +| { "I128": [] } +| { "U128": [] } +| { "F32": [] } +| { "F64": [] } +| { "String": [] } +| { "Array": AlgebraicType } +| { "Map": { + "key_ty": AlgebraicType, + "ty": AlgebraicType, + } } +``` + +### `AlgebraicTypeRef` + +`AlgebraicTypeRef`s are JSON-encoded as non-negative integers. These are indices into a typespace, like the one returned by the [`/database/schema/:name_or_address GET` HTTP endpoint](database#databaseschemaname_or_address-get.). diff --git a/Writerside/topics/sdks/c-sharp/c-sharp_quickstart.md b/Writerside/topics/sdks/c-sharp/c-sharp_quickstart.md new file mode 100644 index 00000000..4d5b1e92 --- /dev/null +++ b/Writerside/topics/sdks/c-sharp/c-sharp_quickstart.md @@ -0,0 +1,438 @@ +# C# Client SDK Quick Start + +In this guide we'll show you how to get up and running with a simple SpacetimeDB app with a client written in C#. + +We'll implement a command-line client for the module created in our [Rust](rust_quickstart.md) or [C# Module](quickstart.md) Quickstart guides. Ensure you followed one of these guides before continuing. + +## Project structure + +Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: + +```bash +cd quickstart-chat +``` + +Within it, create a new C# console application project called `client` using either Visual Studio, Rider or the .NET CLI: + +```bash +dotnet new console -o client +``` + +Open the project in your IDE of choice. + +## Add the NuGet package for the C# SpacetimeDB SDK + +Add the `SpacetimeDB.ClientSDK` [NuGet package](https://www.nuget.org/packages/spacetimedbsdk) using Visual Studio or Rider _NuGet Package Manager_ or via the .NET CLI: + +```bash +dotnet add package SpacetimeDB.ClientSDK +``` + +## Generate your module types + +The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. + +In your `quickstart-chat` directory, run: + +```bash +mkdir -p client/module_bindings +spacetime generate --lang csharp --out-dir client/module_bindings --project-path server +``` + +Take a look inside `client/module_bindings`. The CLI should have generated five files: + +``` +module_bindings +├── Message.cs +├── ReducerEvent.cs +├── SendMessageReducer.cs +├── SetNameReducer.cs +└── User.cs +``` + +## Add imports to Program.cs + +Open `client/Program.cs` and add the following imports: + +```csharp +using SpacetimeDB; +using SpacetimeDB.Types; +using System.Collections.Concurrent; +``` + +We will also need to create some global variables that will be explained when we use them later. Add the following to the top of `Program.cs`: + +```csharp +// our local client SpacetimeDB identity +Identity? local_identity = null; + +// declare a thread safe queue to store commands in format (command, args) +ConcurrentQueue<(string,string)> input_queue = new ConcurrentQueue<(string, string)>(); + +// declare a threadsafe cancel token to cancel the process loop +CancellationTokenSource cancel_token = new CancellationTokenSource(); +``` + +## Define 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 several things: + +1. Initialize the `AuthToken` module, which loads and stores our authentication token to/from local storage. +2. Create the `SpacetimeDBClient` instance. +3. Register callbacks on any events we want to handle. These will print to standard output messages received from the database and updates about users' names and online statuses. +4. Start our processing thread which connects to the SpacetimeDB module, updates the SpacetimeDB client and processes commands that come in from the input loop running in the main thread. +5. Start the input loop, which reads commands from standard input and sends them to the processing thread. +6. When the input loop exits, stop the processing thread and wait for it to exit. + +```csharp +void Main() +{ + AuthToken.Init(".spacetime_csharp_quickstart"); + + // create the client, pass in a logger to see debug messages + SpacetimeDBClient.CreateInstance(new ConsoleLogger()); + + RegisterCallbacks(); + + // spawn a thread to call process updates and process commands + var thread = new Thread(ProcessThread); + thread.Start(); + + InputLoop(); + + // this signals the ProcessThread to stop + cancel_token.Cancel(); + thread.Join(); +} +``` + +## Register callbacks + +We need to handle several sorts of events: + +1. `onConnect`: When we connect, we will call `Subscribe` to tell the module what tables we care about. +2. `onIdentityReceived`: When we receive our credentials, we'll use the `AuthToken` module to save our token so that the next time we connect, we can re-authenticate as the same user. +3. `onSubscriptionApplied`: When we get the onSubscriptionApplied callback, that means our local client cache has been fully populated. At this time we'll print the user menu. +4. `User.OnInsert`: When a new user joins, we'll print a message introducing them. +5. `User.OnUpdate`: When a user is updated, we'll print their new name, or declare their new online status. +6. `Message.OnInsert`: When we receive a new message, we'll print it. +7. `Reducer.OnSetNameEvent`: If the server rejects our attempt to set our name, we'll print an error. +8. `Reducer.OnSendMessageEvent`: If the server rejects a message we send, we'll print an error. + +```csharp +void RegisterCallbacks() +{ + SpacetimeDBClient.instance.onConnect += OnConnect; + SpacetimeDBClient.instance.onIdentityReceived += OnIdentityReceived; + SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + + User.OnInsert += User_OnInsert; + User.OnUpdate += User_OnUpdate; + + Message.OnInsert += Message_OnInsert; + + Reducer.OnSetNameEvent += Reducer_OnSetNameEvent; + Reducer.OnSendMessageEvent += Reducer_OnSendMessageEvent; +} +``` + +### 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 `OnInsert` and `OnDelete` methods, which are automatically generated 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.OnInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. + +`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `ReducerEvent`. This will be `null` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument. + +Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `UserNameOrIdentity` to handle this. + +```csharp +string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()!.Substring(0, 8); + +void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) +{ + if (insertedValue.Online) + { + Console.WriteLine($"{UserNameOrIdentity(insertedValue)} is online"); + } +} +``` + +### Notify about updated users + +Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User::update_by_identity` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column. + +`OnUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. + +In our module, users can be updated for three reasons: + +1. They've set their name using the `SetName` reducer. +2. They're an existing user re-connecting, so their `Online` has been set to `true`. +3. They've disconnected, so their `Online` has been set to `false`. + +We'll print an appropriate message in each of these cases. + +```csharp +void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) +{ + if (oldValue.Name != newValue.Name) + { + Console.WriteLine($"{UserNameOrIdentity(oldValue)} renamed to {newValue.Name}"); + } + + if (oldValue.Online == newValue.Online) + return; + + if (newValue.Online) + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); + } + else + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); + } +} +``` + +### 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 `SendMessage` reducer invocation. We have to handle the backlog we receive when our subscription is initialized separately, to ensure they're printed in the correct order. To that effect, our `OnInsert` callback will check if its `ReducerEvent` argument is not `null`, and only print in that case. + +To find the `User` based on the message's `Sender` identity, we'll use `User::FilterByIdentity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `FilterByIdentity` accepts a `byte[]`, rather than an `Identity`. The `Sender` identity stored in the message is also a `byte[]`, not an `Identity`, so we can just pass it to the filter method. + +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`. + +```csharp +void PrintMessage(Message message) +{ + var sender = User.FilterByIdentity(message.Sender); + var senderName = "unknown"; + if (sender != null) + { + senderName = UserNameOrIdentity(sender); + } + + Console.WriteLine($"{senderName}: {message.Text}"); +} + +void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent) +{ + if (dbEvent != null) + { + PrintMessage(insertedValue); + } +} +``` + +### Warn if our name was rejected + +We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducerEvent` method of the `Reducer` namespace, which is automatically implemented for each reducer by `spacetime generate`. + +Each reducer callback takes one fixed argument: + +The ReducerEvent that triggered the callback. It contains several fields. The ones we care about are: + +1. The `Identity` of the client that called the reducer. +2. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`. +3. The error message, if any, that the reducer returned. + +It also takes a variable amount of additional arguments that match the reducer's arguments. + +These callbacks will be invoked in one of two cases: + +1. If the reducer was successful and altered any of our subscribed rows. +2. If we requested an invocation which failed. + +Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. + +We already handle successful `SetName` invocations using our `User.OnUpdate` 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 `Reducer_OnSetNameEvent` as a `Reducer.OnSetNameEvent` callback which checks if the reducer failed, and if it did, prints an error message including the rejected name. + +We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. + +```csharp +void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) +{ + bool localIdentityFailedToChangeName = + reducerEvent.Identity == local_identity && + reducerEvent.Status == ClientApi.Event.Types.Status.Failed; + + if (localIdentityFailedToChangeName) + { + Console.Write($"Failed to change name to {name}"); + } +} +``` + +### 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. + +```csharp +void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) +{ + bool localIdentityFailedToSendMessage = + reducerEvent.Identity == local_identity && + reducerEvent.Status == ClientApi.Event.Types.Status.Failed; + + if (localIdentityFailedToSendMessage) + { + Console.Write($"Failed to send message {text}"); + } +} +``` + +## Connect callback + +Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. + +```csharp +void OnConnect() +{ + SpacetimeDBClient.instance.Subscribe(new List + { + "SELECT * FROM User", "SELECT * FROM Message" + }); +} +``` + +## OnIdentityReceived callback + +This callback is executed when we receive our credentials from the SpacetimeDB module. We'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app. + +```csharp +void OnIdentityReceived(string authToken, Identity identity, Address _address) +{ + local_identity = identity; + AuthToken.SaveToken(authToken); +} +``` + +## OnSubscriptionApplied callback + +Once our subscription is applied, we'll print all the previously sent messages. We'll define a function `PrintMessagesInOrder` to do this. `PrintMessagesInOrder` calls the automatically generated `Iter` function on our `Message` table, which returns an iterator over all rows in the table. We'll use the `OrderBy` method on the iterator to sort the messages by their `Sent` timestamp. + +```csharp +void PrintMessagesInOrder() +{ + foreach (Message message in Message.Iter().OrderBy(item => item.Sent)) + { + PrintMessage(message); + } +} + +void OnSubscriptionApplied() +{ + Console.WriteLine("Connected"); + PrintMessagesInOrder(); +} +``` + + + +## Process thread + +Since the input loop will be blocking, we'll run our processing code in a separate thread. This thread will: + +1. Connect to the module. We'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`. We will also store if SSL is enabled in a constant called `SSL_ENABLED`. This only needs to be `true` if we are using `SpacetimeDB Cloud`. Replace `` with the name you chose when publishing your module during the module quickstart. + +`Connect` takes an auth token, which is `null` for a new connection, or a stored string for a returning user. We are going to use the optional AuthToken module which uses local storage to store the auth token. If you want to use your own way to associate an auth token with a user, you can pass in your own auth token here. + +2. Loop until the thread is signaled to exit, calling `Update` on the SpacetimeDBClient to process any updates received from the module, and `ProcessCommand` to process any commands received from the input loop. + +3. Finally, Close the connection to the module. + +```csharp +const string HOST = "http://localhost:3000"; +const string DBNAME = "module"; + +void ProcessThread() +{ + SpacetimeDBClient.instance.Connect(AuthToken.Token, HOST, DBNAME); + + // loop until cancellation token + while (!cancel_token.IsCancellationRequested) + { + SpacetimeDBClient.instance.Update(); + + ProcessCommands(); + + Thread.Sleep(100); + } + + SpacetimeDBClient.instance.Close(); +} +``` + +## Input loop and ProcessCommands + +The input loop will read commands from standard input and send them to the processing thread using the input queue. The `ProcessCommands` function is called every 100ms by the processing thread to process any pending commands. + +Supported Commands: + +1. Send a message: `message`, send the message to the module by calling `Reducer.SendMessage` which is automatically generated by `spacetime generate`. + +2. Set name: `name`, will send the new name to the module by calling `Reducer.SetName` which is automatically generated by `spacetime generate`. + +```csharp +void InputLoop() +{ + while (true) + { + var input = Console.ReadLine(); + if (input == null) + { + break; + } + + if (input.StartsWith("/name ")) + { + input_queue.Enqueue(("name", input.Substring(6))); + continue; + } + else + { + input_queue.Enqueue(("message", input)); + } + } +} + +void ProcessCommands() +{ + // process input queue commands + while (input_queue.TryDequeue(out var command)) + { + switch (command.Item1) + { + case "message": + Reducer.SendMessage(command.Item2); + break; + case "name": + Reducer.SetName(command.Item2); + break; + } + } +} +``` + +## Run the client + +Finally we just need to add a call to `Main` in `Program.cs`: + +```csharp +Main(); +``` + +Now, we can run the client by hitting start in Visual Studio or Rider; or by running the following command in the `client` directory: + +```bash +dotnet run --project client +``` + +## What's next? + +Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example. diff --git a/Writerside/topics/sdks/c-sharp/sdks_c-sharp_index.md b/Writerside/topics/sdks/c-sharp/sdks_c-sharp_index.md new file mode 100644 index 00000000..a0f1c7f3 --- /dev/null +++ b/Writerside/topics/sdks/c-sharp/sdks_c-sharp_index.md @@ -0,0 +1,959 @@ +# The SpacetimeDB C# client SDK + +The SpacetimeDB client C# for Rust contains all the tools you need to build native clients for SpacetimeDB modules using C#. + +## Table of Contents + +- [The SpacetimeDB C# client SDK](#the-spacetimedb-c-client-sdk.) + - [Table of Contents](#table-of-contents.) + - [Install the SDK](#install-the-sdk.) + - [Using the `dotnet` CLI tool](#using-the-dotnet-cli-tool.) + - [Using Unity](#using-unity.) + - [Generate module bindings](#generate-module-bindings.) + - [Initialization](#initialization.) + - [Static Method `SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) + - [Property `SpacetimeDBClient.instance`](#property-spacetimedbclientinstance.) + - [Class `NetworkManager`](#class-networkmanager.) + - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect.) + - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived.) + - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect.) + - [Query subscriptions & one-time actions](#subscribe-to-queries.) + - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.) + - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied.) + - [Method `SpacetimeDBClient.OneOffQuery`](#event-spacetimedbclientoneoffquery.) + - [View rows of subscribed tables](#view-rows-of-subscribed-tables.) + - [Class `{TABLE}`](#class-table.) + - [Static Method `{TABLE}.Iter`](#static-method-tableiter.) + - [Static Method `{TABLE}.FilterBy{COLUMN}`](#static-method-tablefilterbycolumn.) + - [Static Method `{TABLE}.Count`](#static-method-tablecount.) + - [Static Event `{TABLE}.OnInsert`](#static-event-tableoninsert.) + - [Static Event `{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) + - [Static Event `{TABLE}.OnDelete`](#static-event-tableondelete.) + - [Static Event `{TABLE}.OnUpdate`](#static-event-tableonupdate.) + - [Observe and invoke reducers](#observe-and-invoke-reducers.) + - [Class `Reducer`](#class-reducer.) + - [Static Method `Reducer.{REDUCER}`](#static-method-reducerreducer.) + - [Static Event `Reducer.On{REDUCER}`](#static-event-reduceronreducer.) + - [Class `ReducerEvent`](#class-reducerevent.) + - [Enum `Status`](#enum-status.) + - [Variant `Status.Committed`](#variant-statuscommitted.) + - [Variant `Status.Failed`](#variant-statusfailed.) + - [Variant `Status.OutOfEnergy`](#variant-statusoutofenergy.) + - [Identity management](#identity-management.) + - [Class `AuthToken`](#class-authtoken.) + - [Static Method `AuthToken.Init`](#static-method-authtokeninit.) + - [Static Property `AuthToken.Token`](#static-property-authtokentoken.) + - [Static Method `AuthToken.SaveToken`](#static-method-authtokensavetoken.) + - [Class `Identity`](#class-identity.) + - [Class `Identity`](#class-identity-1.) + - [Customizing logging](#customizing-logging.) + - [Interface `ISpacetimeDBLogger`](#interface-ispacetimedblogger.) + - [Class `ConsoleLogger`](#class-consolelogger.) + - [Class `UnityDebugLogger`](#class-unitydebuglogger.) + +## Install the SDK + +### Using the `dotnet` CLI tool + +If you would like to create a console application using .NET, you can create a new project using `dotnet new console` and add the SpacetimeDB SDK to your dependencies: + +```bash +dotnet add package spacetimedbsdk +``` + +(See also the [CSharp Quickstart](quickstart1.) for an in-depth example of such a console application.) + +### Using Unity + +To install the SpacetimeDB SDK into a Unity project, [download the SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest), packaged as a `.unitypackage`. + +In Unity navigate to the `Assets > Import Package > Custom Package` menu in the menu bar. Select your `SpacetimeDB.Unity.Comprehensive.Tutorial.unitypackage` file and leave all folders checked. + +(See also the [Unity Tutorial](part-1.)) + +## Generate module bindings + +Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the C# interface files using the Spacetime CLI. From your project directory, run: + +```bash +mkdir -p module_bindings +spacetime generate --lang cs --out-dir module_bindings --project-path PATH-TO-MODULE-DIRECTORY +``` + +Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. + +## Initialization + +### Static Method `SpacetimeDBClient.CreateInstance` + +```cs +namespace SpacetimeDB { + +public class SpacetimeDBClient { + public static void CreateInstance(ISpacetimeDBLogger loggerToUse); +} + +} +``` + +Create a global SpacetimeDBClient instance, accessible via [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance.) + +| Argument | Type | Meaning | +| ------------- | ----------------------------------------------------- | --------------------------------- | +| `loggerToUse` | [`ISpacetimeDBLogger`](#interface-ispacetimedblogger.) | The logger to use to log messages | + +There is a provided logger called [`ConsoleLogger`](#class-consolelogger.) which logs to `System.Console`, and can be used as follows: + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; +SpacetimeDBClient.CreateInstance(new ConsoleLogger()); +``` + +### Property `SpacetimeDBClient.instance` + +```cs +namespace SpacetimeDB { + +public class SpacetimeDBClient { + public static SpacetimeDBClient instance; +} + +} +``` + +This is the global instance of a SpacetimeDB client in a particular .NET/Unity process. Much of the SDK is accessible through this instance. + +### Class `NetworkManager` + +The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. + +![Unity-AddNetworkManager](Unity-AddNetworkManager.JPG) + +This component will handle calling [`SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect.), you still need to handle that yourself. See the [Unity Quickstart](UnityQuickStart.) and [Unity Tutorial](UnityTutorialPart1.) for more information. + +### Method `SpacetimeDBClient.Connect` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public void Connect( + string? token, + string host, + string addressOrName, + bool sslEnabled = true + ); +} + +} +``` + + + +Connect to a database named `addressOrName` accessible over the internet at the URI `host`. + +| Argument | Type | Meaning | +| --------------- | --------- | -------------------------------------------------------------------------- | +| `token` | `string?` | Identity token to use, if one is available. | +| `host` | `string` | URI of the SpacetimeDB instance running the module. | +| `addressOrName` | `string` | Address or name of the module. | +| `sslEnabled` | `bool` | Whether or not to use SSL when connecting to SpacetimeDB. Default: `true`. | + +If a `token` is supplied, it will be passed to the new connection to identify and authenticate the user. Otherwise, a new token and [`Identity`](#class-identity.) will be generated by the server and returned in [`onConnect`](#event-spacetimedbclientonconnect.). + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +const string DBNAME = "chat"; + +// Connect to a local DB with a fresh identity +SpacetimeDBClient.instance.Connect(null, "localhost:3000", DBNAME, false); + +// Connect to cloud with a fresh identity +SpacetimeDBClient.instance.Connect(null, "dev.spacetimedb.net", DBNAME, true); + +// Connect to cloud using a saved identity from the filesystem, or get a new one and save it +AuthToken.Init(); +Identity localIdentity; +SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); +SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { + AuthToken.SaveToken(authToken); + localIdentity = identity; +} +``` + +(You should probably also store the returned `Identity` somewhere; see the [`onIdentityReceived`](#event-spacetimedbclientonidentityreceived.) event.) + +### Event `SpacetimeDBClient.onIdentityReceived` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public event Action onIdentityReceived; +} + +} +``` + ++Called when we receive an auth token, [`Identity`](#class-identity.) and [`Address`](#class-address.) from the server. The [`Identity`](#class-identity.) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The [`Address`](#class-address.) is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity.). + +To store the auth token to the filesystem, use the static method [`AuthToken.SaveToken`](#static-method-authtokensavetoken.). You may also want to store the returned [`Identity`](#class-identity.) in a local variable. + +If an existing auth token is used to connect to the database, the same auth token and the identity it came with will be returned verbatim in `onIdentityReceived`. + +```cs +// Connect to cloud using a saved identity from the filesystem, or get a new one and save it +AuthToken.Init(); +Identity localIdentity; +SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); +SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { + AuthToken.SaveToken(authToken); + localIdentity = identity; +} +``` + +### Event `SpacetimeDBClient.onConnect` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public event Action onConnect; +} + +} +``` + +Allows registering delegates to be invoked upon authentication with the database. + +Once this occurs, the SDK is prepared for calls to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.). + +## Subscribe to queries + +### Method `SpacetimeDBClient.Subscribe` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public void Subscribe(List queries); +} + +} +``` + +| Argument | Type | Meaning | +| --------- | -------------- | ---------------------------- | +| `queries` | `List` | SQL queries to subscribe to. | + +Subscribe to a set of queries, to be notified when rows which match those queries are altered. + +`Subscribe` will return an error if called before establishing a connection with the [`SpacetimeDBClient.Connect`](#method-connect.) function. In that case, the queries are not registered. + +The `Subscribe` method does not return data directly. `spacetime generate` will generate classes [`SpacetimeDB.Types.{TABLE}`](#class-table.) for each table in your module. These classes are used to reecive information from the database. See the section [View Rows of Subscribed Tables](#view-rows-of-subscribed-tables.) for more information. + +A new call to `Subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache, and [`{TABLE}.OnDelete`](#event-tableondelete.) callbacks will be invoked for them. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +void Main() +{ + AuthToken.Init(); + SpacetimeDBClient.CreateInstance(new ConsoleLogger()); + + SpacetimeDBClient.instance.onConnect += OnConnect; + + // Our module contains a table named "Loot" + Loot.OnInsert += Loot_OnInsert; + + SpacetimeDBClient.instance.Connect(/* ... */); +} + +void OnConnect() +{ + SpacetimeDBClient.instance.Subscribe(new List { + "SELECT * FROM Loot" + }); +} + +void Loot_OnInsert( + Loot loot, + ReducerEvent? event +) { + Console.Log($"Loaded loot {loot.itemType} at coordinates {loot.position}"); +} +``` + +### Event `SpacetimeDBClient.onSubscriptionApplied` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public event Action onSubscriptionApplied; +} + +} +``` + +Register a delegate to be invoked when a subscription is registered with the database. + +```cs +using SpacetimeDB; + +void OnSubscriptionApplied() +{ + Console.WriteLine("Now listening on queries."); +} + +void Main() +{ + // ...initialize... + SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; +} +``` + +### Method [`OneTimeQuery`](#method-spacetimedbclientsubscribe.) + +You may not want to subscribe to a query, but instead want to run a query once and receive the results immediately via a `Task` result: + +```csharp +// Query all Messages from the sender "bob" +SpacetimeDBClient.instance.OneOffQuery("WHERE sender = \"bob\""); +``` + +## View rows of subscribed tables + +The SDK maintains a local view of the database called the "client cache". This cache contains whatever rows are selected via a call to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.). These rows are represented in the SpacetimeDB .Net SDK as instances of [`SpacetimeDB.Types.{TABLE}`](#class-table.). + +ONLY the rows selected in a [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.) call will be available in the client cache. All operations in the client sdk operate on these rows exclusively, and have no information about the state of the rest of the database. + +In particular, SpacetimeDB does not support foreign key constraints. This means that if you are using a column as a foreign key, SpacetimeDB will not automatically bring in all of the rows that key might reference. You will need to manually subscribe to all tables you need information from. + +To optimize network performance, prefer selecting as few rows as possible in your [`Subscribe`](#method-spacetimedbclientsubscribe.) query. Processes that need to view the entire state of the database are better run inside the database -- that is, inside modules. + +### Class `{TABLE}` + +For each table defined by a module, `spacetime generate` will generate a class [`SpacetimeDB.Types.{TABLE}`](#class-table.) whose name is that table's name converted to `PascalCase`. The generated class contains a property for each of the table's columns, whose names are the column names converted to `camelCase`. It also contains various static events and methods. + +Static Methods: + +- [`{TABLE}.Iter()`](#static-method-tableiter.) iterates all subscribed rows in the client cache. +- [`{TABLE}.FilterBy{COLUMN}(value)`](#static-method-tablefilterbycolumn.) filters subscribed rows in the client cache by a column value. +- [`{TABLE}.Count()`](#static-method-tablecount.) counts the number of subscribed rows in the client cache. + +Static Events: + +- [`{TABLE}.OnInsert`](#static-event-tableoninsert.) is called when a row is inserted into the client cache. +- [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) is called when a row is about to be removed from the client cache. +- If the table has a primary key attribute, [`{TABLE}.OnUpdate`](#static-event-tableonupdate.) is called when a row is updated. +- [`{TABLE}.OnDelete`](#static-event-tableondelete.) is called while a row is being removed from the client cache. You should almost always use [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) instead. + +Note that it is not possible to directly insert into the database from the client SDK! All insertion validation should be performed inside serverside modules for security reasons. You can instead [invoke reducers](#observe-and-invoke-reducers.), which run code inside the database that can insert rows for you. + +#### Static Method `{TABLE}.Iter` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public static System.Collections.Generic.IEnumerable Iter(); +} + +} +``` + +Iterate over all the subscribed rows in the table. This method is only available after [`SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied.) has occurred. + +When iterating over rows and filtering for those containing a particular column, [`TableType::filter`](#method-filter.) will be more efficient, so prefer it when possible. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { + SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); +}; +SpacetimeDBClient.instance.onSubscriptionApplied += () => { + // Will print a line for each `User` row in the database. + foreach (var user in User.Iter()) { + Console.WriteLine($"User: {user.Name}"); + } +}; +SpacetimeDBClient.instance.connect(/* ... */); +``` + +#### Static Method `{TABLE}.FilterBy{COLUMN}` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + // If the column has no #[unique] or #[primarykey] constraint + public static System.Collections.Generic.IEnumerable
FilterBySender(COLUMNTYPE value); + + // If the column has a #[unique] or #[primarykey] constraint + public static TABLE? FilterBySender(COLUMNTYPE value); +} + +} +``` + +For each column of a table, `spacetime generate` generates a static method on the [table class](#class-table.) to filter or seek subscribed rows where that column matches a requested value. These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `PascalCase`. + +The method's return type depends on the column's attributes: + +- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filterBy{COLUMN}` method returns a `{TABLE}?`, where `{TABLE}` is the [table class](#class-table.). +- For non-unique columns, the `filter_by` method returns an `IEnumerator<{TABLE}>`. + +#### Static Method `{TABLE}.Count` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public static int Count(); +} + +} +``` + +Return the number of subscribed rows in the table, or 0 if there is no active connection. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { + SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); +}; +SpacetimeDBClient.instance.onSubscriptionApplied += () => { + Console.WriteLine($"There are {User.Count()} users in the database."); +}; +SpacetimeDBClient.instance.connect(/* ... */); +``` + +#### Static Event `{TABLE}.OnInsert` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public delegate void InsertEventHandler( + TABLE insertedValue, + ReducerEvent? dbEvent + ); + public static event InsertEventHandler OnInsert; +} + +} +``` + +Register a delegate for when a subscribed row is newly inserted into the database. + +The delegate takes two arguments: + +- A [`{TABLE}`](#class-table.) instance with the data of the inserted row +- A [`ReducerEvent?`], which contains the data of the reducer that inserted the row, or `null` if the row is being inserted while initializing a subscription. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +/* initialize, subscribe to table User... */ + +User.OnInsert += (User user, ReducerEvent? reducerEvent) => { + if (reducerEvent == null) { + Console.WriteLine($"New user '{user.Name}' received during subscription update."); + } else { + Console.WriteLine($"New user '{user.Name}' inserted by reducer {reducerEvent.Reducer}."); + } +}; +``` + +#### Static Event `{TABLE}.OnBeforeDelete` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public delegate void DeleteEventHandler( + TABLE deletedValue, + ReducerEvent dbEvent + ); + public static event DeleteEventHandler OnBeforeDelete; +} + +} +``` + +Register a delegate for when a subscribed row is about to be deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked for each of those rows before any of them is deleted. + +The delegate takes two arguments: + +- A [`{TABLE}`](#class-table.) instance with the data of the deleted row +- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that deleted the row. + +This event should almost always be used instead of [`OnDelete`](#static-event-tableondelete.). This is because often, many rows will be deleted at once, and `OnDelete` can be invoked in an arbitrary order on these rows. This means that data related to a row may already be missing when `OnDelete` is called. `OnBeforeDelete` does not have this problem. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +/* initialize, subscribe to table User... */ + +User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { + Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); +}; +``` + +#### Static Event `{TABLE}.OnDelete` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public delegate void DeleteEventHandler( + TABLE deletedValue, + SpacetimeDB.ReducerEvent dbEvent + ); + public static event DeleteEventHandler OnDelete; +} + +} +``` + +Register a delegate for when a subscribed row is being deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked on those rows in arbitrary order, and data for some rows may already be missing when it is invoked. For this reason, prefer the event [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.). + +The delegate takes two arguments: + +- A [`{TABLE}`](#class-table.) instance with the data of the deleted row +- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that deleted the row. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +/* initialize, subscribe to table User... */ + +User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { + Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); +}; +``` + +#### Static Event `{TABLE}.OnUpdate` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public delegate void UpdateEventHandler( + TABLE oldValue, + TABLE newValue, + ReducerEvent dbEvent + ); + public static event UpdateEventHandler OnUpdate; +} + +} +``` + +Register a delegate for when a subscribed row is being updated. This event is only available if the row has a column with the `#[primary_key]` attribute. + +The delegate takes three arguments: + +- A [`{TABLE}`](#class-table.) instance with the old data of the updated row +- A [`{TABLE}`](#class-table.) instance with the new data of the updated row +- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that updated the row. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +/* initialize, subscribe to table User... */ + +User.OnUpdate += (User oldUser, User newUser, ReducerEvent reducerEvent) => { + Debug.Assert(oldUser.UserId == newUser.UserId, "Primary key never changes in an update"); + + Console.WriteLine($"User with ID {oldUser.UserId} had name changed "+ + $"from '{oldUser.Name}' to '{newUser.Name}' by reducer {reducerEvent.Reducer}."); +}; +``` + +## Observe and invoke reducers + +"Reducer" is SpacetimeDB's name for the stored procedures that run in modules inside the database. You can invoke reducers from a connected client SDK, and also receive information about which reducers are running. + +`spacetime generate` generates a class [`SpacetimeDB.Types.Reducer`](#class-reducer.) that contains methods and events for each reducer defined in a module. To invoke a reducer, use the method [`Reducer.{REDUCER}`](#static-method-reducerreducer.) generated for it. To receive a callback each time a reducer is invoked, use the static event [`Reducer.On{REDUCER}`](#static-event-reduceronreducer.). + +### Class `Reducer` + +```cs +namespace SpacetimeDB.Types { + +class Reducer {} + +} +``` + +This class contains a static method and event for each reducer defined in a module. + +#### Static Method `Reducer.{REDUCER}` + +```cs +namespace SpacetimeDB.Types { +class Reducer { + +/* void {REDUCER_NAME}(...ARGS...) */ + +} +} +``` + +For each reducer defined by a module, `spacetime generate` generates a static method which sends a request to the database to invoke that reducer. The generated function's name is the reducer's name converted to `PascalCase`. + +Reducers don't run immediately! They run as soon as the request reaches the database. Don't assume data inserted by a reducer will be available immediately after you call this method. + +For reducers which accept a `ReducerContext` as their first argument, the `ReducerContext` is not included in the generated function's argument list. + +For example, if we define a reducer in Rust as follows: + +```rust +#[spacetimedb(reducer)] +pub fn set_name( + ctx: ReducerContext, + user_id: u64, + name: String +) -> Result<(), Error>; +``` + +The following C# static method will be generated: + +```cs +namespace SpacetimeDB.Types { +class Reducer { + +public static void SendMessage(UInt64 userId, string name); + +} +} +``` + +#### Static Event `Reducer.On{REDUCER}` + +```cs +namespace SpacetimeDB.Types { +class Reducer { + +public delegate void /*{REDUCER}*/Handler(ReducerEvent reducerEvent, /* {ARGS...} */); + +public static event /*{REDUCER}*/Handler On/*{REDUCER}*/Event; + +} +} +``` + +For each reducer defined by a module, `spacetime generate` generates an event to run each time the reducer is invoked. The generated functions are named `on{REDUCER}Event`, where `{REDUCER}` is the reducer's name converted to `PascalCase`. + +The first argument to the event handler is an instance of [`SpacetimeDB.Types.ReducerEvent`](#class-reducerevent.) describing the invocation -- its timestamp, arguments, and whether it succeeded or failed. The remaining arguments are the arguments passed to the reducer. Reducers cannot have return values, so no return value information is included. + +For example, if we define a reducer in Rust as follows: + +```rust +#[spacetimedb(reducer)] +pub fn set_name( + ctx: ReducerContext, + user_id: u64, + name: String +) -> Result<(), Error>; +``` + +The following C# static method will be generated: + +```cs +namespace SpacetimeDB.Types { +class Reducer { + +public delegate void SetNameHandler( + ReducerEvent reducerEvent, + UInt64 userId, + string name +); +public static event SetNameHandler OnSetNameEvent; + +} +} +``` + +Which can be used as follows: + +```cs +/* initialize, wait for onSubscriptionApplied... */ + +Reducer.SetNameHandler += ( + ReducerEvent reducerEvent, + UInt64 userId, + string name +) => { + if (reducerEvent.Status == ClientApi.Event.Types.Status.Committed) { + Console.WriteLine($"User with id {userId} set name to {name}"); + } else if (reducerEvent.Status == ClientApi.Event.Types.Status.Failed) { + Console.WriteLine( + $"User with id {userId} failed to set name to {name}:" + + reducerEvent.ErrMessage + ); + } else if (reducerEvent.Status == ClientApi.Event.Types.Status.OutOfEnergy) { + Console.WriteLine( + $"User with id {userId} failed to set name to {name}:" + + "Invoker ran out of energy" + ); + } +}; +Reducer.SetName(USER_ID, NAME); +``` + +### Class `ReducerEvent` + +`spacetime generate` defines an class `ReducerEvent` containing an enum `ReducerType` with a variant for each reducer defined by a module. The variant's name will be the reducer's name converted to `PascalCase`. + +For example, the example project shown in the Rust Module quickstart will generate the following (abridged) code. + +```cs +namespace SpacetimeDB.Types { + +public enum ReducerType +{ + /* A member for each reducer in the module, with names converted to PascalCase */ + None, + SendMessage, + SetName, +} +public partial class SendMessageArgsStruct +{ + /* A member for each argument of the reducer SendMessage, with names converted to PascalCase. */ + public string Text; +} +public partial class SetNameArgsStruct +{ + /* A member for each argument of the reducer SetName, with names converted to PascalCase. */ + public string Name; +} +public partial class ReducerEvent : ReducerEventBase { + // Which reducer was invoked + public ReducerType Reducer { get; } + // If event.Reducer == ReducerType.SendMessage, the arguments + // sent to the SendMessage reducer. Otherwise, accesses will + // throw a runtime error. + public SendMessageArgsStruct SendMessageArgs { get; } + // If event.Reducer == ReducerType.SetName, the arguments + // passed to the SetName reducer. Otherwise, accesses will + // throw a runtime error. + public SetNameArgsStruct SetNameArgs { get; } + + /* Additional information, present on any ReducerEvent */ + // The name of the reducer. + public string ReducerName { get; } + // The timestamp of the reducer invocation inside the database. + public ulong Timestamp { get; } + // The identity of the client that invoked the reducer. + public SpacetimeDB.Identity Identity { get; } + // Whether the reducer succeeded, failed, or ran out of energy. + public ClientApi.Event.Types.Status Status { get; } + // If event.Status == Status.Failed, the error message returned from inside the module. + public string ErrMessage { get; } +} + +} +``` + +#### Enum `Status` + +```cs +namespace ClientApi { +public sealed partial class Event { +public static partial class Types { + +public enum Status { + Committed = 0, + Failed = 1, + OutOfEnergy = 2, +} + +} +} +} +``` + +An enum whose variants represent possible reducer completion statuses of a reducer invocation. + +##### Variant `Status.Committed` + +The reducer finished successfully, and its row changes were committed to the database. + +##### Variant `Status.Failed` + +The reducer failed, either by panicking or returning a `Err`. + +##### Variant `Status.OutOfEnergy` + +The reducer was canceled because the module owner had insufficient energy to allow it to run to completion. + +## Identity management + +### Class `AuthToken` + +The AuthToken helper class handles creating and saving SpacetimeDB identity tokens in the filesystem. + +#### Static Method `AuthToken.Init` + +```cs +namespace SpacetimeDB { + +class AuthToken { + public static void Init( + string configFolder = ".spacetime_csharp_sdk", + string configFile = "settings.ini", + string? configRoot = null + ); +} + +} +``` + +Creates a file `$"{configRoot}/{configFolder}/{configFile}"` to store tokens. +If no arguments are passed, the default is `"%HOME%/.spacetime_csharp_sdk/settings.ini"`. + +| Argument | Type | Meaning | +| -------------- | -------- | ---------------------------------------------------------------------------------- | +| `configFolder` | `string` | The folder to store the config file in. Default is `"spacetime_csharp_sdk"`. | +| `configFile` | `string` | The name of the config file. Default is `"settings.ini"`. | +| `configRoot` | `string` | The root folder to store the config file in. Default is the user's home directory. | + +#### Static Property `AuthToken.Token` + +```cs +namespace SpacetimeDB { + +class AuthToken { + public static string? Token { get; } +} + +} +``` + +The auth token stored on the filesystem, if one exists. + +#### Static Method `AuthToken.SaveToken` + +```cs +namespace SpacetimeDB { + +class AuthToken { + public static void SaveToken(string token); +} + +} +``` + +Save a token to the filesystem. + +### Class `Identity` + +```cs +namespace SpacetimeDB +{ + public struct Identity : IEquatable + { + public byte[] Bytes { get; } + public static Identity From(byte[] bytes); + public bool Equals(Identity other); + public static bool operator ==(Identity a, Identity b); + public static bool operator !=(Identity a, Identity b); + } +} +``` + +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. + +### Class `Identity` +```cs +namespace SpacetimeDB +{ + public struct Address : IEquatable
+ { + public byte[] Bytes { get; } + public static Address? From(byte[] bytes); + public bool Equals(Address other); + public static bool operator ==(Address a, Address b); + public static bool operator !=(Address a, Address b); + } +} +``` + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity.). + +## Customizing logging + +The SpacetimeDB C# SDK performs internal logging. Instances of [`ISpacetimeDBLogger`](#interface-ispacetimedblogger.) can be passed to [`SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) to customize how SDK logs are delivered to your application. + +This is set up automatically for you if you use Unity-- adding a [`NetworkManager`](#class-networkmanager.) component to your unity scene will automatically initialize the `SpacetimeDBClient` with a [`UnityDebugLogger`](#class-unitydebuglogger.). + +Outside of unity, all you need to do is the following: + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; +SpacetimeDBClient.CreateInstance(new ConsoleLogger()); +``` + +### Interface `ISpacetimeDBLogger` + +```cs +namespace SpacetimeDB +{ + +public interface ISpacetimeDBLogger +{ + void Log(string message); + void LogError(string message); + void LogWarning(string message); + void LogException(Exception e); +} + +} +``` + +This interface provides methods that are invoked when the SpacetimeDB C# SDK needs to log at various log levels. You can create custom implementations if needed to integrate with existing logging solutions. + +### Class `ConsoleLogger` + +```cs +namespace SpacetimeDB { + +public class ConsoleLogger : ISpacetimeDBLogger {} + +} +``` + +An `ISpacetimeDBLogger` implementation for regular .NET applications, using `Console.Write` when logs are received. + +### Class `UnityDebugLogger` + +```cs +namespace SpacetimeDB { + +public class UnityDebugLogger : ISpacetimeDBLogger {} + +} +``` + +An `ISpacetimeDBLogger` implementation for Unity, using the Unity `Debug.Log` api. diff --git a/Writerside/topics/sdks/python/python_index.md b/Writerside/topics/sdks/python/python_index.md new file mode 100644 index 00000000..a87d8ac5 --- /dev/null +++ b/Writerside/topics/sdks/python/python_index.md @@ -0,0 +1,552 @@ +# The SpacetimeDB Python client SDK + +The SpacetimeDB client SDK for Python contains all the tools you need to build native clients for SpacetimeDB modules using Python. + +## Install the SDK + +Use pip to install the SDK: + +```bash +pip install spacetimedb-sdk +``` + +## Generate module bindings + +Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the Python interface files using the Spacetime CLI. From your project directory, run: + +```bash +mkdir -p module_bindings +spacetime generate --lang python \ + --out-dir module_bindings \ + --project-path PATH-TO-MODULE-DIRECTORY +``` + +Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. + +Import your bindings in your client's code: + +```python +import module_bindings +``` + +## Basic vs Async SpacetimeDB Client + +This SDK provides two different client modules for interacting with your SpacetimeDB module. + +The Basic client allows you to have control of the main loop of your application and you are responsible for regularly calling the client's `update` function. This is useful in settings like PyGame where you want to have full control of the main loop. + +The Async client has a run function that you call after you set up all your callbacks and it will take over the main loop and handle updating the client for you. With the async client, you can have a regular "tick" function by using the `schedule_event` function. + +## Common Client Reference + +The following functions and types are used in both the Basic and Async clients. + +### API at a glance + +| Definition | Description | +|---------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------| +| Type [`Identity`](#type-identity.) | A unique public identifier for a client. | +| Type [`Address`](#type-address.) | An opaque identifier for differentiating connections by the same `Identity`. | +| Type [`ReducerEvent`](#type-reducerevent.) | `class` containing information about the reducer that triggered a row update event. | +| Type [`module_bindings::{TABLE}`](#type-table.) | Autogenerated `class` type for a table, holding one row. | +| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](#method-filter_by_column.) | Autogenerated method to iterate over or seek subscribed rows where a column matches a value. | +| Method [`module_bindings::{TABLE}::iter`](#method-iter.) | Autogenerated method to iterate over all subscribed rows. | +| Method [`module_bindings::{TABLE}::register_row_update`](#method-register_row_update.) | Autogenerated method to register a callback that fires when a row changes. | +| Function [`module_bindings::{REDUCER_NAME}::{REDUCER_NAME}`](#function-reducer.) | Autogenerated function to invoke a reducer. | +| Function [`module_bindings::{REDUCER_NAME}::register_on_{REDUCER_NAME}`](#function-register_on_reducer.) | Autogenerated function to register a callback to run whenever the reducer is invoked. | + +### Type `Identity` + +```python +class Identity: + @staticmethod + def from_string(string) + + @staticmethod + def from_bytes(data) + + def __str__(self) + + def __eq__(self, other) +``` + +| Member | Args | Meaning | +| ------------- | ---------- | ------------------------------------ | +| `from_string` | `str` | Create an Identity from a hex string | +| `from_bytes` | `bytes` | Create an Identity from raw bytes | +| `__str__` | `None` | Convert the Identity to a hex string | +| `__eq__` | `Identity` | Compare two Identities for equality | + +A unique public identifier for a user of a database. + +### Type `Address` + +```python +class Address: + @staticmethod + def from_string(string) + + @staticmethod + def from_bytes(data) + + def __str__(self) + + def __eq__(self, other) +``` + +| Member | Type | Meaning | +|---------------|-----------|-------------------------------------| +| `from_string` | `str` | Create an Address from a hex string | +| `from_bytes` | `bytes` | Create an Address from raw bytes | +| `__str__` | `None` | Convert the Address to a hex string | +| `__eq__` | `Address` | Compare two Identities for equality | + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity.). + +### Type `ReducerEvent` + +```python +class ReducerEvent: + def __init__(self, caller_identity, reducer_name, status, message, args): + self.caller_identity = caller_identity + self.reducer_name = reducer_name + self.status = status + self.message = message + self.args = args +``` + +| Member | Type | Meaning | +|-------------------|---------------------|------------------------------------------------------------------------------------| +| `caller_identity` | `Identity` | The identity of the user who invoked the reducer | +| `caller_address` | `Optional[Address]` | The address of the user who invoked the reducer, or `None` for scheduled reducers. | +| `reducer_name` | `str` | The name of the reducer that was invoked | +| `status` | `str` | The status of the reducer invocation ("committed", "failed", "outofenergy") | +| `message` | `str` | The message returned by the reducer if it fails | +| `args` | `List[str]` | The arguments passed to the reducer | + +This class contains the information about a reducer event to be passed to row update callbacks. + +### Type `{TABLE}` + +```python +class TABLE: + is_table_class = True + + primary_key = "identity" + + @classmethod + def register_row_update(cls, callback: Callable[[str,TABLE,TABLE,ReducerEvent], None]) + + @classmethod + def iter(cls) -> Iterator[User] + + @classmethod + def filter_by_COLUMN_NAME(cls, COLUMN_VALUE) -> TABLE +``` + +This class is autogenerated for each table in your module. It contains methods for filtering and iterating over subscribed rows. + +### Method `filter_by_{COLUMN}` + +```python +def filter_by_COLUMN(self, COLUMN_VALUE) -> TABLE +``` + +| Argument | Type | Meaning | +| -------------- | ------------- | ---------------------- | +| `column_value` | `COLUMN_TYPE` | The value to filter by | + +For each column of a table, `spacetime generate` generates a `classmethod` on the [table class](#type-table.) to filter or seek 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 depends on the column's attributes: + +- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filter_by` method returns a `{TABLE}` or None, where `{TABLE}` is the [table struct](#type-table.). +- For non-unique columns, the `filter_by` method returns an `Iterator` that can be used in a `for` loop. + +### Method `iter` + +```python +def iter(self) -> Iterator[TABLE] +``` + +Iterate over all the subscribed rows in the table. + +### Method `register_row_update` + +```python +def register_row_update(self, callback: Callable[[str,TABLE,TABLE,ReducerEvent], None]) +``` + +| Argument | Type | Meaning | +| ---------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `callback` | `Callable[[str,TABLE,TABLE,ReducerEvent]` | Callback to be invoked when a row is updated (Args: row_op, old_value, new_value, reducer_event) | + +Register a callback function to be executed when a row is updated. Callback arguments are: + +- `row_op`: The type of row update event. One of `"insert"`, `"delete"`, or `"update"`. +- `old_value`: The previous value of the row, `None` if the row was inserted. +- `new_value`: The new value of the row, `None` if the row was deleted. +- `reducer_event`: The [`ReducerEvent`](#type-reducerevent.) that caused the row update, or `None` if the row was updated as a result of a subscription change. + +### Function `{REDUCER_NAME}` + +```python +def {REDUCER_NAME}(arg1, arg2) +``` + +This function is autogenerated for each reducer in your module. It is used to invoke the reducer. The arguments match the arguments defined in the reducer's `#[reducer]` attribute. + +### Function `register_on_{REDUCER_NAME}` + +```python +def register_on_{REDUCER_NAME}(callback: Callable[[Identity, Optional[Address], str, str, ARG1_TYPE, ARG1_TYPE], None]) +``` + +| Argument | Type | Meaning | +| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| `callback` | `Callable[[Identity, str, str, ARG1_TYPE, ARG1_TYPE], None]` | Callback to be invoked when the reducer is invoked (Args: caller_identity, status, message, args) | + +Register a callback function to be executed when the reducer is invoked. Callback arguments are: + +- `caller_identity`: The identity of the user who invoked the reducer. +- `caller_address`: The address of the user who invoked the reducer, or `None` for scheduled reducers. +- `status`: The status of the reducer invocation ("committed", "failed", "outofenergy"). +- `message`: The message returned by the reducer if it fails. +- `args`: Variable number of arguments passed to the reducer. + +## Async Client Reference + +### API at a glance + +| Definition | Description | +| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| Function [`SpacetimeDBAsyncClient::run`](#function-run.) | Run the client. This function will not return until the client is closed. | +| Function [`SpacetimeDBAsyncClient::subscribe`](#function-subscribe.) | Subscribe to receive data and transaction updates for the provided queries. | +| Function [`SpacetimeDBAsyncClient::register_on_subscription_applied`](#function-register_on_subscription_applied.) | Register a callback when the local cache is updated as a result of a change to the subscription queries. | +| Function [`SpacetimeDBAsyncClient::force_close`](#function-force_close.) | Signal the client to stop processing events and close the connection to the server. | +| Function [`SpacetimeDBAsyncClient::schedule_event`](#function-schedule_event.) | Schedule an event to be fired after a delay | + +### Function `run` + +```python +async def run( + self, + auth_token, + host, + address_or_name, + ssl_enabled, + on_connect, + subscription_queries=[], + ) +``` + +Run the client. This function will not return until the client is closed. + +| Argument | Type | Meaning | +| ---------------------- | --------------------------------- | -------------------------------------------------------------- | +| `auth_token` | `str` | Auth token to authenticate the user. (None if new user) | +| `host` | `str` | Hostname of SpacetimeDB server | +| `address_or_name` | `&str` | Name or address of the module. | +| `ssl_enabled` | `bool` | Whether to use SSL when connecting to the server. | +| `on_connect` | `Callable[[str, Identity], None]` | Callback to be invoked when the client connects to the server. | +| `subscription_queries` | `List[str]` | List of queries to subscribe to. | + +If `auth_token` is not None, they will be passed to the new connection to identify and authenticate the user. Otherwise, a new Identity and auth token will be generated by the server. An optional [local_config](#local_config.) module can be used to store the user's auth token to local storage. + +If you are connecting to SpacetimeDB Cloud `testnet` the host should be `testnet.spacetimedb.com` and `ssl_enabled` should be `True`. If you are connecting to SpacetimeDB Standalone locally, the host should be `localhost:3000` and `ssl_enabled` should be `False`. For instructions on how to deploy to these environments, see the [Deployment Section](testnet.) + +```python +asyncio.run( + spacetime_client.run( + AUTH_TOKEN, + "localhost:3000", + "my-module-name", + False, + on_connect, + ["SELECT * FROM User", "SELECT * FROM Message"], + ) +) +``` + +### Function `subscribe` + +```rust +def subscribe(self, queries: List[str]) +``` + +Subscribe to a set of queries, to be notified when rows which match those queries are altered. + +| Argument | Type | Meaning | +| --------- | ----------- | ---------------------------- | +| `queries` | `List[str]` | SQL queries to subscribe to. | + +The `queries` should be a slice of strings representing SQL queries. + +A new call to `subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache. Row update events will be dispatched for any inserts and deletes that occur as a result of the new queries. For these events, the [`ReducerEvent`](#type-reducerevent.) argument will be `None`. + +This should be called before the async client is started with [`run`](#function-run.). + +```python +spacetime_client.subscribe(["SELECT * FROM User;", "SELECT * FROM Message;"]) +``` + +Subscribe to a set of queries, to be notified when rows which match those queries are altered. + +### Function `register_on_subscription_applied` + +```python +def register_on_subscription_applied(self, callback) +``` + +Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. + +| Argument | Type | Meaning | +| ---------- | -------------------- | ------------------------------------------------------ | +| `callback` | `Callable[[], None]` | Callback to be invoked when subscriptions are applied. | + +The callback will be invoked after a successful [`subscribe`](#function-subscribe.) call when the initial set of matching rows becomes available. + +```python +spacetime_client.register_on_subscription_applied(on_subscription_applied) +``` + +### Function `force_close` + +```python +def force_close(self) +) +``` + +Signal the client to stop processing events and close the connection to the server. + +```python +spacetime_client.force_close() +``` + +### Function `schedule_event` + +```python +def schedule_event(self, delay_secs, callback, *args) +``` + +Schedule an event to be fired after a delay + +To create a repeating event, call schedule_event() again from within the callback function. + +| Argument | Type | Meaning | +| ------------ | -------------------- | -------------------------------------------------------------- | +| `delay_secs` | `float` | number of seconds to wait before firing the event | +| `callback` | `Callable[[], None]` | Callback to be invoked when the event fires. | +| `args` | `*args` | Variable number of arguments to pass to the callback function. | + +```python +def application_tick(): + # ... do some work + + spacetime_client.schedule_event(0.1, application_tick) + +spacetime_client.schedule_event(0.1, application_tick) +``` + +## Basic Client Reference + +### API at a glance + +| Definition | Description | +|------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| Function [`SpacetimeDBClient::init`](#function-init.) | Create a network manager instance. | +| Function [`SpacetimeDBClient::subscribe`](#function-subscribe.) | Subscribe to receive data and transaction updates for the provided queries. | +| Function [`SpacetimeDBClient::register_on_event`](#function-register_on_event.) | Register a callback function to handle transaction update events. | +| Function [`SpacetimeDBClient::unregister_on_event`](#function-unregister_on_event.) | Unregister a callback function that was previously registered using `register_on_event`. | +| Function [`SpacetimeDBClient::register_on_subscription_applied`](#function-register_on_subscription_applied.) | Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. | +| Function [`SpacetimeDBClient::unregister_on_subscription_applied`](#function-unregister_on_subscription_applied.) | Unregister a callback function from the subscription update event. | +| Function [`SpacetimeDBClient::update`](#function-update.) | Process all pending incoming messages from the SpacetimeDB module. | +| Function [`SpacetimeDBClient::close`](#function-close.) | Close the WebSocket connection. | +| Type [`TransactionUpdateMessage`](#type-transactionupdatemessage.) | Represents a transaction update message. | + +### Function `init` + +```python +@classmethod +def init( + auth_token: str, + host: str, + address_or_name: str, + ssl_enabled: bool, + autogen_package: module, + on_connect: Callable[[], NoneType] = None, + on_disconnect: Callable[[str], NoneType] = None, + on_identity: Callable[[str, Identity, Address], NoneType] = None, + on_error: Callable[[str], NoneType] = None +) +``` + +Create a network manager instance. + +| Argument | Type | Meaning | +|-------------------|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `auth_token` | `str` | This is the token generated by SpacetimeDB that matches the user's identity. If None, token will be generated | +| `host` | `str` | Hostname:port for SpacetimeDB connection | +| `address_or_name` | `str` | The name or address of the database to connect to | +| `ssl_enabled` | `bool` | Whether to use SSL when connecting to the server. | +| `autogen_package` | `ModuleType` | Python package where SpacetimeDB module generated files are located. | +| `on_connect` | `Callable[[], None]` | Optional callback called when a connection is made to the SpacetimeDB module. | +| `on_disconnect` | `Callable[[str], None]` | Optional callback called when the Python client is disconnected from the SpacetimeDB module. The argument is the close message. | +| `on_identity` | `Callable[[str, Identity, Address], None]` | Called when the user identity is recieved from SpacetimeDB. First argument is the auth token used to login in future sessions. Third argument is the client connection's [`Address`](#type-address.). | +| `on_error` | `Callable[[str], None]` | Optional callback called when the Python client connection encounters an error. The argument is the error message. | + +This function creates a new SpacetimeDBClient instance. It should be called before any other functions in the SpacetimeDBClient class. This init will call connect for you. + +```python +SpacetimeDBClient.init(autogen, on_connect=self.on_connect) +``` + +### Function `subscribe` + +```python +def subscribe(queries: List[str]) +``` + +Subscribe to receive data and transaction updates for the provided queries. + +| Argument | Type | Meaning | +| --------- | ----------- | -------------------------------------------------------------------------------------------------------- | +| `queries` | `List[str]` | A list of queries to subscribe to. Each query is a string representing an sql formatted query statement. | + +This function sends a subscription request to the SpacetimeDB module, indicating that the client wants to receive data and transaction updates related to the specified queries. + +```python +queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] +SpacetimeDBClient.instance.subscribe(queries) +``` + +### Function `register_on_event` + +```python +def register_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) +``` + +Register a callback function to handle transaction update events. + +| Argument | Type | Meaning | +| ---------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `callback` | `Callable[[TransactionUpdateMessage], None]` | A callback function that takes a single argument of type `TransactionUpdateMessage`. This function will be invoked with a `TransactionUpdateMessage` instance containing information about the transaction update event. | + +This function registers a callback function that will be called when a reducer modifies a table matching any of the subscribed queries or if a reducer called by this Python client encounters a failure. + +```python +def handle_event(transaction_update): + # Code to handle the transaction update event + +SpacetimeDBClient.instance.register_on_event(handle_event) +``` + +### Function `unregister_on_event` + +```python +def unregister_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) +``` + +Unregister a callback function that was previously registered using `register_on_event`. + +| Argument | Type | Meaning | +| ---------- | -------------------------------------------- | ------------------------------------ | +| `callback` | `Callable[[TransactionUpdateMessage], None]` | The callback function to unregister. | + +```python +SpacetimeDBClient.instance.unregister_on_event(handle_event) +``` + +### Function `register_on_subscription_applied` + +```python +def register_on_subscription_applied(callback: Callable[[], NoneType]) +``` + +Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. + +| Argument | Type | Meaning | +| ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `callback` | `Callable[[], None]` | A callback function that will be invoked on each subscription update. The callback function should not accept any arguments and should not return any value. | + +```python +def subscription_callback(): + # Code to be executed on each subscription update + +SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) +``` + +### Function `unregister_on_subscription_applied` + +```python +def unregister_on_subscription_applied(callback: Callable[[], NoneType]) +``` + +Unregister a callback function from the subscription update event. + +| Argument | Type | Meaning | +| ---------- | -------------------- | -------------------------------------------------------------------------------------------------------- | +| `callback` | `Callable[[], None]` | A callback function that was previously registered with the `register_on_subscription_applied` function. | + +```python +def subscription_callback(): + # Code to be executed on each subscription update + +SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) +``` + +### Function `update` + +```python +def update() +``` + +Process all pending incoming messages from the SpacetimeDB module. + +This function must be called on a regular interval in the main loop to process incoming messages. + +```python +while True: + SpacetimeDBClient.instance.update() # Call the update function in a loop to process incoming messages + # Additional logic or code can be added here +``` + +### Function `close` + +```python +def close() +``` + +Close the WebSocket connection. + +This function closes the WebSocket connection to the SpacetimeDB module. + +```python +SpacetimeDBClient.instance.close() +``` + +### Type `TransactionUpdateMessage` + +```python +class TransactionUpdateMessage: + def __init__( + self, + caller_identity: Identity, + status: str, + message: str, + reducer_name: str, + args: Dict + ) +``` + +| Member | Args | Meaning | +| ----------------- | ---------- | ------------------------------------------------- | +| `caller_identity` | `Identity` | The identity of the caller. | +| `status` | `str` | The status of the transaction. | +| `message` | `str` | A message associated with the transaction update. | +| `reducer_name` | `str` | The reducer used for the transaction. | +| `args` | `Dict` | Additional arguments for the transaction. | + +Represents a transaction update message. Used in on_event callbacks. + +For more details, see [`register_on_event`](#function-register_on_event.). diff --git a/Writerside/topics/sdks/python/python_quickstart.md b/Writerside/topics/sdks/python/python_quickstart.md new file mode 100644 index 00000000..fe6dbc22 --- /dev/null +++ b/Writerside/topics/sdks/python/python_quickstart.md @@ -0,0 +1,379 @@ +# Python Client SDK Quick Start + +In this guide, we'll show you how to get up and running with a simple SpacetimDB app with a client written in Python. + +We'll implement a command-line client for the module created in our [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart6.) guides. Make sure you follow one of these guides before you start on this one. + +## Install the SpacetimeDB SDK Python Package + +1. Run pip install + +```bash +pip install spacetimedb_sdk +``` + +## Project structure + +Enter the directory `quickstart-chat` you created in the Rust or C# Module Quickstart guides and create a `client` folder: + +```bash +cd quickstart-chat +mkdir client +``` + +## Create the Python main file + +Create a file called `main.py` in the `client` and open it in your favorite editor. We prefer [VS Code](https://code.visualstudio.com/). + +## Add imports + +We need to add several imports for this quickstart: + +- [`asyncio`](https://docs.python.org/3/library/asyncio.html) is required to run the async code in the SDK. +- [`multiprocessing.Queue`](https://docs.python.org/3/library/multiprocessing.html) allows us to pass our input to the async code, which we will run in a separate thread. +- [`threading`](https://docs.python.org/3/library/threading.html) allows us to spawn our async code in a separate thread so the main thread can run the input loop. + +- `spacetimedb_sdk.spacetimedb_async_client.SpacetimeDBAsyncClient` is the async wrapper around the SpacetimeDB client which we use to interact with our SpacetimeDB module. +- `spacetimedb_sdk.local_config` is an optional helper module to load the auth token from local storage. + +```python +import asyncio +from multiprocessing import Queue +import threading + +from spacetimedb_sdk.spacetimedb_async_client import SpacetimeDBAsyncClient +import spacetimedb_sdk.local_config as local_config +``` + +## Generate your module types + +The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. + +In your `client` directory, run: + +```bash +mkdir -p module_bindings +spacetime generate --lang python --out-dir module_bindings --project-path ../server +``` + +Take a look inside `client/module_bindings`. The CLI should have generated five files: + +``` +module_bindings ++-- message.py ++-- send_message_reducer.py ++-- set_name_reducer.py ++-- user.py +``` + +Now we import these types by adding the following lines to `main.py`: + +```python +import module_bindings +from module_bindings.user import User +from module_bindings.message import Message +import module_bindings.send_message_reducer as send_message_reducer +import module_bindings.set_name_reducer as set_name_reducer +``` + +## Global variables + +Next we will add our global `input_queue` and `local_identity` variables which we will explain later when they are used. + +```python +input_queue = Queue() +local_identity = None +``` + +## Define 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 four things: + +1. Init the optional local config module. The first parameter is the directory name to be created in the user home directory. +1. Create our async SpacetimeDB client. +1. Register our callbacks. +1. Start the async client in a thread. +1. Run a loop to read user input and send it to a repeating event in the async client. +1. When the user exits, stop the async client and exit the program. + +```python +if __name__ == "__main__": + local_config.init(".spacetimedb-python-quickstart") + + spacetime_client = SpacetimeDBAsyncClient(module_bindings) + + register_callbacks(spacetime_client) + + thread = threading.Thread(target=run_client, args=(spacetime_client,)) + thread.start() + + input_loop() + + spacetime_client.force_close() + thread.join() +``` + +## Register callbacks + +We need to handle several sorts of events: + +1. OnSubscriptionApplied is a special callback that is executed when the local client cache is populated. We will talk more about this later. +2. When a new user joins or a user is updated, we'll print an appropriate message. +3. When we receive a new message, we'll print it. +4. If the server rejects our attempt to set our name, we'll print an error. +5. If the server rejects a message we send, we'll print an error. +6. We use the `schedule_event` function to register a callback to be executed after 100ms. This callback will check the input queue for any user input and execute the appropriate command. + +Because python requires functions to be defined before they're used, the following code must be added to `main.py` before main block: + +```python +def register_callbacks(spacetime_client): + spacetime_client.client.register_on_subscription_applied(on_subscription_applied) + + User.register_row_update(on_user_row_update) + Message.register_row_update(on_message_row_update) + + set_name_reducer.register_on_set_name(on_set_name_reducer) + send_message_reducer.register_on_send_message(on_send_message_reducer) + + spacetime_client.schedule_event(0.1, check_commands) +``` + +### Handling User row updates + +For each table, we can register a row update callback to be run whenever a subscribed row is inserted, updated or deleted. We register these callbacks using the `register_row_update` methods that are generated automatically 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::row_update` callbacks may be invoked with users who are offline. We'll only notify about online users. + +We are also going to check for updates to the user row. This can happen for three reasons: + +1. They've set their name using the `set_name` reducer. +2. They're an existing user re-connecting, so their `online` has been set to `true`. +3. They've disconnected, so their `online` has been set to `false`. + +We'll print an appropriate message in each of these cases. + +`row_update` callbacks take four arguments: the row operation ("insert", "update", or "delete"), the old row if it existed, the new or updated row, and a `ReducerEvent`. This will `None` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an class that contains information about the reducer that triggered this row update event. + +Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `user_name_or_identity` handle this. + +Add these functions before the `register_callbacks` function: + +```python +def user_name_or_identity(user): + if user.name: + return user.name + else: + return (str(user.identity))[:8] + +def on_user_row_update(row_op, user_old, user, reducer_event): + if row_op == "insert": + if user.online: + print(f"User {user_name_or_identity(user)} connected.") + elif row_op == "update": + if user_old.online and not user.online: + print(f"User {user_name_or_identity(user)} disconnected.") + elif not user_old.online and user.online: + print(f"User {user_name_or_identity(user)} connected.") + + if user_old.name != user.name: + print( + f"User {user_name_or_identity(user_old)} renamed to {user_name_or_identity(user)}." + ) +``` + +### 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 `on_message_row_update` callback will check if its `reducer_event` argument is not `None`, and only print in that case. + +To find the `User` based on the message's `sender` identity, we'll use `User::filter_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filter_by_identity` accepts a `bytes`, rather than an `&Identity`. The `sender` identity stored in the message is also a `bytes`, not an `Identity`, so we can just pass it to the filter method. + +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`. + +Add these functions before the `register_callbacks` function: + +```python +def on_message_row_update(row_op, message_old, message, reducer_event): + if reducer_event is not None and row_op == "insert": + print_message(message) + +def print_message(message): + user = User.filter_by_identity(message.sender) + user_name = "unknown" + if user is not None: + user_name = user_name_or_identity(user) + + print(f"{user_name}: {message.text}") +``` + +### Warn if our name was rejected + +We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `register_on_` method, which is automatically implemented for each reducer by `spacetime generate`. + +Each reducer callback takes four fixed arguments: + +1. The `Identity` of the client who requested the reducer invocation. +2. The `Address` of the client who requested the reducer invocation, or `None` for scheduled reducers. +3. The `Status` of the reducer run, one of `committed`, `failed` or `outofenergy`. +4. The `Message` returned by the reducer in error cases, or `None` if the reducer succeeded. + +It also takes a variable number of arguments which match the calling arguments of the reducer. + +These callbacks will be invoked in one of two cases: + +1. If the reducer was successful and altered any of our subscribed rows. +2. If we requested an invocation which failed. + +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 `on_set_name_reducer` as a callback which checks if the reducer failed, and if it did, prints an error message including the rejected name. + +We'll test both that our identity matches the sender and that the status is `failed`, even though the latter implies the former, for demonstration purposes. + +Add this function before the `register_callbacks` function: + +```python +def on_set_name_reducer(sender_id, sender_address, status, message, name): + if sender_id == local_identity: + if status == "failed": + print(f"Failed to set name: {message}") +``` + +### 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. + +Add this function before the `register_callbacks` function: + +```python +def on_send_message_reducer(sender_id, sender_address, status, message, msg): + if sender_id == local_identity: + if status == "failed": + print(f"Failed to send message: {message}") +``` + +### OnSubscriptionApplied callback + +This callback fires after the client cache is updated as a result in a change to the client subscription. This happens after connect and if after calling `subscribe` to modify the subscription. + +In this case, we want to print all the existing messages when the subscription is applied. `print_messages_in_order` iterates over all the `Message`s we've received, sorts them, and then prints them. `Message.iter()` is generated for all table types, and returns an iterator over all the messages in the client's cache. + +Add these functions before the `register_callbacks` function: + +```python +def print_messages_in_order(): + all_messages = sorted(Message.iter(), key=lambda x: x.sent) + for entry in all_messages: + print(f"{user_name_or_identity(User.filter_by_identity(entry.sender))}: {entry.text}") + +def on_subscription_applied(): + print(f"\nSYSTEM: Connected.") + print_messages_in_order() +``` + +### Check commands repeating event + +We'll use a repeating event to check the user input queue every 100ms. If there's a command in the queue, we'll execute it. If not, we'll just keep waiting. Notice that at the end of the function we call `schedule_event` again to so the event will repeat. + +If the command is to send a message, we'll call the `send_message` reducer. If the command is to set our name, we'll call the `set_name` reducer. + +Add these functions before the `register_callbacks` function: + +```python +def check_commands(): + global input_queue + + if not input_queue.empty(): + choice = input_queue.get() + if choice[0] == "name": + set_name_reducer.set_name(choice[1]) + else: + send_message_reducer.send_message(choice[1]) + + spacetime_client.schedule_event(0.1, check_commands) +``` + +### OnConnect callback + +This callback fires after the client connects to the server. We'll use it to save our credentials to a file so that we can re-authenticate as the same user next time we connect. + +The `on_connect` callback takes three arguments: + +1. The `Auth Token` is the equivalent of your private key. This is the only way to authenticate with the SpacetimeDB module as this user. +2. The `Identity` is the equivalent of your public key. This is used to uniquely identify this user and will be sent to other clients. We store this in a global variable so we can use it to identify that a given message or transaction was sent by us. +3. The `Address` is an opaque identifier 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. + +To store our auth token, we use the optional component `local_config`, which provides a simple interface for storing and retrieving a single `Identity` from a file. We'll use the `local_config::set_string` method to store the auth token. Other projects might want to associate this token with some other identifier such as an email address or Steam ID. + +The `on_connect` callback is passed to the client connect function so it just needs to be defined before the `run_client` described next. + +```python +def on_connect(auth_token, identity): + global local_identity + local_identity = identity + + local_config.set_string("auth_token", auth_token) +``` + +## Async client thread + +We are going to write a function that starts the async client, which will be executed on a separate thread. + +```python +def run_client(spacetime_client): + asyncio.run( + spacetime_client.run( + local_config.get_string("auth_token"), + "localhost:3000", + "chat", + False, + on_connect, + ["SELECT * FROM User", "SELECT * FROM Message"], + ) + ) +``` + +## Input loop + +Finally, we need a function to be executed on the main loop which listens for user input and adds it to the queue. + +```python +def input_loop(): + global input_queue + + while True: + user_input = input() + if len(user_input) == 0: + return + elif user_input.startswith("/name "): + input_queue.put(("name", user_input[6:])) + else: + input_queue.put(("message", user_input)) +``` + +## Run the client + +Make sure your module from the Rust or C# module quickstart is published. If you used a different module name than `chat`, you will need to update the `connect` call in the `run_client` function. + +Run the client: + +```bash +python main.py +``` + +If you want to connect another client, you can use the --client command line option, which is built into the local_config module. This will create different settings file for the new client's auth token. + +```bash +python main.py --client 2 +``` + +## Next steps + +Congratulations! You've built a simple chat app with a Python client. You can now use this as a starting point for your own SpacetimeDB apps. + +For a more complex example of the Spacetime Python SDK, check out our [AI Agent](https://github.com/clockworklabs/spacetime-mud/tree/main/ai-agent-python-client) for the [Spacetime Multi-User Dungeon](https://github.com/clockworklabs/spacetime-mud). The AI Agent uses the OpenAI API to create dynamic content on command. diff --git a/Writerside/topics/sdks/rust/sdks_rust_index.md b/Writerside/topics/sdks/rust/sdks_rust_index.md new file mode 100644 index 00000000..239cabff --- /dev/null +++ b/Writerside/topics/sdks/rust/sdks_rust_index.md @@ -0,0 +1,1183 @@ +# The SpacetimeDB Rust client SDK + +The SpacetimeDB client SDK for Rust contains all the tools you need to build native clients for SpacetimeDB modules using Rust. + +## Install the SDK + +First, create a new project using `cargo new` and add the SpacetimeDB SDK to your dependencies: + +```bash +cargo add spacetimedb +``` + +## Generate module bindings + +Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's `src` directory and generate the Rust interface files using the Spacetime CLI. From your project directory, run: + +```bash +mkdir -p src/module_bindings +spacetime generate --lang rust \ + --out-dir src/module_bindings \ + --project-path PATH-TO-MODULE-DIRECTORY +``` + +Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. + +Declare a `mod` for the bindings in your client's `src/main.rs`: + +```rust +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`](rust_#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`](rust_#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`](rust_#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`](rust_#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}`](rust_#type-table.) | Autogenerated `struct` type for a table, holding one row. | +| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](rust_#method-filter_by_column.) | Autogenerated method to iterate over or seek subscribed rows where a 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`](rust_#method-iter.) | Iterate over all subscribed rows. | +| Method [`spacetimedb_sdk::table::TableType::filter`](rust_#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`](rust_#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}`](rust_#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` + +```rust +module_bindings::connect( + spacetimedb_uri: impl TryInto, + db_name: &str, + credentials: Option, +) -> anyhow::Result<()> +``` + +Connect to a database named `db_name` accessible over the internet at the URI `spacetimedb_uri`. + +| 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` + +```rust +spacetimedb_sdk::disconnect() +``` + +Gracefully close the current WebSocket connection. + +If there is no active connection, this operation does nothing. + +```rust +connect(SPACETIMEDB_URI, MODULE_NAME, credentials) + .expect("Connection failed"); + +run_app(); + +disconnect(); +``` + +### 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!" +``` + +### Function `once_on_disconnect` + +```rust +spacetimedb_sdk::once_on_disconnect( + callback: impl FnOnce() + Send + 'static, +) -> DisconnectCallbackId +``` + +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. + +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. + +```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. +``` + +### Function `remove_on_disconnect` + +```rust +spacetimedb_sdk::remove_on_disconnect( + id: DisconnectCallbackId, +) +``` + +Unregister a previously-registered [`on_disconnect`](#function-on_disconnect.) callback. + +| Argument | Type | Meaning | +| -------- | ---------------------- | ------------------------------------------ | +| `id` | `DisconnectCallbackId` | 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_disconnect(|| unreachable!()); + +remove_on_disconnect(id); + +disconnect(); + +// No `unreachable` panic. +``` + +## Subscribe to queries + +### Function `subscribe` + +```rust +spacetimedb_sdk::subscribe(queries: &[&str]) -> anyhow::Result<()> +``` + +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}`](rust_#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. + +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. + +```rust +subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) + .expect("Called `subscribe` before `connect`"); +``` + +### Function `subscribe_owned` + +```rust +spacetimedb_sdk::subscribe_owned(queries: Vec) -> 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. + +A new call to `subscribe_owned` (or [`subscribe`](rust_#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. + +`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. + +```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` + +```rust +spacetimedb_sdk::on_subscription_applied( + callback: impl FnMut() + Send + 'static, +) -> SubscriptionCallbackId +``` + +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`](rust_#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. +``` + +### Function `once_on_subscription_applied` + +```rust +spacetimedb_sdk::once_on_subscription_applied( + callback: impl FnOnce() + Send + 'static, +) -> SubscriptionCallbackId +``` + +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`](rust_#function-subscribe.) or [`subscribe_owned`](#function-subscribe_owned.) call when the initial set of matching rows becomes available. + +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. + +```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. +``` + +### 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)); + +// Will print "Subscription applied!" + +remove_on_subscription_applied(id); + +subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) + .expect("Called `subscribe` before `connect`"); + +// Nothing printed this time. +``` + +## Identify a client + +### Type `Identity` + +```rust +spacetimedb_sdk::identity::Identity +``` + +A unique public identifier for a client connected to a database. + +### Type `Token` + +```rust +spacetimedb_sdk::identity::Token +``` + +A private access token for a client connected to a database. + +### Type `Credentials` + +```rust +spacetimedb_sdk::identity::Credentials +``` + +Credentials, including a private access token, sufficient to authenticate a client connected to a database. + +| Field | Type | +| ---------- | ---------------------------- | +| `identity` | [`Identity`](rust_#type-identity.) | +| `token` | [`Token`](#type-token.) | + +### Type `Address` + +```rust +spacetimedb_sdk::Address +``` + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](rust_#type-identity.). + +### Function `identity` + +```rust +spacetimedb_sdk::identity::identity() -> Result +``` + +Read the current connection's public [`Identity`](rust_#type-identity.). + +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 identity is {:?}", identity()); + +// Prints "My identity is Ok(Identity { bytes: [...several u8s...] })" +``` + +### Function `token` + +```rust +spacetimedb_sdk::identity::token() -> Result +``` + +Read the current connection's private [`Token`](#type-token.). + +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` + +```rust +spacetimedb_sdk::identity::credentials() -> Result +``` + +Read the current connection's [`Credentials`](#type-credentials.), including a public [`Identity`](rust_#type-identity.) and a private [`Token`](#type-token.). + +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 credentials are {:?}", credentials()); + +// Prints "My credentials are Ok(Credentials { +// identity: Identity { bytes: [...several u8s...] }, +// token: Token { string: "...several Base64 digits..."}, +// })" +``` + +### Function `address` + +```rust +spacetimedb_sdk::identity::address() -> Result
+``` + +Read the current connection's [`Address`](rust_#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()); +``` + +### Function `on_connect` + +```rust +spacetimedb_sdk::identity::on_connect( + callback: impl FnMut(&Credentials, Address) + Send + 'static, +) -> ConnectCallbackId +``` + +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`](rust_#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. + +The returned `ConnectCallbackId` can be passed to [`remove_on_connect`](#function-remove_on_connect.) to unregister the callback. + +```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`. +``` + +### Function `once_on_connect` + +```rust +spacetimedb_sdk::identity::once_on_connect( + callback: impl FnOnce(&Credentials, Address) + Send + 'static, +) -> ConnectCallbackId +``` + +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`](rust_#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. + +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` + +```rust +spacetimedb_sdk::identity::remove_on_connect(id: ConnectCallbackId) +``` + +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. | + +If `id` does not refer to a currently-registered callback, this operation does nothing. + +```rust +let id = on_connect(|_creds, _addr| unreachable!()); + +remove_on_connect(id); + +connect(SPACETIMEDB_URI, DB_NAME, None) + .expect("Failed to connect"); + +sleep(Duration::from_secs(1)); + +// No `unreachable` panic. +``` + +### Function `load_credentials` + +```rust +spacetimedb_sdk::identity::load_credentials( + dirname: &str, +) -> Result> +``` + +Load a saved [`Credentials`](#type-credentials.) from a file within `~/dirname`, if one exists. + +| Argument | Type | Meaning | +| --------- | ------ | ----------------------------------------------------- | +| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | + +`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. + +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.). + +```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"); +``` + +### Function `save_credentials` + +```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.). + +| Argument | Type | Meaning | +| ------------- | -------------- | ----------------------------------------------------- | +| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | +| `credentials` | `&Credentials` | [`Credentials`](#type-credentials.) to store. | + +`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. + +Returns `Err` when IO or serialization fails. + +```rust +const CREDENTIALS_DIR = ".my-module"; + +let creds = load_credentials(CREDENTIALS_DIRectory) + .expect("Error while loading credentials"); + +on_connect(|creds, _addr| { + if let Err(e) = save_credentials(CREDENTIALS_DIR, creds) { + eprintln!("Error while saving credentials: {:?}", e); + } +}); + +connect(SPACETIMEDB_URI, DB_NAME, creds) + .expect("Failed to connect"); +``` + +## View subscribed rows of tables + +### Type `{TABLE}` + +```rust +module_bindings::{TABLE} +``` + +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}` + +```rust +module_bindings::{TABLE}::filter_by_{COLUMN}( + value: {COLUMN_TYPE}, +) -> {FILTER_RESULT}<{TABLE}> +``` + +For each column of a table, `spacetime generate` generates a static method on the [table struct](rust_#type-table.) to filter or seek 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 depends on the column's attributes: + +- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filter_by` method returns an `Option<{TABLE}>`, where `{TABLE}` is the [table struct](rust_#type-table.). +- For non-unique columns, the `filter_by` method returns an `impl Iterator`. + +### Trait `TableType` + +```rust +spacetimedb_sdk::table::TableType +``` + +Every [generated table struct](rust_#type-table.) implements the trait `TableType`. + +#### Method `count` + +```rust +TableType::count() -> usize +``` + +Return the number of subscribed rows in the table, or 0 if there is no active connection. + +This method acquires a global lock. + +```rust +connect(SPACETIMEDB_URI, DB_NAME, None) + .expect("Failed to connect"); + +on_subscription_applied(|| println!("There are {} users", User::count())); + +subscribe(&["SELECT * FROM User;"]) + .unwrap(); + +sleep(Duration::from_secs(1)); + +// Will the number of `User` rows in the database. +``` + +#### Method `iter` + +```rust +TableType::iter() -> impl Iterator +``` + +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`](rust_#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. + +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](rust_#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` + +```rust +TableType::find( + predicate: impl FnMut(&Self) -> bool, +) -> Option +``` + +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 `filter_by_{COLUMN}` methods](rust_#method-filter_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. +``` + +#### Method `on_insert` + +```rust +TableType::on_insert( + callback: impl FnMut(&Self, Option<&ReducerEvent>) + Send + 'static, +) -> InsertCallbackId +``` + +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`](rust_#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(); + +sleep(Duration::from_secs(1)); + +// Will print a note whenever a new `User` row is inserted. +``` + +#### Method `remove_on_insert` + +```rust +TableType::remove_on_insert(id: InsertCallbackId) +``` + +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. | + +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_insert(|_, _| unreachable!()); + +User::remove_on_insert(id); + +subscribe(&["SELECT * FROM User;"]) + .unwrap(); + +sleep(Duration::from_secs(1)); + +// No `unreachable` panic. +``` + +#### Method `on_delete` + +```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: + +- `row: &Self`, the previously-present row which is no longer resident in the database. +- `reducer_event: Option<&ReducerEvent>`, the [`ReducerEvent`](rust_#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. + +```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)); + +// 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" }" +``` + +#### 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. +``` + +### Trait `TableWithPrimaryKey` + +```rust +spacetimedb_sdk::table::TableWithPrimaryKey +``` + +[Generated table structs](rust_#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`](rust_#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) +``` + +| 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. + +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_update(|_, _, _| unreachable!); + +User::remove_on_update(id); + +subscribe(&["SELECT * FROM User;"]) + .unwrap(); + +// No `unreachable` panic. +``` + +## Observe and request reducer invocations + +### Type `ReducerEvent` + +```rust +module_bindings::ReducerEvent +``` + +`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.). + +[`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. + +### Type `{REDUCER}Args` + +```rust +module_bindings::{REDUCER}Args +``` + +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`. + +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...}) +``` + +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`](rust_#type-identity.) of the client which invoked the reducer. +- `caller_address: Option
`, the [`Address`](rust_#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. + +### Function `remove_on_{REDUCER}` + +```rust +module_bindings::remove_on_{REDUCER}(id: ReducerCallbackId<{REDUCER}Args>) +``` + +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. + +### Type `Status` + +```rust +spacetimedb_sdk::reducer::Status +``` + +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. diff --git a/Writerside/topics/sdks/rust/sdks_rust_quickstart.md b/Writerside/topics/sdks/rust/sdks_rust_quickstart.md new file mode 100644 index 00000000..f6049bf5 --- /dev/null +++ b/Writerside/topics/sdks/rust/sdks_rust_quickstart.md @@ -0,0 +1,487 @@ +# Rust Client SDK Quick Start + +In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in Rust. + +We'll implement a command-line client for the module created in our Rust or C# Module Quickstart guides. Make sure you follow one of these guides before you start on this one. + +## Project structure + +Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: + +```bash +cd quickstart-chat +``` + +Within it, create a `client` crate, our client application, which users run locally: + +```bash +cargo new client +``` + +## Depend on `spacetimedb-sdk` and `hex` + +`client/Cargo.toml` should be initialized without any dependencies. We'll need two: + +- [`spacetimedb-sdk`](https://crates.io/crates/spacetimedb-sdk), which defines client-side interfaces for interacting with a remote SpacetimeDB module. +- [`hex`](https://crates.io/crates/hex), which we'll use to print unnamed users' identities as hexadecimal strings. + +Below the `[dependencies]` line in `client/Cargo.toml`, add: + +```toml +spacetimedb-sdk = "0.7" +hex = "0.4" +``` + +Make sure you depend on the same version of `spacetimedb-sdk` as is reported by the SpacetimeDB CLI tool's `spacetime version`! + +## Clear `client/src/main.rs` + +`client/src/main.rs` should be initialized with a trivial "Hello world" program. Clear it out so we can write our chat client. + +In your `quickstart-chat` directory, run: + +```bash +rm client/src/main.rs +touch client/src/main.rs +``` + +## Generate your module types + +The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. + +In your `quickstart-chat` directory, run: + +```bash +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: + +``` +module_bindings +├── message.rs +├── mod.rs +├── send_message_reducer.rs +├── set_name_reducer.rs +└── user.rs +``` + +We need to declare the module in our client crate, and we'll want to import its definitions. + +To `client/src/main.rs`, add: + +```rust +mod module_bindings; +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. + +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}, +}; +``` + +## Define 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: + +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: + +```rust +fn main() { + register_callbacks(); + connect_to_db(); + subscribe_to_tables(); + user_input_loop(); +} +``` + +## Register callbacks + +We need to handle several sorts of events: + +1. When we connect and receive our credentials, we'll save them to a file so that the next time we connect, we can re-authenticate as the same user. +2. When a new user joins, we'll print a message introducing them. +3. When a user is updated, we'll print their new name, or declare their new online status. +4. When we receive a new message, we'll print it. +5. When we're informed of the backlog of past messages, we'll sort them and print them in order. +6. If the server rejects our attempt to set our name, we'll print an error. +7. If the server rejects a message we send, we'll print an error. +8. When our connection ends, we'll print a note, then exit the process. + +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); + + // When a new user joins, print a notification. + User::on_insert(on_user_inserted); + + // When a user's status changes, print a notification. + User::on_update(on_user_updated); + + // When a new message is received, print it. + Message::on_insert(on_message_inserted); + + // When we receive the message backlog, print it in timestamp order. + on_subscription_applied(on_sub_applied); + + // When we fail to set our name, print a warning. + 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); +} +``` + +### 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. + +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) { + eprintln!("Failed to save credentials: {:?}", e); + } +} + +const CREDS_DIR: &str = ".spacetime_chat"; +``` + +### 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`. + +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. + +`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. + +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. + +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>) { + if user.online { + println!("User {} connected.", user_name_or_identity(user)); + } +} + +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]) +} +``` + +### 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. + +`on_update` callbacks take three arguments: the old row, the new row, and an `Option<&ReducerEvent>`. + +In our module, users can be updated for three reasons: + +1. They've set their name using the `set_name` reducer. +2. They're an existing user re-connecting, so their `online` has been set to `true`. +3. They've disconnected, so their `online` has been set to `false`. + +We'll print an appropriate message in each of these cases. + +To `client/src/main.rs`, add: + +```rust +/// Our `User::on_update` callback: +/// print a notification about name and status changes. +fn on_user_updated(old: &User, new: &User, _: Option<&ReducerEvent>) { + if old.name != new.name { + println!( + "User {} renamed to {}.", + user_name_or_identity(old), + user_name_or_identity(new) + ); + } + if old.online && !new.online { + println!("User {} disconnected.", user_name_or_identity(new)); + } + if !old.online && new.online { + println!("User {} connected.", user_name_or_identity(new)); + } +} +``` + +### 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. + +To find the `User` based on the message's `sender` identity, we'll use `User::filter_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filter_by_identity` accepts an owned `Identity`, rather than a reference. We can `clone` the identity held in `message.sender`. + +We'll print the user's name or identity in the same way as we did when notifying about `User` table events, but here we have to handle the case where we don't find a matching `User` row. This can happen when the module owner sends a message using the CLI's `spacetime call`. In this case, we'll print `unknown`. + +To `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 print_message(message: &Message) { + let sender = User::filter_by_identity(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: + +```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::>(); + messages.sort_by_key(|m| m.sent); + for message in messages { + print_message(&message); + } +} +``` + +### Warn if our name was rejected + +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. + +These callbacks will be invoked in one of two cases: + +1. If the reducer was successful and altered any of our subscribed rows. +2. If we requested an invocation which failed. + +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. + +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); + } +} +``` + +### 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); + } +} +``` + +### 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 +/// The URL of the SpacetimeDB instance hosting our chat module. +const SPACETIMEDB_URI: &str = "http://localhost:3000"; + +/// The module name we chose when we published our module. +const DB_NAME: &str = ""; + +/// 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"); +} +``` + +## Subscribe to queries + +SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. + +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(); +} +``` + +## 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. + +`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`. + +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() { + 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()); + } else { + send_message(line); + } + } +} +``` + +## Run it + +Change your directory to the client app, then compile and run it. From the `quickstart-chat` directory, run: + +```bash +cd client +cargo run +``` + +You should see something like: + +``` +User d9e25c51996dea2f connected. +``` + +Now try sending a message. Type `Hello, world!` and press enter. You should see something like: + +``` +d9e25c51996dea2f: Hello, world! +``` + +Next, set your name. Type `/name `, replacing `` with your name. You should see something like: + +``` +User d9e25c51996dea2f renamed to . +``` + +Then send another message. Type `Hello after naming myself.` and press enter. You should see: + +``` +: 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: + +``` +User connected. +: Hello, world! +: Hello after naming myself. +``` + +## What's next? + +You can find the full code for this client [in the Rust SDK's examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/sdk/examples/quickstart-chat). + +Check out the [Rust SDK Reference](rust1.) 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. + +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). + +Or, you could extend the module and the client together, perhaps: + +- 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. diff --git a/Writerside/topics/sdks/sdks_index.md b/Writerside/topics/sdks/sdks_index.md new file mode 100644 index 00000000..bcc59bfd --- /dev/null +++ b/Writerside/topics/sdks/sdks_index.md @@ -0,0 +1,74 @@ + 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 + +- [Rust](rust1.) - [(Quickstart)](quickstart2.) +- [C#](c-sharp1.) - [(Quickstart)](quickstart3.) +- [TypeScript](typescript.) - [(Quickstart)](quickstart4.) +- [Python](python.) - [(Quickstart)](quickstart5.) + +## Key Features + +The SpacetimeDB Client SDKs offer the following key functionalities: + +### Connection Management + +The SDKs handle the process of connecting and disconnecting from the SpacetimeDB server, simplifying this process for the client applications. + +### Authentication + +The SDKs support authentication using an auth token, allowing clients to securely establish a session with the SpacetimeDB server. + +### Local Database View + +Each client can define a local view of the database via a subscription consisting of a set of queries. This local view is maintained by the server and populated into a local cache on the client side. + +### Reducer Calls + +The SDKs allow clients to call transactional functions (reducers) on the server. + +### Callback Registrations + +The SpacetimeDB Client SDKs offer powerful callback functionality that allow clients to monitor changes in their local database view. These callbacks come in two forms: + +#### Connection and Subscription Callbacks + +Clients can also register callbacks that trigger when the connection to the server is established or lost, or when a subscription is updated. This allows clients to react to changes in the connection status. + +#### Row Update Callbacks + +Clients can register callbacks that trigger when any row in their local cache is updated by the server. These callbacks contain information about the reducer that triggered the change. This feature enables clients to react to changes in data that they're interested in. + +#### Reducer Call Callbacks + +Clients can also register callbacks that fire when a reducer call modifies something in the client's local view. This allows the client to know when a transactional function it has executed has had an effect on the data it cares about. + +Additionally, when a client makes a reducer call that fails, the SDK triggers the registered reducer callback on the client that initiated the failed call with the error message that was returned from the server. This allows for appropriate error handling or user notifications. + +## Choosing a Language + +When selecting a language for your client application with SpacetimeDB, a variety of factors come into play. While the functionality of the SDKs remains consistent across different languages, the choice of language will often depend on the specific needs and context of your application. Here are a few considerations: + +### Team Expertise + +The familiarity of your development team with a particular language can greatly influence your choice. You might want to choose a language that your team is most comfortable with to increase productivity and reduce development time. + +### Application Type + +Different languages are often better suited to different types of applications. For instance, if you are developing a web-based application, you might opt for TypeScript due to its seamless integration with web technologies. On the other hand, if you're developing a desktop application, you might choose C# or Python, depending on your requirements and platform. Python is also very useful for utility scripts and tools. + +### Performance + +The performance characteristics of the different languages can also be a factor. If your application is performance-critical, you might opt for Rust, known for its speed and memory efficiency. + +### Platform Support + +The platform you're targeting can also influence your choice. For instance, if you're developing a game or a 3D application using the Unity engine, you'll want to choose the C# SDK, as Unity uses C# as its primary scripting language. + +### Ecosystem and Libraries + +Each language has its own ecosystem of libraries and tools that can help in developing your application. If there's a library in a particular language that you want to use, it may influence your choice. + +Remember, the best language to use is the one that best fits your use case and the one you and your team are most comfortable with. It's worth noting that due to the consistent functionality across different SDKs, transitioning from one language to another should you need to in the future will primarily involve syntax changes rather than changes in the application's logic. + +You may want to use multiple languages in your application. For instance, you might want to use C# in Unity for your game logic, TypeScript for a web-based administration panel, and Python for utility scripts. This is perfectly fine, as the SpacetimeDB server is completely client-agnostic. diff --git a/Writerside/topics/sdks/typescript/typescript_index.md b/Writerside/topics/sdks/typescript/typescript_index.md new file mode 100644 index 00000000..2316ecbb --- /dev/null +++ b/Writerside/topics/sdks/typescript/typescript_index.md @@ -0,0 +1,942 @@ +# The SpacetimeDB Typescript client SDK + +The SpacetimeDB client SDK for TypeScript contains all the tools you need to build clients for SpacetimeDB modules using Typescript, either in the browser or with NodeJS. + +> You need a database created before use the client, so make sure to follow the Rust or C# Module Quickstart guides if need one. + +## Install the SDK + +First, create a new client project, and add the following to your `tsconfig.json` file: + +```json +{ + "compilerOptions": { + //You can use any target higher than this one + //https://www.typescriptlang.org/tsconfig#target + "target": "es2015" + } +} +``` + +Then add the SpacetimeDB SDK to your dependencies: + +```bash +cd client +npm install @clockworklabs/spacetimedb-sdk +``` + +You should have this folder layout starting from the root of your project: + +```bash +quickstart-chat +├── client +│ ├── node_modules +│ ├── public +│ └── src +└── server + └── src +``` + +### Tip for utilities/scripts + +If want to create a quick script to test your module bindings from the command line, you can use https://www.npmjs.com/package/tsx to execute TypeScript files. + +Then you create a `script.ts` file and add the imports, code and execute with: + +```bash +npx tsx src/script.ts +``` + +## Generate module bindings + +Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's `src` directory and generate the Typescript interface files using the Spacetime CLI. From your project directory, run: + +```bash +mkdir -p client/src/module_bindings +spacetime generate --lang typescript \ + --out-dir client/src/module_bindings \ + --project-path server +``` + +And now you will get the files for the `reducers` & `tables`: + +```bash +quickstart-chat +├── client +│ ├── node_modules +│ ├── public +│ └── src +| └── module_bindings +| ├── add_reducer.ts +| ├── person.ts +| └── say_hello_reducer.ts +└── server + └── src +``` + +Import the `module_bindings` in your client's _main_ file: + +```typescript +import { SpacetimeDBClient, Identity } from "@clockworklabs/spacetimedb-sdk"; + +import Person from "./module_bindings/person"; +import AddReducer from "./module_bindings/add_reducer"; +import SayHelloReducer from "./module_bindings/say_hello_reducer"; +console.log(Person, AddReducer, SayHelloReducer); +``` + +> There is a known issue where if you do not use every type in your file, it will not pull them into the published build. To fix this, we are using `console.log` to force them to get pulled in. + +## API at a glance + +### Classes + +| Class | Description | +|-------------------------------------------------|------------------------------------------------------------------------------| +| [`SpacetimeDBClient`](#class-spacetimedbclient.) | The database client connection to a SpacetimeDB server. | +| [`Identity`](typescript_#class-identity.) | The user's public identity. | +| [`Address`](typescript_#class-address.) | An opaque identifier for differentiating connections by the same `Identity`. | +| [`{Table}`](typescript_#class-table.) | `{Table}` is a placeholder for each of the generated tables. | +| [`{Reducer}`](typescript_#class-reducer.) | `{Reducer}` is a placeholder for each of the generated reducers. | + +### Class `SpacetimeDBClient` + +The database client connection to a SpacetimeDB server. + +Defined in [spacetimedb-sdk.spacetimedb](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/spacetimedb.ts): + +| Constructors | Description | +| ----------------------------------------------------------------- | ------------------------------------------------------------------------ | +| [`SpacetimeDBClient.constructor`](#spacetimedbclient-constructor.) | Creates a new `SpacetimeDBClient` database client. | +| Properties | +| [`SpacetimeDBClient.identity`](#spacetimedbclient-identity.) | The user's public identity. | +| [`SpacetimeDBClient.live`](#spacetimedbclient-live.) | Whether the client is connected. | +| [`SpacetimeDBClient.token`](#spacetimedbclient-token.) | The user's private authentication token. | +| Methods | | +| [`SpacetimeDBClient.connect`](#spacetimedbclient-connect.) | Connect to a SpacetimeDB module. | +| [`SpacetimeDBClient.disconnect`](#spacetimedbclient-disconnect.) | Close the current connection. | +| [`SpacetimeDBClient.subscribe`](#spacetimedbclient-subscribe.) | Subscribe to a set of queries. | +| Events | | +| [`SpacetimeDBClient.onConnect`](#spacetimedbclient-onconnect.) | Register a callback to be invoked upon authentication with the database. | +| [`SpacetimeDBClient.onError`](#spacetimedbclient-onerror.) | Register a callback to be invoked upon a error. | + +## Constructors + +### `SpacetimeDBClient` constructor + +Creates a new `SpacetimeDBClient` database client and set the initial parameters. + +```ts +new SpacetimeDBClient(host: string, name_or_address: string, auth_token?: string, protocol?: "binary" | "json") +``` + +#### Parameters + +| Name | Type | Description | +| :---------------- | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| `host` | `string` | The host of the SpacetimeDB server. | +| `name_or_address` | `string` | The name or address of the SpacetimeDB module. | +| `auth_token?` | `string` | The credentials to use to connect to authenticate with SpacetimeDB. | +| `protocol?` | `"binary"` \| `"json"` | Define how encode the messages: `"binary"` \| `"json"`. Binary is more efficient and compact, but JSON provides human-readable debug information. | + +#### Example + +```ts +const host = "ws://localhost:3000"; +const name_or_address = "database_name"; +const auth_token = undefined; +const protocol = "binary"; + +var spacetimeDBClient = new SpacetimeDBClient( + host, + name_or_address, + auth_token, + protocol +); +``` + +## Class methods + +### `SpacetimeDBClient.registerReducers` + +Registers reducer classes for use with a SpacetimeDBClient + +```ts +registerReducers(...reducerClasses: ReducerClass[]) +``` + +#### Parameters + +| Name | Type | Description | +| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | +| `reducerClasses` | `ReducerClass` | A list of classes to register | + +#### Example + +```ts +import SayHelloReducer from './types/say_hello_reducer'; +import AddReducer from './types/add_reducer'; + +SpacetimeDBClient.registerReducers(SayHelloReducer, AddReducer); +``` + +--- + +### `SpacetimeDBClient.registerTables` + +Registers table classes for use with a SpacetimeDBClient + +```ts +registerTables(...reducerClasses: TableClass[]) +``` + +#### Parameters + +| Name | Type | Description | +| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | +| `tableClasses` | `TableClass` | A list of classes to register | + +#### Example + +```ts +import User from './types/user'; +import Player from './types/player'; + +SpacetimeDBClient.registerTables(User, Player); +``` + +--- + +## Properties + +### `SpacetimeDBClient` identity + +The user's public [Identity](typescript_#class-identity.). + +``` +identity: Identity | undefined +``` + +--- + +### `SpacetimeDBClient` live + +Whether the client is connected. + +```ts +live: boolean; +``` + +--- + +### `SpacetimeDBClient` token + +The user's private authentication token. + +``` +token: string | undefined +``` + +#### Parameters + +| Name | Type | Description | +| :------------ | :----------------------------------------------------- | :------------------------------ | +| `reducerName` | `string` | The name of the reducer to call | +| `serializer` | [`Serializer`](serializer.Serializer.md) | - | + +--- + +### `SpacetimeDBClient` connect + +Connect to The SpacetimeDB Websocket For Your Module. By default, this will use a secure websocket connection. The parameters are optional, and if not provided, will use the values provided on construction of the client. + +```ts +connect(host: string?, name_or_address: string?, auth_token: string?): Promise +``` + +#### Parameters + +| Name | Type | Description | +| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | +| `host?` | `string` | The hostname of the SpacetimeDB server. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | +| `name_or_address?` | `string` | The name or address of the SpacetimeDB module. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | +| `auth_token?` | `string` | The credentials to use to authenticate with SpacetimeDB. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | + +#### Returns + +`Promise`<`void`\> + +#### Example + +```ts +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 +); +// Connect with the initial parameters +spacetimeDBClient.connect(); +//Set the `auth_token` +spacetimeDBClient.connect(undefined, undefined, NEW_TOKEN); +``` + +--- + +### `SpacetimeDBClient` disconnect + +Close the current connection. + +```ts +disconnect(): void +``` + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); + +spacetimeDBClient.disconnect(); +``` + +--- + +### `SpacetimeDBClient` subscribe + +Subscribe to a set of queries, to be notified when rows which match those queries are altered. + +> A new call to `subscribe` will remove all previous subscriptions and replace them with the new `queries`. +> If any rows matched the previous subscribed queries but do not match the new queries, +> those rows will be removed from the client cache, and [`{Table}.on_delete`](#table-ondelete.) callbacks will be invoked for them. + +```ts +subscribe(queryOrQueries: string | string[]): void +``` + +#### Parameters + +| Name | Type | Description | +| :--------------- | :--------------------- | :------------------------------- | +| `queryOrQueries` | `string` \| `string`[] | A `SQL` query or list of queries | + +#### Example + +```ts +spacetimeDBClient.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); +``` + +## Events + +### `SpacetimeDBClient` onConnect + +Register a callback to be invoked upon authentication with the database. + +```ts +onConnect(callback: (token: string, identity: Identity) => void): void +``` + +The callback will be invoked with the public user [Identity](typescript_#class-identity.), private authentication token and connection [`Address`](typescript_#class-address.) provided by the database. If credentials were supplied to [connect](#spacetimedbclient-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 will be generated by the database to identify this user. + +The credentials passed to the callback can be saved and used to authenticate the same user in future connections. + +#### Parameters + +| Name | Type | +|:-----------|:-----------------------------------------------------------------------------------------------------------------| +| `callback` | (`token`: `string`, `identity`: [`Identity`](typescript_#class-identity.), `address`: [`Address`](typescript_#class-address.)) => `void` | + +#### Example + +```ts +spacetimeDBClient.onConnect((token, identity, address) => { + console.log("Connected to SpacetimeDB"); + console.log("Token", token); + console.log("Identity", identity); + console.log("Address", address); +}); +``` + +--- + +### `SpacetimeDBClient` onError + +Register a callback to be invoked upon an error. + +```ts +onError(callback: (...args: any[]) => void): void +``` + +#### Parameters + +| Name | Type | +| :--------- | :----------------------------- | +| `callback` | (...`args`: `any`[]) => `void` | + +#### Example + +```ts +spacetimeDBClient.onError((...args: any[]) => { + console.error("ERROR", args); +}); +``` + +### Class `Identity` + +A unique public identifier for a user of a database. + +Defined in [spacetimedb-sdk.identity](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/identity.ts): + +| Constructors | Description | +| ----------------------------------------------- | -------------------------------------------- | +| [`Identity.constructor`](#identity-constructor.) | Creates a new `Identity`. | +| Methods | | +| [`Identity.isEqual`](#identity-isequal.) | Compare two identities for equality. | +| [`Identity.toHexString`](#identity-tohexstring.) | Print the identity as a hexadecimal string. | +| Static methods | | +| [`Identity.fromString`](#identity-fromstring.) | Parse an Identity from a hexadecimal string. | + +## Constructors + +### `Identity` constructor + +```ts +new Identity(data: Uint8Array) +``` + +#### Parameters + +| Name | Type | +| :----- | :----------- | +| `data` | `Uint8Array` | + +## Methods + +### `Identity` isEqual + +Compare two identities for equality. + +```ts +isEqual(other: Identity): boolean +``` + +#### Parameters + +| Name | Type | +| :------ | :---------------------------- | +| `other` | [`Identity`](typescript_#class-identity.) | + +#### Returns + +`boolean` + +--- + +### `Identity` toHexString + +Print an `Identity` as a hexadecimal string. + +```ts +toHexString(): string +``` + +#### Returns + +`string` + +--- + +### `Identity` fromString + +Static method; parse an Identity from a hexadecimal string. + +```ts +Identity.fromString(str: string): Identity +``` + +#### Parameters + +| Name | Type | +| :---- | :------- | +| `str` | `string` | + +#### Returns + +[`Identity`](typescript_#class-identity.) + +### Class `Address` + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](typescript_#type-identity.). + +Defined in [spacetimedb-sdk.address](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/address.ts): + +| Constructors | Description | +| ----------------------------------------------- | -------------------------------------------- | +| [`Address.constructor`](#address-constructor.) | Creates a new `Address`. | +| Methods | | +| [`Address.isEqual`](#address-isequal.) | Compare two identities for equality. | +| [`Address.toHexString`](#address-tohexstring.) | Print the address as a hexadecimal string. | +| Static methods | | +| [`Address.fromString`](#address-fromstring.) | Parse an Address from a hexadecimal string. | + +## Constructors + +### `Address` constructor + +```ts +new Address(data: Uint8Array) +``` + +#### Parameters + +| Name | Type | +| :----- | :----------- | +| `data` | `Uint8Array` | + +## Methods + +### `Address` isEqual + +Compare two addresses for equality. + +```ts +isEqual(other: Address): boolean +``` + +#### Parameters + +| Name | Type | +| :------ | :---------------------------- | +| `other` | [`Address`](typescript_#class-address.) | + +#### Returns + +`boolean` + +___ + +### `Address` toHexString + +Print an `Address` as a hexadecimal string. + +```ts +toHexString(): string +``` + +#### Returns + +`string` + +___ + +### `Address` fromString + +Static method; parse an Address from a hexadecimal string. + +```ts +Address.fromString(str: string): Address +``` + +#### Parameters + +| Name | Type | +| :---- | :------- | +| `str` | `string` | + +#### Returns + +[`Address`](typescript_#class-address.) + +### Class `{Table}` + +For each table defined by a module, `spacetime generate` generates a `class` in the `module_bindings` folder whose name is that table's name converted to `PascalCase`. + +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; returned subscribed rows with a given value in a particular 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 + +### {Table} name + +• **name**: `string` + +The name of the `Class`. + +--- + +### {Table} tableName + +The name of the table in the database. + +▪ `Static` **tableName**: `string` = `"Person"` + +## Methods + +### {Table} all + +Return all the subscribed rows in the table. + +```ts +{Table}.all(): {Table}[] +``` + +#### Returns + +`{Table}[]` + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); + +spacetimeDBClient.onConnect((token, identity, address) => { + spacetimeDBClient.subscribe(["SELECT * FROM Person"]); + + setTimeout(() => { + console.log(Person.all()); // Prints all the `Person` rows in the database. + }, 5000); +}); +``` + +--- + +### {Table} count + +Return the number of subscribed rows in the table, or 0 if there is no active connection. + +```ts +{Table}.count(): number +``` + +#### Returns + +`number` + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); + +spacetimeDBClient.onConnect((token, identity, address) => { + spacetimeDBClient.subscribe(["SELECT * FROM Person"]); + + setTimeout(() => { + console.log(Person.count()); + }, 5000); +}); +``` + +--- + +### {Table} filterBy{COLUMN} + +For each column of a table, `spacetime generate` generates a static method on the `Class` to filter or seek subscribed rows where that column matches a requested value. + +These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `camelCase`. + +```ts +{Table}.filterBy{COLUMN}(value): {Table}[] +``` + +#### Parameters + +| Name | Type | +| :------ | :-------------------------- | +| `value` | The type of the `{COLUMN}`. | + +#### Returns + +`{Table}[]` + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); + +spacetimeDBClient.onConnect((token, identity, address) => { + spacetimeDBClient.subscribe(["SELECT * FROM Person"]); + + setTimeout(() => { + console.log(Person.filterByName("John")); // prints all the `Person` rows named John. + }, 5000); +}); +``` + +--- + +### {Table} fromValue + +Deserialize an `AlgebraicType` into this `{Table}`. + +```ts + {Table}.fromValue(value: AlgebraicValue): {Table} +``` + +#### Parameters + +| Name | Type | +| :------ | :--------------- | +| `value` | `AlgebraicValue` | + +#### Returns + +`{Table}` + +--- + +### {Table} getAlgebraicType + +Serialize `this` into an `AlgebraicType`. + +#### Example + +```ts +{Table}.getAlgebraicType(): AlgebraicType +``` + +#### Returns + +`AlgebraicType` + +--- + +### {Table} onInsert + +Register an `onInsert` callback for when a subscribed row is newly inserted into the database. + +```ts +{Table}.onInsert(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | Description | +| :--------- | :---------------------------------------------------------------------------- | :----------------------------------------------------- | +| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is inserted. | + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); +spacetimeDBClient.onConnect((token, identity, address) => { + 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); + } +}); +``` + +--- + +### {Table} removeOnInsert + +Unregister a previously-registered [`onInsert`](#table-oninsert.) callback. + +```ts +{Table}.removeOnInsert(callback: (value: Person, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | +| :--------- | :---------------------------------------------------------------------------- | +| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | + +--- + +### {Table} onUpdate + +Register an `onUpdate` callback to run when an existing row is modified by primary key. + +```ts +{Table}.onUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +`onUpdate` callbacks are only meaningful for tables with a column declared as a primary key. Tables without primary keys will never fire `onUpdate` callbacks. + +#### Parameters + +| Name | Type | Description | +| :--------- | :------------------------------------------------------------------------------------------------------ | :---------------------------------------------------- | +| `callback` | (`oldValue`: `{Table}`, `newValue`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is updated. | + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); +spacetimeDBClient.onConnect((token, identity, address) => { + spacetimeDBClient.subscribe(["SELECT * FROM Person"]); +}); + +Person.onUpdate((oldPerson, newPerson, reducerEvent) => { + console.log("Person updated by reducer", reducerEvent, oldPerson, newPerson); +}); +``` + +--- + +### {Table} removeOnUpdate + +Unregister a previously-registered [`onUpdate`](#table-onupdate.) callback. + +```ts +{Table}.removeOnUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | +| :--------- | :------------------------------------------------------------------------------------------------------ | +| `callback` | (`oldValue`: `{Table}`, `newValue`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | + +--- + +### {Table} onDelete + +Register an `onDelete` callback for when a subscribed row is removed from the database. + +```ts +{Table}.onDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | Description | +| :--------- | :---------------------------------------------------------------------------- | :---------------------------------------------------- | +| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is removed. | + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); +spacetimeDBClient.onConnect((token, identity, address) => { + 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 + ); + } +}); +``` + +--- + +### {Table} removeOnDelete + +Unregister a previously-registered [`onDelete`](#table-ondelete.) callback. + +```ts +{Table}.removeOnDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | +| :--------- | :---------------------------------------------------------------------------- | +| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | + +### Class `{Reducer}` + +`spacetime generate` defines an `{Reducer}` class in the `module_bindings` folder for each reducer defined by a module. + +The class's name will be the reducer's name converted to `PascalCase`. + +| Static methods | Description | +| ------------------------------- | ------------------------------------------------------------ | +| [`Reducer.call`](#reducer-call.) | Executes the reducer. | +| Events | | +| [`Reducer.on`](#reducer-on.) | Register a callback to run each time the reducer is invoked. | + +## Static methods + +### {Reducer} call + +Executes the reducer. + +```ts +{Reducer}.call(): void +``` + +#### Example + +```ts +SayHelloReducer.call(); +``` + +## Events + +### {Reducer} on + +Register a callback to run each time the reducer is invoked. + +```ts +{Reducer}.on(callback: (reducerEvent: ReducerEvent, ...reducerArgs: any[]) => void): void +``` + +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. + +#### Parameters + +| Name | Type | +| :--------- | :---------------------------------------------------------- | +| `callback` | `(reducerEvent: ReducerEvent, ...reducerArgs: any[]) => void)` | + +#### Example + +```ts +SayHelloReducer.on((reducerEvent, ...reducerArgs) => { + console.log("SayHelloReducer called", reducerEvent, reducerArgs); +}); +``` diff --git a/Writerside/topics/sdks/typescript/typescript_quickstart.md b/Writerside/topics/sdks/typescript/typescript_quickstart.md new file mode 100644 index 00000000..13ccd4d6 --- /dev/null +++ b/Writerside/topics/sdks/typescript/typescript_quickstart.md @@ -0,0 +1,502 @@ +# Typescript Client SDK Quick Start + +In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in Typescript. + +We'll implement a basic single page web app for the module created in our Rust or C# Module Quickstart guides. **Make sure you follow one of these guides before you start on this one.** + +## Project structure + +Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: + +```bash +cd quickstart-chat +``` + +Within it, create a `client` react app: + +```bash +npx create-react-app client --template typescript +``` + +We also need to install the `spacetime-client-sdk` package: + +```bash +cd client +npm install @clockworklabs/spacetimedb-sdk +``` + +## Basic layout + +We are going to start by creating a basic layout for our app. The page contains four sections: + +1. A profile section, where we can set our name. +2. A message section, where we can see all the messages. +3. A system section, where we can see system messages. +4. A new message section, where we can send a new message. + +The `onSubmitNewName` and `onMessageSubmit` callbacks will be called when the user clicks the submit button in the profile and new message sections, respectively. We'll hook these up later. + +Replace the entire contents of `client/src/App.tsx` with the following: + +```typescript +import React, { useEffect, useState } from "react"; +import logo from "./logo.svg"; +import "./App.css"; + +export type MessageType = { + name: string; + message: string; +}; + +function App() { + const [newName, setNewName] = useState(""); + const [settingName, setSettingName] = useState(false); + const [name, setName] = useState(""); + const [systemMessage, setSystemMessage] = useState(""); + const [messages, setMessages] = useState([]); + + const [newMessage, setNewMessage] = useState(""); + + const onSubmitNewName = (e: React.FormEvent) => { + e.preventDefault(); + setSettingName(false); + // Fill in app logic here + }; + + const onMessageSubmit = (e: React.FormEvent) => { + e.preventDefault(); + // Fill in app logic here + setNewMessage(""); + }; + + return ( +
+
+

Profile

+ {!settingName ? ( + <> +

{name}

+ + + ) : ( +
+ setNewName(e.target.value)} + /> + + + )} +
+
+

Messages

+ {messages.length < 1 &&

No messages

} +
+ {messages.map((message, key) => ( +
+

+ {message.name} +

+

{message.message}

+
+ ))} +
+
+
+

System

+
+

{systemMessage}

+
+
+
+
+

New Message

+ + + +
+
+ ); +} + +export default App; +``` + +Now when you run `npm start`, you should see a basic chat app that does not yet send or receive messages. + +## Generate your module types + +The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. + +In your `quickstart-chat` directory, run: + +```bash +mkdir -p client/src/module_bindings +spacetime generate --lang typescript --out-dir client/src/module_bindings --project-path server +``` + +Take a look inside `client/src/module_bindings`. The CLI should have generated four files: + +``` +module_bindings +├── message.ts +├── send_message_reducer.ts +├── set_name_reducer.ts +└── user.ts +``` + +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 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); +``` + +## Create your SpacetimeDB client + +First, we need to create a SpacetimeDB client and connect to the module. Create your client at the top of the `App` function. + +We are going to create a stateful variable to store our client's SpacetimeDB identity when we receive it. Also, we are using `localStorage` to retrieve your auth token if this client has connected before. We will explain these later. + +Replace `` with the name you chose when publishing your module during the module quickstart. If you are using SpacetimeDB Cloud, the host will be `wss://spacetimedb.com/spacetimedb`. + +Add this before the `App` function declaration: + +```typescript +let token = localStorage.getItem("auth_token") || undefined; +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "chat", + token +); +``` + +Inside the `App` function, add a few refs: + +```typescript +let local_identity = useRef(undefined); +let initialized = useRef(false); +const client = useRef(spacetimeDBClient); +``` + +## Register callbacks and connect + +We need to handle several sorts of events: + +1. `onConnect`: When we connect and receive our credentials, we'll save them to browser local storage, so that the next time we connect, we can re-authenticate as the same user. +2. `initialStateSync`: When we're informed of the backlog of past messages, we'll sort them and update the `message` section of the page. +3. `Message.onInsert`: When we receive a new message, we'll update the `message` section of the page. +4. `User.onInsert`: When a new user joins, we'll update the `system` section of the page with an appropiate message. +5. `User.onUpdate`: When a user is updated, we'll add a message with their new name, or declare their new online status to the `system` section of the page. +6. `SetNameReducer.on`: If the server rejects our attempt to set our name, we'll update the `system` section of the page with an appropriate error message. +7. `SendMessageReducer.on`: If the server rejects a message we send, we'll update the `system` section of the page with an appropriate error message. + +We will add callbacks for each of these items in the following sections. All of these callbacks will be registered inside the `App` function after the `useRef` declarations. + +### onConnect Callback + +On connect SpacetimeDB will provide us with our client credentials. + +Each user has a set of 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. + +These 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. + +We want to store our local client identity in a stateful variable and also save our `token` to local storage for future connections. + +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. + +Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. + +To the body of `App`, add: + +```typescript +client.current.onConnect((token, identity, address) => { + console.log("Connected to SpacetimeDB"); + + local_identity.current = identity; + + localStorage.setItem("auth_token", token); + + client.current.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); +}); +``` + +### initialStateSync callback + +This callback fires when our local client cache of the database is populated. This is a good time to set the initial messages list. + +We'll define a helper function, `setAllMessagesInOrder`, to supply the `MessageType` class for our React application. It will call the autogenerated `Message.all` function to get an array of `Message` rows, then sort them and convert them to `MessageType`. + +To find the `User` based on the message's `sender` identity, we'll use `User::filterByIdentity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filterByIdentity` accepts a `UInt8Array`, rather than an `Identity`. The `sender` identity stored in the message is also a `UInt8Array`, not an `Identity`, so we can just pass it to the filter method. + +Whenever we want to display a user name, if they have set a name, we'll use that. If they haven't set a name, we'll instead use the first 8 bytes of their identity, encoded as hexadecimal. We'll define the function `userNameOrIdentity` to handle this. + +We also 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 display `unknown`. + +To the body of `App`, add: + +```typescript +function userNameOrIdentity(user: User): string { + console.log(`Name: ${user.name} `); + if (user.name !== null) { + return user.name || ""; + } else { + var identityStr = new Identity(user.identity).toHexString(); + console.log(`Name: ${identityStr} `); + return new Identity(user.identity).toHexString().substring(0, 8); + } +} + +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 sender_identity = User.filterByIdentity(message.sender); + let display_name = sender_identity + ? userNameOrIdentity(sender_identity) + : "unknown"; + + return { + name: display_name, + message: message.text, + }; + }); + + setMessages(messagesType); +} + +client.current.on("initialStateSync", () => { + setAllMessagesInOrder(); + var user = User.filterByIdentity(local_identity?.current?.toUint8Array()!); + setName(userNameOrIdentity(user!)); +}); +``` + +### Message.onInsert callback - Update messages + +When we receive a new message, we'll update the messages section of the page. Keep in mind that we only want to do this for new messages, i.e. those inserted by a `send_message` reducer invocation. When the server is initializing our cache, we'll get a callback for each existing message, but we don't want to update the page for those. To that effect, our `onInsert` callback will check if its `ReducerEvent` argument is not `undefined`, and only update the `message` section in that case. + +To the body of `App`, add: + +```typescript +Message.onInsert((message, reducerEvent) => { + if (reducerEvent !== undefined) { + setAllMessagesInOrder(); + } +}); +``` + +### User.onInsert callback - 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 `onInsert` and `onDelete` methods of the trait `TableType`, 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.onInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. + +`onInsert` and `onDelete` callbacks take two arguments: the altered row, and a `ReducerEvent | undefined`. This will be `undefined` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is a class containing information about the reducer that triggered this event. For now, we can ignore this argument. + +We are going to add a helper function called `appendToSystemMessage` that will append a line to the `systemMessage` state. We will use this to update the `system` message when a new user joins. + +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); +} + +User.onInsert((user, reducerEvent) => { + if (user.online) { + appendToSystemMessage(`${userNameOrIdentity(user)} has connected.`); + } +}); +``` + +### User.onUpdate callback - 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 `onUpdate` method which is automatically implemented by `spacetime generate` for any table with a `#[primarykey]` column. + +`onUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. + +In our module, users can be updated for three reasons: + +1. They've set their name using the `set_name` reducer. +2. They're an existing user re-connecting, so their `online` has been set to `true`. +3. They've disconnected, so their `online` has been set to `false`. + +We'll update the `system` message in each of these cases. + +To the body of `App`, add: + +```typescript +User.onUpdate((oldUser, user, reducerEvent) => { + if (oldUser.online === false && user.online === true) { + appendToSystemMessage(`${userNameOrIdentity(user)} has connected.`); + } else if (oldUser.online === true && user.online === false) { + appendToSystemMessage(`${userNameOrIdentity(user)} has disconnected.`); + } + + if (user.name !== oldUser.name) { + appendToSystemMessage( + `User ${userNameOrIdentity(oldUser)} renamed to ${userNameOrIdentity( + user + )}.` + ); + } +}); +``` + +### SetNameReducer.on callback - Handle errors and update profile name + +We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducer` method which is automatically implemented for each reducer by `spacetime generate`. + +Each reducer callback takes a number of parameters: + +1. `ReducerEvent` that contains information about the reducer that triggered this event. It contains several fields. The ones we care about are: + + - `callerIdentity`: The `Identity` of the client that called the reducer. + - `status`: The `Status` of the reducer run, one of `"Committed"`, `"Failed"` or `"OutOfEnergy"`. + - `message`: The error message, if any, that the reducer returned. + +2. The rest of the parameters are arguments passed to the reducer. + +These callbacks will be invoked in one of two cases: + +1. If the reducer was successful and altered any of our subscribed rows. +2. If we requested an invocation which failed. + +Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. + +We already handle other users' `set_name` calls using our `User.onUpdate` callback, but we need some additional behavior for setting our own name. If our name was rejected, we'll update the `system` message. If our name was accepted, we'll update our name in the app. + +We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. + +If the reducer status comes back as `committed`, we'll update the name in our app. + +To the body of `App`, add: + +```typescript +SetNameReducer.on((reducerEvent, newName) => { + if ( + local_identity.current && + reducerEvent.callerIdentity.isEqual(local_identity.current) + ) { + if (reducerEvent.status === "failed") { + appendToSystemMessage(`Error setting name: ${reducerEvent.message} `); + } else if (reducerEvent.status === "committed") { + setName(newName); + } + } +}); +``` + +### SendMessageReducer.on callback - Handle errors + +We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different. We don't need to do anything for successful SendMessage reducer runs; our Message.onInsert callback already displays them. + +To the body of `App`, add: + +```typescript +SendMessageReducer.on((reducerEvent, newMessage) => { + if ( + local_identity.current && + reducerEvent.callerIdentity.isEqual(local_identity.current) + ) { + if (reducerEvent.status === "failed") { + appendToSystemMessage(`Error sending message: ${reducerEvent.message} `); + } + } +}); +``` + +## Update the UI button callbacks + +We need to update the `onSubmitNewName` and `onMessageSubmit` callbacks to send the appropriate reducer to the module. + +`spacetime generate` defined two functions for us, `SetNameReducer.call` and `SendMessageReducer.call`, 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 `SetNameReducer.call` and `SendMessageReducer.call` take one argument, a `String`. + +Add the following to the `onSubmitNewName` callback: + +```typescript +SetNameReducer.call(newName); +``` + +Add the following to the `onMessageSubmit` callback: + +```typescript +SendMessageReducer.call(newMessage); +``` + +## Connecting to the module + +We need to connect to the module when the app loads. We'll do this by adding a `useEffect` hook to the `App` function. This hook should only run once, when the component is mounted, but we are going to use an `initialized` boolean to ensure that it only runs once. + +```typescript +useEffect(() => { + if (!initialized.current) { + client.current.connect(); + initialized.current = true; + } +}, []); +``` + +## What's next? + +When you run `npm start` you should see a chat app that can send and receive messages. If you open it in multiple private browser windows, you should see that messages are synchronized between them. + +Congratulations! You've built a simple chat app with SpacetimeDB. You can find the full source code for this app [here](https://github.com/clockworklabs/spacetimedb-typescript-sdk/tree/main/examples/quickstart) + +For a more advanced example of the SpacetimeDB TypeScript SDK, take a look at the [Spacetime MUD (multi-user dungeon)](https://github.com/clockworklabs/spacetime-mud/tree/main/react-client). + +## Troubleshooting + +If you encounter the following error: + +``` +TS2802: Type 'IterableIterator' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher. +``` + +You can fix it by changing your compiler target. Add the following to your `tsconfig.json` file: + +```json +{ + "compilerOptions": { + "target": "es2015" + } +} +``` diff --git a/Writerside/topics/sql/sql_index.md b/Writerside/topics/sql/sql_index.md new file mode 100644 index 00000000..96f0c223 --- /dev/null +++ b/Writerside/topics/sql/sql_index.md @@ -0,0 +1,407 @@ +# SQL Support + +SpacetimeDB supports a subset of SQL as a query language. Developers can evaluate SQL queries against a Spacetime database via the `spacetime sql` command-line tool and the [`/database/sql/:name_or_address POST` HTTP endpoint](database#databasesqlname_or_address-post.). Client developers also write SQL queries when subscribing to events in the [WebSocket API](ws#subscribe.) or via an SDK `subscribe` function. + +SpacetimeDB aims to support much of the [SQL 2016 standard](https://www.iso.org/standard/63555.html), and in particular aims to be compatible with [PostgreSQL](https://www.postgresql.org/). + +SpacetimeDB 0.6 implements a relatively small subset of SQL. Future SpacetimeDB versions will implement additional SQL features. + +## Types + +| Type | Description | +| --------------------------------------------- | -------------------------------------- | +| [Nullable types](#nullable-types.) | Types which may not hold a value. | +| [Logic types](#logic-types.) | Booleans, i.e. `true` and `false`. | +| [Integer types](#integer-types.) | Numbers without fractional components. | +| [Floating-point types](#floating-point-types.) | Numbers with fractional components. | +| [Text types](#text-types.) | UTF-8 encoded text. | + +### Definition statements + +| Statement | Description | +| ----------------------------- | ------------------------------------ | +| [CREATE TABLE](#create-table.) | Create a new table. | +| [DROP TABLE](#drop-table.) | Remove a table, discarding all rows. | + +### Query statements + +| Statement | Description | +| ----------------- | -------------------------------------------------------------------------------------------- | +| [FROM](#from.) | A source of data, like a table or a value. | +| [JOIN](#join.) | Combine several data sources. | +| [SELECT](#select.) | Select specific rows and columns from a data source, and optionally compute a derived value. | +| [DELETE](#delete.) | Delete specific rows from a table. | +| [INSERT](#insert.) | Insert rows into a table. | +| [UPDATE](#update.) | Update specific rows in a table. | + +## Data types + +SpacetimeDB is built on the Spacetime Algebraic Type System, or SATS. SATS is a richer, more expressive type system than the one included in the SQL language. + +Because SATS is a richer type system than SQL, some SATS types cannot cleanly correspond to SQL types. In particular, the SpacetimeDB SQL interface is unable to construct or compare instances of product and sum types. As such, SpacetimeDB SQL must largely restrict themselves to interacting with columns of builtin types. + +Most SATS builtin types map cleanly to SQL types. + +### Nullable types + +SpacetimeDB types, by default, do not permit `NULL` as a value. Nullable types are encoded in SATS using a sum type which corresponds to [Rust's `Option`](https://doc.rust-lang.org/stable/std/option/enum.Option.html). In SQL, such types can be written by adding the constraint `NULL`, like `INT NULL`. + +### Logic types + +| SQL | SATS | Example | +| --------- | ------ | --------------- | +| `BOOLEAN` | `Bool` | `true`, `false` | + +### Numeric types + +#### Integer types + +An integer is a number without a fractional component. + +Adding the `UNSIGNED` constraint to an integer type allows only positive values. This allows representing a larger positive range without increasing the width of the integer. + +| SQL | SATS | Example | Min | Max | +| ------------------- | ----- | ------- | ------ | ----- | +| `TINYINT` | `I8` | 1 | -(2⁷) | 2⁷-1 | +| `TINYINT UNSIGNED` | `U8` | 1 | 0 | 2⁸-1 | +| `SMALLINT` | `I16` | 1 | -(2¹⁵) | 2¹⁵-1 | +| `SMALLINT UNSIGNED` | `U16` | 1 | 0 | 2¹⁶-1 | +| `INT`, `INTEGER` | `I32` | 1 | -(2³¹) | 2³¹-1 | +| `INT UNSIGNED` | `U32` | 1 | 0 | 2³²-1 | +| `BIGINT` | `I64` | 1 | -(2⁶³) | 2⁶³-1 | +| `BIGINT UNSIGNED` | `U64` | 1 | 0 | 2⁶⁴-1 | + +#### Floating-point types + +SpacetimeDB supports single- and double-precision [binary IEEE-754 floats](https://en.wikipedia.org/wiki/IEEE_754). + +| SQL | SATS | Example | Min | Max | +| ----------------- | ----- | ------- | ------------------------ | ----------------------- | +| `REAL` | `F32` | 1.0 | -3.40282347E+38 | 3.40282347E+38 | +| `DOUBLE`, `FLOAT` | `F64` | 1.0 | -1.7976931348623157E+308 | 1.7976931348623157E+308 | + +### Text types + +SpacetimeDB supports a single string type, `String`. SpacetimeDB strings are UTF-8 encoded. + +| SQL | SATS | Example | Notes | +| ----------------------------------------------- | -------- | ------- | -------------------- | +| `CHAR`, `VARCHAR`, `NVARCHAR`, `TEXT`, `STRING` | `String` | 'hello' | Always UTF-8 encoded | + +> SpacetimeDB SQL currently does not support length contraints like `CHAR(10)`. + +## Syntax + +### Comments + +SQL line comments begin with `--`. + +```sql +-- This is a comment +``` + +### Expressions + +We can express different, composable, values that are universally called `expressions`. + +An expression is one of the following: + +#### Literals + +| Example | Description | +| --------- | ----------- | +| `1` | An integer. | +| `1.0` | A float. | +| `'hello'` | A string. | +| `true` | A boolean. | + +#### Binary operators + +| Example | Description | +| ------- | ------------------- | +| `1 > 2` | Integer comparison. | +| `1 + 2` | Integer addition. | + +#### Logical expressions + +Any expression which returns a boolean, i.e. `true` or `false`, is a logical expression. + +| Example | Description | +| ---------------- | ------------------------------------------------------------ | +| `1 > 2` | Integer comparison. | +| `1 + 2 == 3` | Equality comparison between a constant and a computed value. | +| `true AND false` | Boolean and. | +| `true OR false` | Boolean or. | +| `NOT true` | Boolean inverse. | + +#### Function calls + +| Example | Description | +| --------------- | -------------------------------------------------- | +| `lower('JOHN')` | Apply the function `lower` to the string `'JOHN'`. | + +#### Table identifiers + +| Example | Description | +| ------------- | ------------------------- | +| `inventory` | Refers to a table. | +| `"inventory"` | Refers to the same table. | + +#### Column references + +| Example | Description | +| -------------------------- | ------------------------------------------------------- | +| `inventory_id` | Refers to a column. | +| `"inventory_id"` | Refers to the same column. | +| `"inventory.inventory_id"` | Refers to the same column, explicitly naming its table. | + +#### Wildcards + +Special "star" expressions which select all the columns of a table. + +| Example | Description | +| ------------- | ------------------------------------------------------- | +| `*` | Refers to all columns of a table identified by context. | +| `inventory.*` | Refers to all columns of the `inventory` table. | + +#### Parenthesized expressions + +Sub-expressions can be enclosed in parentheses for grouping and to override operator precedence. + +| Example | Description | +| ------------- | ----------------------- | +| `1 + (2 / 3)` | One plus a fraction. | +| `(1 + 2) / 3` | A sum divided by three. | + +### `CREATE TABLE` + +A `CREATE TABLE` statement creates a new, initially empty table in the database. + +The syntax of the `CREATE TABLE` statement is: + +> **CREATE TABLE** _table_name_ (_column_name_ _data_type_, ...); + +![create-table](create_table.svg) + +#### Examples + +Create a table `inventory` with two columns, an integer `inventory_id` and a string `name`: + +```sql +CREATE TABLE inventory (inventory_id INTEGER, name TEXT); +``` + +Create a table `player` with two integer columns, an `entity_id` and an `inventory_id`: + +```sql +CREATE TABLE player (entity_id INTEGER, inventory_id INTEGER); +``` + +Create a table `location` with three columns, an integer `entity_id` and floats `x` and `z`: + +```sql +CREATE TABLE location (entity_id INTEGER, x REAL, z REAL); +``` + +### `DROP TABLE` + +A `DROP TABLE` statement removes a table from the database, deleting all its associated rows, indexes, constraints and sequences. + +To empty a table of rows without destroying the table, use [`DELETE`](#delete.). + +The syntax of the `DROP TABLE` statement is: + +> **DROP TABLE** _table_name_; + +![drop-table](drop_table.svg) + +Examples: + +```sql +DROP TABLE inventory; +``` + +## Queries + +### `FROM` + +A `FROM` clause derives a data source from a table name. + +The syntax of the `FROM` clause is: + +> **FROM** _table_name_ _join_clause_?; + +![from](from.svg) + +#### Examples + +Select all rows from the `inventory` table: + +```sql +SELECT * FROM inventory; +``` + +### `JOIN` + +A `JOIN` clause combines two data sources into a new data source. + +Currently, SpacetimeDB SQL supports only inner joins, which return rows from two data sources where the values of two columns match. + +The syntax of the `JOIN` clause is: + +> **JOIN** _table_name_ **ON** _expr_ = _expr_; + +![join](join.svg) + +### Examples + +Select all players rows who have a corresponding location: + +```sql +SELECT player.* FROM player + JOIN location + ON location.entity_id = player.entity_id; +``` + +Select all inventories which have a corresponding player, and where that player has a corresponding location: + +```sql +SELECT inventory.* FROM inventory + JOIN player + ON inventory.inventory_id = player.inventory_id + JOIN location + ON player.entity_id = location.entity_id; +``` + +### `SELECT` + +A `SELECT` statement returns values of particular columns from a data source, optionally filtering the data source to include only rows which satisfy a `WHERE` predicate. + +The syntax of the `SELECT` command is: + +> **SELECT** _column_expr_ > **FROM** _from_expr_ +> {**WHERE** _expr_}? + +![sql-select](select.svg) + +#### Examples + +Select all columns of all rows from the `inventory` table: + +```sql +SELECT * FROM inventory; +SELECT inventory.* FROM inventory; +``` + +Select only the `inventory_id` column of all rows from the `inventory` table: + +```sql +SELECT inventory_id FROM inventory; +SELECT inventory.inventory_id FROM inventory; +``` + +An optional `WHERE` clause can be added to filter the data source using a [logical expression](#logical-expressions.). The `SELECT` will return only the rows from the data source for which the expression returns `true`. + +#### Examples + +Select all columns of all rows from the `inventory` table, with a filter that is always true: + +```sql +SELECT * FROM inventory WHERE 1 = 1; +``` + +Select all columns of all rows from the `inventory` table with the `inventory_id` 1: + +```sql +SELECT * FROM inventory WHERE inventory_id = 1; +``` + +Select only the `name` column of all rows from the `inventory` table with the `inventory_id` 1: + +```sql +SELECT name FROM inventory WHERE inventory_id = 1; +``` + +Select all columns of all rows from the `inventory` table where the `inventory_id` is 2 or greater: + +```sql +SELECT * FROM inventory WHERE inventory_id > 1; +``` + +### `INSERT` + +An `INSERT INTO` statement inserts new rows into a table. + +One can insert one or more rows specified by value expressions. + +The syntax of the `INSERT INTO` statement is: + +> **INSERT INTO** _table_name_ (_column_name_, ...) **VALUES** (_expr_, ...), ...; + +![sql-insert](insert.svg) + +#### Examples + +Insert a single row: + +```sql +INSERT INTO inventory (inventory_id, name) VALUES (1, 'health1'); +``` + +Insert two rows: + +```sql +INSERT INTO inventory (inventory_id, name) VALUES (1, 'health1'), (2, 'health2'); +``` + +### UPDATE + +An `UPDATE` statement changes the values of a set of specified columns in all rows of a table, optionally filtering the table to update only rows which satisfy a `WHERE` predicate. + +Columns not explicitly modified with the `SET` clause retain their previous values. + +If the `WHERE` clause is absent, the effect is to update all rows in the table. + +The syntax of the `UPDATE` statement is + +> **UPDATE** _table_name_ **SET** > _column_name_ = _expr_, ... +> {_WHERE expr_}?; + +![sql-update](update.svg) + +#### Examples + +Set the `name` column of all rows from the `inventory` table with the `inventory_id` 1 to `'new name'`: + +```sql +UPDATE inventory + SET name = 'new name' + WHERE inventory_id = 1; +``` + +### DELETE + +A `DELETE` statement deletes rows that satisfy the `WHERE` clause from the specified table. + +If the `WHERE` clause is absent, the effect is to delete all rows in the table. In that case, the result is a valid empty table. + +The syntax of the `DELETE` statement is + +> **DELETE** _table_name_ +> {**WHERE** _expr_}?; + +![sql-delete](delete.svg) + +#### Examples + +Delete all the rows from the `inventory` table with the `inventory_id` 1: + +```sql +DELETE FROM inventory WHERE inventory_id = 1; +``` + +Delete all rows from the `inventory` table, leaving it empty: + +```sql +DELETE FROM inventory; +``` diff --git a/docs/unity/part-2b-c-sharp.md b/Writerside/topics/unity/homeless.md similarity index 88% rename from docs/unity/part-2b-c-sharp.md rename to Writerside/topics/unity/homeless.md index 07d45e7a..121fe538 100644 --- a/docs/unity/part-2b-c-sharp.md +++ b/Writerside/topics/unity/homeless.md @@ -1,22 +1,15 @@ -# Unity Multiplayer Tutorial +### Create the Module -## Part 2 of 3: Inspecting the C# Server Module +1. It is important that you already have the SpacetimeDB CLI tool [installed](install.). -In this part of the tutorial, we will create a SpacetimeDB (STDB) server module using C# for the Unity multiplayer game. The server module will handle the game logic and data management for the game. +2. Run SpacetimeDB locally using the installed CLI. In a **new** terminal or command window, run the following command: -💡 Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -## The Entity Component Systems (ECS) - -Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). - -We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. - -# Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (C#) - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! +```bash +spacetime start +``` -This progressive tutorial is continued from the [Part 1 Tutorial](/docs/unity/part-1.md) +💡 Standalone mode will run in the foreground. +💡 Below examples Rust language, [but you may also use C#](c-sharp_index.md). ## Create a Server Module @@ -44,7 +37,7 @@ using SpacetimeDB.Module; using static SpacetimeDB.Runtime; ``` -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](/docs/modules/c-sharp). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](c-sharp.). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. **Append to the bottom of lib.cs:** @@ -296,7 +289,7 @@ cd server spacetime publish -c unity-tutorial ``` -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. ### Finally, Add Chat Support @@ -357,6 +350,6 @@ Now that we added chat support, let's publish the latest module version to Space spacetime publish -c unity-tutorial ``` -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. -From here, the tutorial continues with more-advanced topics. The [next tutorial](/docs/unity/part-4.md) introduces Resources & Scheduling. +From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside/topics/unity/part-1.md b/Writerside/topics/unity/part-1.md new file mode 100644 index 00000000..bfad0644 --- /dev/null +++ b/Writerside/topics/unity/part-1.md @@ -0,0 +1,57 @@ +# Unity Multiplayer Tutorial + +## Part 1 of 3: Setup + +This tutorial will guide you through setting up a multiplayer game project using Unity and SpacetimeDB. We will start by cloning the project, connecting it to SpacetimeDB and running the project. + +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! + +> [!IMPORTANT] +> TODO: This draft may link to WIP repos or docs - be sure to replace with final links after prerequisite PRs are approved (that are not yet approved upon writing this) + +## 1. Clone the Project + +Let's name it `SpacetimeDBUnityTutorial` for reference: +```bash +git clone https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade SpacetimeDBUnityTutorial +``` + +This project repo is separated into two sub-projects: + +1. [Server](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp) (SpacetimeDB Module) +1. [Client](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Client) (Unity project) + +> [!TIP] +> You may optionally _update_ the [SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk) via the Package Manager in Unity + +## 2. Publishing the Project + +From Unity, you don't need CLI commands for common functionality: + +1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) +1. Create an identity -> Select `testnet` for the server +1. Browse to your repo root `Server-Csharp` dir -> **Publish** -> **Generate** Unity files + +💡For the next section, we'll use the selected `Server` and publish result `Host` + +![Unity Publisher Tool](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-publisher.jpg) + +## 3. Connecting the Project + +1. Open `Scenes/Main` in Unity -> select the `GameManager` GameObject in the inspector. +1. Matching the earlier Publish setup: + 1. For the GameManager `Db Name or Address`, input `testnet` + 1. For the GameManager `Host`, input `https://testnet.spacetimedb.com +1. Save your scene + +## 4. Running the Project + +With the same `Main` scene open, press play! + +![Gameplay Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-action.jpg) + +![UI Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-ui.jpg) + +You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. + +Congratulations! You have successfully set up your multiplayer game project. In the next section, we will break down how Server Modules work and analyze the demo code. diff --git a/Writerside/topics/unity/part-2.md b/Writerside/topics/unity/part-2.md new file mode 100644 index 00000000..348a4e7f --- /dev/null +++ b/Writerside/topics/unity/part-2.md @@ -0,0 +1,483 @@ +# Unity Multiplayer Tutorial + +## Part 2 of 3: Analyzing the C# Server Module + +This progressive tutorial is continued from [Part 1](part-11.md). + +In this part of the tutorial, we will create a SpacetimeDB (SpacetimeDB) server module using C# for the Unity multiplayer game. The server module will handle the game logic and data management for the game. + +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! + +## The Entity Component Systems (ECS) + +Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). + +We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. + +## C# Module Limitations & Nuances + +Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), there are some limitations and nuances to be aware of when developing your server module. Sometimes errors may occur instantly, but others may not reflect until you build or used at runtime. + +In other words, it's best to be aware _before_ you start: + +1. No DateTime-like types in Types or Tables: + - Use `string` for timestamps (exampled at at [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs), or `long` for Unix Epoch time. + +1. No Timers or async/await, such as those to create repeating loops: + - For repeating invokers, instead **re**schedule it from within a fired [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) function. + +1. Using `Debug` advanced option in the `Publisher` Unity editor tool will add callstack symbols for easier debugging: + - However, avoid using `Debug` mode when publishing outside of a `localhost` server: + - Due to WASM buffer size limitations, publishing outside `localhost` may fail. + +1. If you `throw` a new `Exception`, no error logs will appear. Instead, use either: + 1. Use `Log(message, LogLevel.Error);` before you throw. + 2. Use the demo's static [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs) class to `Utils.Throw()` to wrap the error log before throwing. + +1. `[AutoIncrement]` or `[PrimaryKeyAuto]` will never equal 0: + - Inserting a new row with an Auto key equaling 0 will always return a unique, non-0 value. + - +1. Enums cannot declare values out of the default order: + - For example, `{ Foo = 0, Bar = 3 }` will fail to compile. + +## Namespaces + +Common `using` statements include: + +```csharp +using SpacetimeDB; // Contains class|func|struct attributes like [Table], [Type], [Reducer] +using static SpacetimeDB.Runtime; // Contains Identity DbEventArgs, Log() +using SpacetimeDB.Module; // Contains prop attributes like [Column] +using Module.Utils; // Helper to workaround the `throw` and `DateTime` limitations noted above +``` + +- You will mostly see `SpacetimeDB.Module` in [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs) for schema definitions +- `SpacetimeDB` and `SpacetimeDB.Runtime` can be found in most all SpacetimeDB scripts +- `Module.Utils` parse DateTimeOffset into a timestamp string and wraps `throw` with error logs + +## Partial Classes & Structs + +- Throughout the demo, you will notice most classes or structs with a SpacetimeDB [Attribute] such as `[Table]` or `[Reducer]` will be defined with the `partial` keyword. + +- This allows the _Roslyn Compiler_ to [incrementally generate](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) additions to the SpacetimeDB SDK, such as adding helper functions and utilities. This means SpacetimeDB takes care of all the low-level tooling for you, such as inserting, updating or querying the DB. + - This further allows you to separate your models from logic within the same class. + +* Notice that the module class, itself, is also a `static partial class`. + +## Types & Tables + +`[Table]` attributes are database columns, while `[Type]` attributes are define a schema. + +### Types + +`[Type]` attributes attach to properties containing `[Table]` attributes when you want to use a custom Type that's not [SpacetimeDB natively-supported](c-sharp#supported-types.). These are generally defined as a `partial struct` or `partial class` + +Let's inspect a real example `Type`; open [Server-cs/Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): + +In Unity, you are likely familiar with the `Vector2` type. In SpacetimeDB, let's inspect the `StdbVector2` type to store 2D positions in the database: + +```csharp +/// A spacetime type which can be used in tables & reducers to represent a 2D position (such as movement) +[Type] +public partial class StdbVector2 +{ + public float X; + public float Z; +} +``` + +Since `Types` are used in `Tables`, we can now use a custom SpacetimeDB `StdbVector3` `Type` in a `[Table]`. + +### Tables + +`[Table] attributes use `[Type]`s - either custom (like `StdbVector2` above) or [SpacetimeDB natively-supported types](../modules/c-sharp#supported-types). These are generally defined as a `struct` or `class` + +Let's inspect a real example `Table`, looking again at [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): + +```csharp +/// Represents chat messages within the game, including the sender and message content +[Table] +public partial class ChatMessage +{ + /// Primary key, automatically incremented + [Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong ChatEntityId; + + /// The entity id of the player (or NPC) that sent the message + public ulong SourceEntityId; + + /// Message contents + public string? ChatText; + + /// + /// Stringified ISO 8601 format (Unix Epoch Time) + /// + /// DateTime.ToUniversalTime().ToString("o"); + /// + public static string GetTimestamp(DateTimeOffset dateTimeOffset) => + dateTimeOffset.ToUniversalTime().ToString("o"); +} +``` + +**Let's break this down:** +In addition, this allows for static helper utilities like `GetTimestamp.GetTimestamp()` above. Let + + +```csharp +/// This component will be created for all world objects that can move smoothly throughout the world, keeping track +/// of position, the last time the component was updated & the direction the mobile object is currently moving. +[Table] +public partial class MobileEntityComponent +{ + /// Primary key for the mobile entity + [Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + /// The last known location of this entity + public StdbVector2? Location; + + /// Movement direction, {0,0} if not moving at all. + public StdbVector2? Direction; + + /// Timestamp when movement started. Timestamp::UNIX_EPOCH if not moving. + public string? MoveStartTimestamp; +} +``` + +**Let's break this down:** + +- `EntityId` is the unique identifier for the table, declared as a `ulong` +- Location and Direction are both `StdbVector2` types discussed above +- `MoveStartTimestamp` is a string of epoch time, as you cannot use `DateTime`-like types within Tables. + - See the [Limitations](#limitations.) section below + +## Reducers + +Reducers are cloud functions that run on the server and can be called from the client, always returning `void`. + +Looking at the most straight-forward example, open [Chat.cs]( + + + + + + + +```csharp + +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](/docs/modules/c-sharp). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. + +**Append to the bottom of lib.cs:** + +```csharp +/// We're using this table as a singleton, +/// so there should typically only be one element where the version is 0. +[SpacetimeDB.Table] +public partial class Config +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public Identity Version; + public string? MessageOfTheDay; +} +``` + +Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. + +**Append to the bottom of lib.cs:** + +```csharp +/// This allows us to store 3D points in tables. +[SpacetimeDB.Type] +public partial class StdbVector3 +{ + public float X; + public float Y; + public float Z; +} +``` + +Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. + +```csharp +/// This stores information related to all entities in our game. In this tutorial +/// all entities must at least have an entity_id, a position, a direction and they +/// must specify whether or not they are moving. +[SpacetimeDB.Table] +public partial class EntityComponent +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong EntityId; + public StdbVector3 Position; + public float Direction; + public bool Moving; +} +``` + +Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. + +**Append to the bottom of lib.cs:** + +```csharp +/// All players have this component and it associates an entity with the user's +/// Identity. It also stores their username and whether or not they're logged in. +[SpacetimeDB.Table] +public partial class PlayerComponent +{ + // An EntityId that matches an EntityId in the `EntityComponent` table. + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + // The user's identity, which is unique to each player + [SpacetimeDB.Column(ColumnAttrs.Unique)] + public Identity Identity; + public string? Username; + public bool LoggedIn; +} +``` + +Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: + +**Append to the bottom of lib.cs:** + +```csharp +/// This reducer is called when the user logs in for the first time and +/// enters a username. +[SpacetimeDB.Reducer] +public static void CreatePlayer(DbEventArgs dbEvent, string username) +{ + // Get the Identity of the client who called this reducer + Identity sender = dbEvent.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"); + } + + // Create a new entity for this player + try + { + new EntityComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, + Direction = 0, + Moving = false, + }.Insert(); + } + catch + { + Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); + Throw; + } + + // The PlayerComponent uses the same entity_id and stores the identity of + // the owner, username, and whether or not they are logged in. + try + { + new PlayerComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Identity = dbEvent.Sender, + Username = username, + LoggedIn = true, + }.Insert(); + } + catch + { + Log("Error: Failed to insert PlayerComponent", LogLevel.Error); + throw; + } + Log($"Player created: {username}"); +} +``` + +--- + +**SpacetimeDB Reducers** + +"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. + +--- + +SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. + +- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. +- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. +- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. + +Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the module is initially published +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void OnInit() +{ + try + { + new Config + { + Version = 0, + MessageOfTheDay = "Hello, World!", + }.Insert(); + } + catch + { + Log("Error: Failed to insert Config", LogLevel.Error); + throw; + } +} +``` + +We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the client connects, we update the LoggedIn state to true +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void ClientConnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:true); +``` +```csharp +/// Called when the client disconnects, we update the logged_in state to false +[SpacetimeDB.Reducer(ReducerKind.Disconnect)] +public static void ClientDisonnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:false); +``` +```csharp +/// This helper function gets the PlayerComponent, sets the LoggedIn +/// variable and updates the PlayerComponent table row. +private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) +{ + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + player.LoggedIn = loggedIn; + PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); +} +``` + +Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. + +Using the `EntityId` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. + +**Append to the bottom of lib.cs:** + +```csharp +/// Updates the position of a player. This is also called when the player stops moving. +[SpacetimeDB.Reducer] +private static void UpdatePlayerPosition( + DbEventArgs dbEvent, + StdbVector3 position, + float direction, + bool moving) +{ + // First, look up the player using the sender identity + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + // Use the Player's EntityId to retrieve and update the EntityComponent + ulong playerEntityId = player.EntityId; + EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); + if (entity is null) + { + throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); + } + + entity.Position = position; + entity.Direction = direction; + entity.Moving = moving; + EntityComponent.UpdateByEntityId(playerEntityId, entity); +} +``` + +--- + +**Server Validation** + +In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. + +--- + +### Publishing a Module to SpacetimeDB + +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. + +```bash +cd server +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +### Finally, Add Chat Support + +The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. + +First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +[SpacetimeDB.Table] +public partial class ChatMessage +{ + // The primary key for this table will be auto-incremented + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + + // The entity id of the player that sent the message + public ulong SenderId; + + // Message contents + public string? Text; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +/// Adds a chat entry to the ChatMessage table +[SpacetimeDB.Reducer] +public static void SendChatMessage(DbEventArgs dbEvent, string text) +{ + // Get the player's entity id + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + + // Insert the chat message + new ChatMessage + { + SenderId = player.EntityId, + Text = text, + }.Insert(); +} +``` + +## Wrapping Up + +💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside/topics/unity/part-2a-rust.md b/Writerside/topics/unity/part-2a-rust.md new file mode 100644 index 00000000..1271b345 --- /dev/null +++ b/Writerside/topics/unity/part-2a-rust.md @@ -0,0 +1,316 @@ +# Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (Rust) + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 1 Tutorial](part-11.md) + +## Create a Server Module + +Run the following command to initialize the SpacetimeDB server module project with Rust as the language: + +```bash +spacetime init --lang=rust server +``` + +This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. + +### SpacetimeDB Tables + +In this section we'll be making some edits to the file `server/src/lib.rs`. We recommend you open up this file in an IDE like VSCode or RustRover. + +**Important: Open the `server/src/lib.rs` file and delete its contents. We will be writing it from scratch here.** + +First we need to add some imports at the top of the file. + +**Copy and paste into lib.rs:** + +```rust +use spacetimedb::{spacetimedb, Identity, SpacetimeType, ReducerContext}; +use log; +``` + +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [Rust module reference](rust.). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. + +**Append to the bottom of lib.rs:** + +```rust +// We're using this table as a singleton, so there should typically only be one element where the version is 0. +#[spacetimedb(table)] +#[derive(Clone)] +pub struct Config { + #[primarykey] + pub version: u32, + pub message_of_the_day: String, +} +``` + +Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `#[derive(SpacetimeType)]` and a `#[spacetimedb(table)]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. + +**Append to the bottom of lib.rs:** + +```rust +// This allows us to store 3D points in tables. +#[derive(SpacetimeType, Clone)] +pub struct StdbVector3 { + pub x: f32, + pub y: f32, + pub z: f32, +} +``` + +Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. + +```rust +// This stores information related to all entities in our game. In this tutorial +// all entities must at least have an entity_id, a position, a direction and they +// must specify whether or not they are moving. +#[spacetimedb(table)] +#[derive(Clone)] +pub struct EntityComponent { + #[primarykey] + // The autoinc macro here just means every time we insert into this table + // we will receive a new row where this value will be increased by one. This + // allows us to easily get rows where `entity_id` is unique. + #[autoinc] + pub entity_id: u64, + pub position: StdbVector3, + pub direction: f32, + pub moving: bool, +} +``` + +Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `entity_id`. You'll see how this works later in the `create_player` reducer. + +**Append to the bottom of lib.rs:** + +```rust +// All players have this component and it associates an entity with the user's +// Identity. It also stores their username and whether or not they're logged in. +#[derive(Clone)] +#[spacetimedb(table)] +pub struct PlayerComponent { + // An entity_id that matches an entity_id in the `EntityComponent` table. + #[primarykey] + pub entity_id: u64, + + // The user's identity, which is unique to each player + #[unique] + pub owner_id: Identity, + pub username: String, + pub logged_in: bool, +} +``` + +Next, we write our very first reducer, `create_player`. From the client we will call this reducer when we create a new player: + +**Append to the bottom of lib.rs:** + +```rust +// This reducer is called when the user logs in for the first time and +// enters a username +#[spacetimedb(reducer)] +pub fn create_player(ctx: ReducerContext, username: String) -> Result<(), String> { + // Get the Identity of the client who called this reducer + let owner_id = ctx.sender; + + // Make sure we don't already have a player with this identity + if PlayerComponent::filter_by_owner_id(&owner_id).is_some() { + log::info!("Player already exists"); + return Err("Player already exists".to_string()); + } + + // Create a new entity for this player and get a unique `entity_id`. + let entity_id = EntityComponent::insert(EntityComponent + { + entity_id: 0, + position: StdbVector3 { x: 0.0, y: 0.0, z: 0.0 }, + direction: 0.0, + moving: false, + }).expect("Failed to create a unique PlayerComponent.").entity_id; + + // The PlayerComponent uses the same entity_id and stores the identity of + // the owner, username, and whether or not they are logged in. + PlayerComponent::insert(PlayerComponent { + entity_id, + owner_id, + username: username.clone(), + logged_in: true, + }).expect("Failed to insert player component."); + + log::info!("Player created: {}({})", username, entity_id); + + Ok(()) +} +``` + +--- + +**SpacetimeDB Reducers** + +"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. + +--- + +SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. + +- `init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. +- `connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `sender` value of the `ReducerContext`. +- `disconnect` - Called when a user disconnects from the SpacetimeDB module. + +Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. + +**Append to the bottom of lib.rs:** + +```rust +// Called when the module is initially published +#[spacetimedb(init)] +pub fn init() { + Config::insert(Config { + version: 0, + message_of_the_day: "Hello, World!".to_string(), + }).expect("Failed to insert config."); +} +``` + +We use the `connect` and `disconnect` reducers to update the logged in state of the player. The `update_player_login_state` helper function looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `logged_in` variable and calls the auto-generated `update` function on `PlayerComponent` to update the row. + +**Append to the bottom of lib.rs:** + +```rust +// Called when the client connects, we update the logged_in state to true +#[spacetimedb(connect)] +pub fn client_connected(ctx: ReducerContext) { + update_player_login_state(ctx, true); +} +``` +```rust +// Called when the client disconnects, we update the logged_in state to false +#[spacetimedb(disconnect)] +pub fn client_disconnected(ctx: ReducerContext) { + update_player_login_state(ctx, false); +} +``` +```rust +// This helper function gets the PlayerComponent, sets the logged +// in variable and updates the PlayerComponent table row. +pub fn update_player_login_state(ctx: ReducerContext, logged_in: bool) { + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + // We clone the PlayerComponent so we can edit it and pass it back. + let mut player = player.clone(); + player.logged_in = logged_in; + PlayerComponent::update_by_entity_id(&player.entity_id.clone(), player); + } +} +``` + +Our final reducer handles player movement. In `update_player_position` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `create_player` first. + +Using the `entity_id` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `update` function. + +**Append to the bottom of lib.rs:** + +```rust +// Updates the position of a player. This is also called when the player stops moving. +#[spacetimedb(reducer)] +pub fn update_player_position( + ctx: ReducerContext, + position: StdbVector3, + direction: f32, + moving: bool, +) -> Result<(), String> { + // First, look up the player using the sender identity, then use that + // entity_id to retrieve and update the EntityComponent + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + if let Some(mut entity) = EntityComponent::filter_by_entity_id(&player.entity_id) { + entity.position = position; + entity.direction = direction; + entity.moving = moving; + EntityComponent::update_by_entity_id(&player.entity_id, entity); + return Ok(()); + } + } + + // If we can not find the PlayerComponent or EntityComponent for + // this player then something went wrong. + return Err("Player not found".to_string()); +} +``` + +--- + +**Server Validation** + +In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. + +--- + +### Publishing a Module to SpacetimeDB + +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. + +```bash +cd server +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +### Finally, Add Chat Support + +The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. + +First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.rs``. + +**Append to the bottom of server/src/lib.rs:** + +```rust +#[spacetimedb(table)] +pub struct ChatMessage { + // The primary key for this table will be auto-incremented + #[primarykey] + #[autoinc] + pub message_id: u64, + + // The entity id of the player that sent the message + pub sender_id: u64, + // Message contents + pub text: String, +} +``` + +Now we need to add a reducer to handle inserting new chat messages. + +**Append to the bottom of server/src/lib.rs:** + +```rust +// Adds a chat entry to the ChatMessage table +#[spacetimedb(reducer)] +pub fn send_chat_message(ctx: ReducerContext, text: String) -> Result<(), String> { + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + // Now that we have the player we can insert the chat message using the player entity id. + ChatMessage::insert(ChatMessage { + // this column auto-increments so we can set it to 0 + message_id: 0, + sender_id: player.entity_id, + text, + }) + .unwrap(); + + return Ok(()); + } + + Err("Player not found".into()) +} +``` + +## Wrapping Up + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside/topics/unity/part-3.md b/Writerside/topics/unity/part-3.md new file mode 100644 index 00000000..12e85ef3 --- /dev/null +++ b/Writerside/topics/unity/part-3.md @@ -0,0 +1,479 @@ +# Unity Tutorial - Basic Multiplayer - Part 3 - Client + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from one of the Part 2 tutorials: +- [Rust Server Module](part-2a-rust1.md) +- [C# Server Module](part-2.) + +## Updating our Unity Project Client to use SpacetimeDB + +Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. + +### Import the SDK and Generate Module Files + +1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. + +```bash +https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git +``` + +![Unity-PackageManager](Unity-PackageManager.JPG) + +3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. + +```bash +mkdir -p ../client/Assets/module_bindings +spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp +``` + +### Connect to Your SpacetimeDB Module + +The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. + +![Unity-AddNetworkManager](Unity-AddNetworkManager.JPG) + +Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: + +**Append to the top of TutorialGameManager.cs** + +```csharp +using SpacetimeDB; +using SpacetimeDB.Types; +using System.Linq; +``` + +At the top of the class definition add the following members: + +**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** + +```csharp +// These are connection variables that are exposed on the GameManager +// inspector. +[SerializeField] private string moduleAddress = "unity-tutorial"; +[SerializeField] private string hostName = "localhost:3000"; + +// This is the identity for this player that is automatically generated +// the first time you log in. We set this variable when the +// onIdentityReceived callback is triggered by the SDK after connecting +private Identity local_identity; +``` + +The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. + +Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. + +**REPLACE the Start() function in TutorialGameManager.cs** + +```csharp +// Start is called before the first frame update +void Start() +{ + instance = this; + + SpacetimeDBClient.instance.onConnect += () => + { + Debug.Log("Connected."); + + // Request all tables + SpacetimeDBClient.instance.Subscribe(new List() + { + "SELECT * FROM *", + }); + }; + + // Called when we have an error connecting to SpacetimeDB + SpacetimeDBClient.instance.onConnectError += (error, message) => + { + Debug.LogError($"Connection error: " + message); + }; + + // Called when we are disconnected from SpacetimeDB + SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => + { + Debug.Log("Disconnected."); + }; + + // Called when we receive the client identity from SpacetimeDB + SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { + AuthToken.SaveToken(token); + local_identity = identity; + }; + + // Called after our local cache is populated from a Subscribe call + SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + + // Now that we’ve registered all our callbacks, lets connect to spacetimedb + SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); +} +``` + +In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. + +Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. + +--- + +**Local Client Cache** + +The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. + +--- + +Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. + +**Append after the Start() function in TutorialGameManager.cs** + +```csharp +void OnSubscriptionApplied() +{ + // If we don't have any data for our player, then we are creating a + // new one. Let's show the username dialog, which will then call the + // create player reducer + var player = PlayerComponent.FilterByOwnerId(local_identity); + if (player == null) + { + // Show username selection + UIUsernameChooser.instance.Show(); + } + + // Show the Message of the Day in our Config table of the Client Cache + UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); + + // Now that we've done this work we can unregister this callback + SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; +} +``` + +### Adding the Multiplayer Functionality + +Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. + +**Append to the top of UIUsernameChooser.cs** + +```csharp +using SpacetimeDB.Types; +``` + +Then we're doing a modification to the `ButtonPressed()` function: + +**Modify the ButtonPressed function in UIUsernameChooser.cs** + +```csharp +public void ButtonPressed() +{ + CameraController.RemoveDisabler(GetHashCode()); + _panel.SetActive(false); + + // Call the SpacetimeDB CreatePlayer reducer + Reducer.CreatePlayer(_usernameField.text); +} +``` + +We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. + +First append this using to the top of `RemotePlayer.cs` + +**Create file RemotePlayer.cs, then replace its contents:** + +```csharp +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using SpacetimeDB.Types; +using TMPro; + +public class RemotePlayer : MonoBehaviour +{ + public ulong EntityId; + + public TMP_Text UsernameElement; + + public string Username { set { UsernameElement.text = value; } } + + void Start() + { + // Initialize overhead name + UsernameElement = GetComponentInChildren(); + var canvas = GetComponentInChildren(); + canvas.worldCamera = Camera.main; + + // Get the username from the PlayerComponent for this object and set it in the UI + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + if (playerComp is null) + { + string inputUsername = UsernameElement.Text; + Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); + Reducer.CreatePlayer(inputUsername); + + // Try again, optimistically assuming success for simplicity + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + } + + Username = playerComp.Username; + + // Get the last location for this player and set the initial position + EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); + transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + + // Register for a callback that is called when the client gets an + // update for a row in the EntityComponent table + EntityComponent.OnUpdate += EntityComponent_OnUpdate; + } +} +``` + +We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. + +**Append to bottom of RemotePlayer class in RemotePlayer.cs:** + +```csharp +private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) +{ + // If the update was made to this object + if(obj.EntityId == EntityId) + { + var movementController = GetComponent(); + + // Update target position, rotation, etc. + movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); + movementController.RemoteTargetRotation = obj.Direction; + movementController.SetMoving(obj.Moving); + } +} +``` + +Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. + +**Append to bottom of Start() function in TutorialGameManager.cs:** + +```csharp +PlayerComponent.OnInsert += PlayerComponent_OnInsert; +``` + +Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. + +**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** + +```csharp +private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) +{ + // If the identity of the PlayerComponent matches our user identity then this is the local player + if(obj.OwnerId == local_identity) + { + // Now that we have our initial position we can start the game + StartGame(); + } + else + { + // Spawn the player object and attach the RemotePlayer component + var remotePlayer = Instantiate(PlayerPrefab); + + // Lookup and apply the position for this new player + var entity = EntityComponent.FilterByEntityId(obj.EntityId); + var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + remotePlayer.transform.position = position; + + var movementController = remotePlayer.GetComponent(); + movementController.RemoteTargetPosition = position; + movementController.RemoteTargetRotation = entity.Direction; + + remotePlayer.AddComponent().EntityId = obj.EntityId; + } +} +``` + +Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. + +**Append to the top of LocalPlayer.cs** + +```csharp +using SpacetimeDB.Types; +using SpacetimeDB; +``` + +**Append to the bottom of LocalPlayer class in LocalPlayer.cs** + +```csharp +private float? lastUpdateTime; +private void FixedUpdate() +{ + float? deltaTime = Time.time - lastUpdateTime; + bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; + bool isConnected = SpacetimeDBClient.instance.IsConnected(); + + if (hasUpdatedRecently || !isConnected) + { + return; + } + + lastUpdateTime = Time.time; + var p = PlayerMovementController.Local.GetModelPosition(); + + Reducer.UpdatePlayerPosition(new StdbVector3 + { + X = p.x, + Y = p.y, + Z = p.z, + }, + PlayerMovementController.Local.GetModelRotation(), + PlayerMovementController.Local.IsMoving()); +} +``` + +Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. + +![GameManager-Inspector2](GameManager-Inspector2.JPG) + +### Play the Game! + +Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. + +![Unity-AddOpenScenes](Unity-AddOpenScenes.JPG) + +When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. + +### Implement Player Logout + +So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. + +**Append to the bottom of Start() function in TutorialGameManager.cs** +```csharp +PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; +``` + +We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. + +Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. + +**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** +```csharp +private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) +{ + OnPlayerComponentChanged(newValue); +} + +private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) +{ + OnPlayerComponentChanged(obj); +} + +private void OnPlayerComponentChanged(PlayerComponent obj) +{ + // If the identity of the PlayerComponent matches our user identity then this is the local player + if(obj.OwnerId == local_identity) + { + // Now that we have our initial position we can start the game + StartGame(); + } + else + { + // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it + var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); + if (obj.LoggedIn) + { + // Only spawn remote players who aren't already spawned + if (existingPlayer == null) + { + // Spawn the player object and attach the RemotePlayer component + var remotePlayer = Instantiate(PlayerPrefab); + + // Lookup and apply the position for this new player + var entity = EntityComponent.FilterByEntityId(obj.EntityId); + var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + remotePlayer.transform.position = position; + + var movementController = remotePlayer.GetComponent(); + movementController.RemoteTargetPosition = position; + movementController.RemoteTargetRotation = entity.Direction; + + remotePlayer.AddComponent().EntityId = obj.EntityId; + } + } + else + { + if (existingPlayer != null) + { + Destroy(existingPlayer.gameObject); + } + } + } +} +``` + +Now you when you play the game you should see remote players disappear when they log out. + +Before updating the client, let's generate the client files and update publish our module. + +**Execute commands in the server/ directory** +```bash +spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp +spacetime publish -c unity-tutorial +``` + +On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. + +**Append to the top of UIChatController.cs:** +```csharp +using SpacetimeDB.Types; +``` + +**REPLACE the OnChatButtonPress function in UIChatController.cs:** + +```csharp +public void OnChatButtonPress() +{ + Reducer.SendChatMessage(_chatInput.text); + _chatInput.text = ""; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: + +**Append to the bottom of the Start() function in TutorialGameManager.cs:** +```csharp +Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; +``` + +Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. + +**Append after the Start() function in TutorialGameManager.cs** +```csharp +private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) +{ + var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); + if (player != null) + { + UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); + } +} +``` + +Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. + +## Conclusion + +This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: + +**Next Unity Tutorial:** [Resources & Scheduling](part-41.md) + +--- + +### Troubleshooting + +- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` + +- If you get this exception when running the project: + +``` +NullReferenceException: Object reference not set to an instance of an object +TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) +``` + +Check to see if your GameManager object in the Scene has the NetworkManager component attached. + +- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. + +``` +Connection error: Unable to connect to the remote server +``` diff --git a/Writerside/topics/unity/part-4.md b/Writerside/topics/unity/part-4.md new file mode 100644 index 00000000..f17ac2b0 --- /dev/null +++ b/Writerside/topics/unity/part-4.md @@ -0,0 +1,261 @@ +# Unity Tutorial - Advanced - Part 4 - Resources and Scheduling + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 3](part-31.md) Tutorial. + +**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** + +In this second part of the lesson, we'll add resource nodes to our project and learn about scheduled reducers. Then we will spawn the nodes on the client so they are visible to the player. + +## Add Resource Node Spawner + +In this section we will add functionality to our server to spawn the resource nodes. + +### Step 1: Add the SpacetimeDB Tables for Resource Nodes + +1. Before we start adding code to the server, we need to add the ability to use the rand crate in our SpacetimeDB module so we can generate random numbers. Open the `Cargo.toml` file in the `Server` directory and add the following line to the `[dependencies]` section. + +```toml +rand = "0.8.5" +``` + +We also need to add the `getrandom` feature to our SpacetimeDB crate. Update the `spacetimedb` line to: + +```toml +spacetimedb = { "0.5", features = ["getrandom"] } +``` + +2. The first entity component we are adding, `ResourceNodeComponent`, stores the resource type. We'll define an enum to describe a `ResourceNodeComponent`'s type. For now, we'll just have one resource type: Iron. In the future, though, we'll add more resources by adding variants to the `ResourceNodeType` enum. Since we are using a custom enum, we need to mark it with the `SpacetimeType` attribute. Add the following code to lib.rs. + +```rust +#[derive(SpacetimeType, Clone)] +pub enum ResourceNodeType { + Iron, +} + +#[spacetimedb(table)] +#[derive(Clone)] +pub struct ResourceNodeComponent { + #[primarykey] + pub entity_id: u64, + + // Resource type of this resource node + pub resource_type: ResourceNodeType, +} +``` + +Because resource nodes never move, the `MobileEntityComponent` is overkill. Instead, we will add a new entity component named `StaticLocationComponent` that only stores the position and rotation. + +```rust +#[spacetimedb(table)] +#[derive(Clone)] +pub struct StaticLocationComponent { + #[primarykey] + pub entity_id: u64, + + pub location: StdbVector2, + pub rotation: f32, +} +``` + +3. We are also going to add a couple of additional column to our Config table. `map_extents` let's our spawner know where it can spawn the nodes. `num_resource_nodes` is the maximum number of nodes to spawn on the map. Update the config table in lib.rs. + +```rust +#[spacetimedb(table)] +pub struct Config { + // Config is a global table with a single row. This table will be used to + // store configuration or global variables + + #[primarykey] + // always 0 + // having a table with a primarykey field which is always zero is a way to store singleton global state + pub version: u32, + + pub message_of_the_day: String, + + // new variables for resource node spawner + // X and Z range of the map (-map_extents to map_extents) + pub map_extents: u32, + // maximum number of resource nodes to spawn on the map + pub num_resource_nodes: u32, +} +``` + +4. In the `init` reducer, we need to set the initial values of our two new variables. Update the following code: + +```rust + Config::insert(Config { + version: 0, + message_of_the_day: "Hello, World!".to_string(), + + // new variables for resource node spawner + map_extents: 25, + num_resource_nodes: 10, + }) + .expect("Failed to insert config."); +``` + +### Step 2: Write our Resource Spawner Repeating Reducer + +1. Add the following code to lib.rs. We are using a special attribute argument called repeat which will automatically schedule the reducer to run every 1000ms. + +```rust +#[spacetimedb(reducer, repeat = 1000ms)] +pub fn resource_spawner_agent(_ctx: ReducerContext, _prev_time: Timestamp) -> Result<(), String> { + let config = Config::filter_by_version(&0).unwrap(); + + // Retrieve the maximum number of nodes we want to spawn from the Config table + let num_resource_nodes = config.num_resource_nodes as usize; + + // Count the number of nodes currently spawned and exit if we have reached num_resource_nodes + let num_resource_nodes_spawned = ResourceNodeComponent::iter().count(); + if num_resource_nodes_spawned >= num_resource_nodes { + log::info!("All resource nodes spawned. Skipping."); + return Ok(()); + } + + // Pick a random X and Z based off the map_extents + let mut rng = rand::thread_rng(); + let map_extents = config.map_extents as f32; + let location = StdbVector2 { + x: rng.gen_range(-map_extents..map_extents), + z: rng.gen_range(-map_extents..map_extents), + }; + // Pick a random Y rotation in degrees + let rotation = rng.gen_range(0.0..360.0); + + // Insert our SpawnableEntityComponent which assigns us our entity_id + let entity_id = SpawnableEntityComponent::insert(SpawnableEntityComponent { entity_id: 0 }) + .expect("Failed to create resource spawnable entity component.") + .entity_id; + + // Insert our static location with the random position and rotation we selected + StaticLocationComponent::insert(StaticLocationComponent { + entity_id, + location: location.clone(), + rotation, + }) + .expect("Failed to insert resource static location component."); + + // Insert our resource node component, so far we only have iron + ResourceNodeComponent::insert(ResourceNodeComponent { + entity_id, + resource_type: ResourceNodeType::Iron, + }) + .expect("Failed to insert resource node component."); + + // Log that we spawned a node with the entity_id and location + log::info!( + "Resource node spawned: {} at ({}, {})", + entity_id, + location.x, + location.z, + ); + + Ok(()) +} +``` + +2. Since this reducer uses `rand::Rng` we need add include it. Add this `use` statement to the top of lib.rs. + +```rust +use rand::Rng; +``` + +3. Even though our reducer is set to repeat, we still need to schedule it the first time. Add the following code to the end of the `init` reducer. You can use this `schedule!` macro to schedule any reducer to run in the future after a certain amount of time. + +```rust + // Start our resource spawner repeating reducer + spacetimedb::schedule!("1000ms", resource_spawner_agent(_, Timestamp::now())); +``` + +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: + +```bash +spacetime generate --out-dir ../Assets/autogen --lang=csharp + +spacetime publish -c yourname/bitcraftmini +``` + +Your resource node spawner will start as soon as you publish since we scheduled it to run in our init reducer. You can watch the log output by using the `--follow` flag on the logs CLI command. + +```bash +spacetime logs -f yourname/bitcraftmini +``` + +### Step 3: Spawn the Resource Nodes on the Client + +1. First we need to update the `GameResource` component in Unity to work for multiplayer. Open GameResource.cs and add `using SpacetimeDB.Types;` to the top of the file. Then change the variable `Type` to be of type `ResourceNodeType` instead of `int`. Also add a new variable called `EntityId` of type `ulong`. + +```csharp + public ulong EntityId; + + public ResourceNodeType Type = ResourceNodeType.Iron; +``` + +2. Now that we've changed the `Type` variable, we need to update the code in the `PlayerAnimator` component that references it. Open PlayerAnimator.cs and update the following section of code. We need to add `using SpacetimeDB.Types;` to this file as well. This fixes the compile errors that result from changing the type of the `Type` variable to our new server generated enum. + +```csharp + var resourceType = res?.Type ?? ResourceNodeType.Iron; + switch (resourceType) + { + case ResourceNodeType.Iron: + _animator.SetTrigger("Mine"); + Interacting = true; + break; + default: + Interacting = false; + break; + } + for (int i = 0; i < _tools.Length; i++) + { + _tools[i].SetActive(((int)resourceType) == i); + } + _target = res; +``` + +3. Now that our `GameResource` is ready to be spawned, lets update the `BitcraftMiniGameManager` component to actually create them. First, we need to add the new tables to our SpacetimeDB subscription. Open BitcraftMiniGameManager.cs and update the following code: + +```csharp + SpacetimeDBClient.instance.Subscribe(new List() + { + "SELECT * FROM Config", + "SELECT * FROM SpawnableEntityComponent", + "SELECT * FROM PlayerComponent", + "SELECT * FROM MobileEntityComponent", + // Our new tables for part 2 of the tutorial + "SELECT * FROM ResourceNodeComponent", + "SELECT * FROM StaticLocationComponent" + }); +``` + +4. Next let's add an `OnInsert` handler for the `ResourceNodeComponent`. Add the following line to the `Start` function. + +```csharp + ResourceNodeComponent.OnInsert += ResourceNodeComponent_OnInsert; +``` + +5. Finally we add the new function to handle the insert event. This function will be called whenever a new `ResourceNodeComponent` is inserted into our local client cache. We can use this to spawn the resource node in the world. Add the following code to the `BitcraftMiniGameManager` class. + +To get the position and the rotation of the node, we look up the `StaticLocationComponent` for this entity by using the EntityId. + +```csharp + private void ResourceNodeComponent_OnInsert(ResourceNodeComponent insertedValue, ReducerEvent callInfo) + { + switch(insertedValue.ResourceType) + { + case ResourceNodeType.Iron: + var iron = Instantiate(IronPrefab); + StaticLocationComponent loc = StaticLocationComponent.FilterByEntityId(insertedValue.EntityId); + Vector3 nodePos = new Vector3(loc.Location.X, 0.0f, loc.Location.Z); + iron.transform.position = new Vector3(nodePos.x, MathUtil.GetTerrainHeight(nodePos), nodePos.z); + iron.transform.rotation = Quaternion.Euler(0.0f, loc.Rotation, 0.0f); + break; + } + } +``` + +### Step 4: Play the Game! + +6. Hit Play in the Unity Editor and you should now see your resource nodes spawning in the world! diff --git a/Writerside/topics/unity/part-5.md b/Writerside/topics/unity/part-5.md new file mode 100644 index 00000000..d4274636 --- /dev/null +++ b/Writerside/topics/unity/part-5.md @@ -0,0 +1,108 @@ +# Unity Tutorial - Advanced - Part 5 - BitCraft Mini + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 4](part-31.md) Tutorial. + +**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** + +BitCraft Mini is a game that we developed which extends the code you've already developed in this tutorial. It is inspired by our game [BitCraft](https://bitcraftonline.com) and illustrates how you could build a more complex game from just the components we've discussed. Right now you can walk around, mine ore, and manage your inventory. + +## 1. Download + +You can git-clone BitCraftMini from here: + +```plaintext +git clone ssh://git@github.com/clockworklabs/BitCraftMini +``` + +Once you have downloaded BitCraftMini, you will need to compile the spacetime module. + +## 2. Compile the Spacetime Module + +In order to compile the BitCraftMini module, you will need to install cargo. You can install cargo from here: + +> https://www.rust-lang.org/tools/install + +Once you have cargo installed, you can compile and publish the module with these commands: + +```bash +cd BitCraftMini/Server +spacetime publish +``` + +`spacetime publish` will output an address where your module has been deployed to. You will want to copy/save this address because you will need it in step 3. Here is an example of what it should look like: + +```plaintext +$ spacetime publish +info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date + Finished release [optimized] target(s) in 0.03s +Publish finished successfully. +Created new database with address: c91c17ecdcea8a05302be2bad9dd59b3 +``` + +Optionally, you can specify a name when you publish the module: + +```bash +spacetime publish "unique-module-name" +``` + +Currently, all the named modules exist in the same namespace so if you get a message saying that database is not owned by you, it means that someone else has already published a module with that name. You can either choose a different name or you can use the address instead. If you specify a name when you publish, you can use that name in place of the autogenerated address in both the CLI and in the Unity client. + +In the BitCraftMini module we have a function called `initialize()`. This function should be called immediately after publishing the module to spacetimedb. This function is in charge of generating some initial settings that are required for the server to operate. You can call this function like so: + +```bash +spacetime call "" "initialize" "[]" +``` + +Here we are telling spacetime to invoke the `initialize()` function on our module "bitcraftmini". If the function had some arguments, we would json encode them and put them into the "[]". Since `initialize()` requires no parameters, we just leave it empty. + +After you have called `initialize()` on the spacetime module you shouldgenerate the client files: + +```bash +spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs +``` + +Here is some sample output: + +```plaintext +$ spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang cs +info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date + Finished release [optimized] target(s) in 0.03s +compilation took 234.613518ms +Generate finished successfully. +``` + +If you've gotten this message then everything should be working properly so far. + +## 3. Replace address in BitCraftMiniGameManager + +The following settings are exposed in the `BitCraftMiniGameManager` inspector: Module Address, Host Name, and SSL Enabled. + +Open the Main scene in Unity and click on the `GameManager` object in the heirarchy. The inspector window will look like this: + +![GameManager-Inspector](GameManager-Inspector.JPG) + +Update the module address with the address you got from the `spacetime publish` command. If you are using SpacetimeDB Cloud `testnet`, the host name should be `testnet.spacetimedb.com` and SSL Enabled should be checked. If you are running SpacetimeDB Standalone locally, the host name should be `localhost:3000` and SSL Enabled should be unchecked. For instructions on how to deploy to these environments, see the [Deployment Section](testnet.) + +## 4. Play Mode + +You should now be able to enter play mode and walk around! You can mine some rocks, cut down some trees and if you connect more clients you can trade with other players. + +## 5. Editing the Module + +If you want to make further updates to the module, make sure to use this publish command instead: + +```bash +spacetime publish +``` + +Where `` is your own address. If you do this instead then you won't have to change the address inside of `BitCraftMiniGameManager.cs` + +When you change the server module you should also regenerate the client files as well: + +```bash +spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs +``` + +You may want to consider putting these 2 commands into a simple shell script to make the process a bit cleaner. diff --git a/Writerside/topics/unity/unity_index.md b/Writerside/topics/unity/unity_index.md new file mode 100644 index 00000000..a16870f0 --- /dev/null +++ b/Writerside/topics/unity/unity_index.md @@ -0,0 +1,24 @@ +# Unity Tutorial Overview + +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! + +The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, Git, using a commandline terminal and coding. + +We'll give you some CLI commands to execute. If you are using Windows, we recommend using Git Bash or PowerShell. For Mac, we recommend Terminal. + +Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll be opening .cs files in an IDE like _Visual Studio_ or _Rider_. + +## Unity Tutorial - Basic Multiplayer +Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](rust.) or [C#](c-sharp.): + +- [Part 1 - Setup](part-11.md) +- [Part 2 - Server (C#)](part-21.md) ☼ +- [Part 3 - Client (Unity)](part-31.md) + +☼ While the tutorial uses C#, the repo cloned in [Part 1](part-11.md) does include a legacy Rust example to optionally use, instead. + +## Unity Tutorial - Advanced +By this point, you should already have a basic understanding of SpacetimeDB client, server and CLI: + +- [Part 4 - Resources & Scheduling](part-41.md) +- [Part 5 - BitCraft Mini](part-51.md) diff --git a/Writerside/topics/webassembly-abi/webassembly-abi_index.md b/Writerside/topics/webassembly-abi/webassembly-abi_index.md new file mode 100644 index 00000000..ceccfbd1 --- /dev/null +++ b/Writerside/topics/webassembly-abi/webassembly-abi_index.md @@ -0,0 +1,499 @@ +# Module ABI Reference + +This document specifies the _low level details_ of module-host interactions (_"Module ABI"_). _**Most users**_ looking to interact with the host will want to use derived and higher level functionality like [`bindings`], `#[spacetimedb(table)]`, and `#[derive(SpacetimeType)]` rather than this low level ABI. For more on those, read the [Rust module quick start][module_quick_start] guide and the [Rust module reference][module_ref]. + +The Module ABI is defined in [`bindings_sys::raw`] and is used by modules to interact with their host and perform various operations like: + +- logging, +- transporting data, +- scheduling reducers, +- altering tables, +- inserting and deleting rows, +- querying tables. + +In the next few sections, we'll define the functions that make up the ABI and what these functions do. + +## General notes + +The functions in this ABI all use the [`C` ABI on the `wasm32` platform][wasm_c_abi]. They are specified in a Rust `extern "C" { .. }` block. For those more familiar with the `C` notation, an [appendix][c_header] is provided with equivalent definitions as would occur in a `.h` file. + +Many functions in the ABI take in- or out-pointers, e.g. `*const u8` and `*mut u8`. The WASM host itself does not have undefined behavior. However, what WASM does not consider a memory access violation could be one according to some other language's abstract machine. For example, running the following on a WASM host would violate Rust's rules around writing across allocations: + +```rust +fn main() { + let mut bytes = [0u8; 12]; + let other_bytes = [0u8; 4]; + unsafe { ffi_func_with_out_ptr_and_len(&mut bytes as *mut u8, 16); } + assert_eq!(other_bytes, [0u8; 4]); +} +``` + +When we note in this reference that traps occur or errors are returned on memory access violations, we only mean those that WASM can directly detected, and not cases like the one above. + +Should memory access violations occur, such as a buffer overrun, undefined behavior will never result, as it does not exist in WASM. However, in many cases, an error code will result. + +Some functions will treat UTF-8 strings _lossily_. That is, if the slice identified by a `(ptr, len)` contains non-UTF-8 bytes, these bytes will be replaced with `�` in the read string. + +Most functions return a `u16` value. This is how these functions indicate an error where a `0` value means that there were no errors. Such functions will instead return any data they need to through out pointers. + +## Logging + +```rust +/// The error log level. +const LOG_LEVEL_ERROR: u8 = 0; +/// The warn log level. +const LOG_LEVEL_WARN: u8 = 1; +/// The info log level. +const LOG_LEVEL_INFO: u8 = 2; +/// The debug log level. +const LOG_LEVEL_DEBUG: u8 = 3; +/// The trace log level. +const LOG_LEVEL_TRACE: u8 = 4; +/// The panic log level. +/// +/// A panic level is emitted just before +/// a fatal error causes the WASM module to trap. +const LOG_LEVEL_PANIC: u8 = 101; + +/// Log at `level` a `text` message occuring in `filename:line_number` +/// with `target` being the module path at the `log!` invocation site. +/// +/// These various pointers are interpreted lossily as UTF-8 strings. +/// The data pointed to are copied. Ownership does not transfer. +/// +/// See https://docs.rs/log/latest/log/struct.Record.html#method.target +/// for more info on `target`. +/// +/// Calls to the function cannot fail +/// irrespective of memory access violations. +/// If they occur, no message is logged. +fn _console_log( + // The level we're logging at. + // One of the `LOG_*` constants above. + level: u8, + // The module path, if any, associated with the message + // or to "blame" for the reason we're logging. + // + // This is a pointer to a buffer holding an UTF-8 encoded string. + // When the pointer is `NULL`, `target` is ignored. + target: *const u8, + // The length of the buffer pointed to by `text`. + // Unused when `target` is `NULL`. + target_len: usize, + // The file name, if any, associated with the message + // or to "blame" for the reason we're logging. + // + // This is a pointer to a buffer holding an UTF-8 encoded string. + // When the pointer is `NULL`, `filename` is ignored. + filename: *const u8, + // The length of the buffer pointed to by `text`. + // Unused when `filename` is `NULL`. + filename_len: usize, + // The line number associated with the message + // or to "blame" for the reason we're logging. + line_number: u32, + // A pointer to a buffer holding an UTF-8 encoded message to log. + text: *const u8, + // The length of the buffer pointed to by `text`. + text_len: usize, +); +``` + +## Buffer handling + +```rust +/// Returns the length of buffer `bufh` without +/// transferring ownership of the data into the function. +/// +/// The `bufh` must have previously been allocating using `_buffer_alloc`. +/// +/// Traps if the buffer does not exist. +fn _buffer_len( + // The buffer previously allocated using `_buffer_alloc`. + // Ownership of the buffer is not taken. + bufh: ManuallyDrop +) -> usize; + +/// Consumes the buffer `bufh`, +/// moving its contents to the WASM byte slice `(ptr, len)`. +/// +/// Returns an error if the buffer does not exist +/// or on any memory access violations associated with `(ptr, len)`. +fn _buffer_consume( + // The buffer to consume and move into `(ptr, len)`. + // Ownership of the buffer and its contents are taken. + // That is, `bufh` won't be usable after this call. + bufh: Buffer, + // A WASM out pointer to write the contents of `bufh` to. + ptr: *mut u8, + // The size of the buffer pointed to by `ptr`. + // This size must match that of `bufh` or a trap will occur. + len: usize +); + +/// Creates a buffer of size `data_len` in the host environment. +/// +/// The contents of the byte slice lasting `data_len` bytes +/// at the `data` WASM pointer are read +/// and written into the newly initialized buffer. +/// +/// Traps on any memory access violations. +fn _buffer_alloc(data: *const u8, data_len: usize) -> Buffer; +``` + +## Reducer scheduling + +```rust +/// Schedules a reducer to be called asynchronously at `time`. +/// +/// The reducer is named as the valid UTF-8 slice `(name, name_len)`, +/// and is passed the slice `(args, args_len)` as its argument. +/// +/// A generated schedule id is assigned to the reducer. +/// This id is written to the pointer `out`. +/// +/// Errors on any memory access violations, +/// if `(name, name_len)` does not point to valid UTF-8, +/// or if the `time` delay exceeds `64^6 - 1` milliseconds from now. +fn _schedule_reducer( + // A pointer to a buffer + // with a valid UTF-8 string of `name_len` many bytes. + name: *const u8, + // The number of bytes in the `name` buffer. + name_len: usize, + // A pointer to a byte buffer of `args_len` many bytes. + args: *const u8, + // The number of bytes in the `args` buffer. + args_len: usize, + // When to call the reducer. + time: u64, + // The schedule ID is written to this out pointer on a successful call. + out: *mut u64, +); + +/// Unschedules a reducer +/// using the same `id` generated as when it was scheduled. +/// +/// This assumes that the reducer hasn't already been executed. +fn _cancel_reducer(id: u64); +``` + +## Altering tables + +```rust +/// Creates an index with the name `index_name` and type `index_type`, +/// on a product of the given columns in `col_ids` +/// in the table identified by `table_id`. +/// +/// Here `index_name` points to a UTF-8 slice in WASM memory +/// and `col_ids` points to a byte slice in WASM memory +/// with each element being a column. +/// +/// Currently only single-column-indices are supported +/// and they may only be of the btree index type. +/// In the former case, the function will panic, +/// and in latter, an error is returned. +/// +/// Returns an error on any memory access violations, +/// if `(index_name, index_name_len)` is not valid UTF-8, +/// or when a table with the provided `table_id` doesn't exist. +/// +/// Traps if `index_type /= 0` or if `col_len /= 1`. +fn _create_index( + // A pointer to a buffer holding an UTF-8 encoded index name. + index_name: *const u8, + // The length of the buffer pointed to by `index_name`. + index_name_len: usize, + // The ID of the table to create the index for. + table_id: u32, + // The type of the index. + // Must be `0` currently, that is, a btree-index. + index_type: u8, + // A pointer to a buffer holding a byte slice + // where each element is the position + // of a column to include in the index. + col_ids: *const u8, + // The length of the byte slice in `col_ids`. Must be `1`. + col_len: usize, +) -> u16; +``` + +## Inserting and deleting rows + +```rust +/// Inserts a row into the table identified by `table_id`, +/// where the row is read from the byte slice `row_ptr` in WASM memory, +/// lasting `row_len` bytes. +/// +/// Errors if there were unique constraint violations, +/// if there were any memory access violations in associated with `row`, +/// if the `table_id` doesn't identify a table, +/// or if `(row, row_len)` doesn't decode from BSATN to a `ProductValue` +/// according to the `ProductType` that the table's schema specifies. +fn _insert( + // The table to insert the row into. + // The interpretation of `(row, row_len)` depends on this ID + // as it's table schema determines how to decode the raw bytes. + table_id: u32, + // An in/out pointer to a byte buffer + // holding the BSATN-encoded `ProductValue` row data to insert. + // + // The pointer is written to with the inserted row re-encoded. + // This is due to auto-incrementing columns. + row: *mut u8, + // The length of the buffer pointed to by `row`. + row_len: usize +) -> u16; + +/// Deletes all rows in the table identified by `table_id` +/// where the column identified by `col_id` matches the byte string, +/// in WASM memory, pointed to by `value`. +/// +/// Matching is defined by decoding of `value` to an `AlgebraicValue` +/// according to the column's schema and then `Ord for AlgebraicValue`. +/// +/// The number of rows deleted is written to the WASM pointer `out`. +/// +/// Errors if there were memory access violations +/// associated with `value` or `out`, +/// if no columns were deleted, +/// or if the column wasn't found. +fn _delete_by_col_eq( + // The table to delete rows from. + table_id: u32, + // The position of the column to match `(value, value_len)` against. + col_id: u32, + // A pointer to a byte buffer holding a BSATN-encoded `AlgebraicValue` + // of the `AlgebraicType` that the table's schema specifies + // for the column identified by `col_id`. + value: *const u8, + // The length of the buffer pointed to by `value`. + value_len: usize, + // An out pointer that the number of rows deleted is written to. + out: *mut u32 +) -> u16; +``` + +## Querying tables + +```rust +/// Queries the `table_id` associated with the given (table) `name` +/// where `name` points to a UTF-8 slice +/// in WASM memory of `name_len` bytes. +/// +/// The table id is written into the `out` pointer. +/// +/// Errors on memory access violations associated with `name` +/// or if the table does not exist. +fn _get_table_id( + // A pointer to a buffer holding the name of the table + // as a valid UTF-8 encoded string. + name: *const u8, + // The length of the buffer pointed to by `name`. + name_len: usize, + // An out pointer to write the table ID to. + out: *mut u32 +) -> u16; + +/// Finds all rows in the table identified by `table_id`, +/// where the row has a column, identified by `col_id`, +/// with data matching the byte string, +/// in WASM memory, pointed to at by `val`. +/// +/// Matching is defined by decoding of `value` +/// to an `AlgebraicValue` according to the column's schema +/// and then `Ord for AlgebraicValue`. +/// +/// The rows found are BSATN encoded and then concatenated. +/// The resulting byte string from the concatenation +/// is written to a fresh buffer +/// with the buffer's identifier written to the WASM pointer `out`. +/// +/// Errors if no table with `table_id` exists, +/// if `col_id` does not identify a column of the table, +/// if `(value, value_len)` cannot be decoded to an `AlgebraicValue` +/// typed at the `AlgebraicType` of the column, +/// or if memory access violations occurred associated with `value` or `out`. +fn _iter_by_col_eq( + // Identifies the table to find rows in. + table_id: u32, + // The position of the column in the table + // to match `(value, value_len)` against. + col_id: u32, + // A pointer to a byte buffer holding a BSATN encoded + // value typed at the `AlgebraicType` of the column. + value: *const u8, + // The length of the buffer pointed to by `value`. + value_len: usize, + // An out pointer to which the new buffer's id is written to. + out: *mut Buffer +) -> u16; + +/// Starts iteration on each row, as bytes, +/// of a table identified by `table_id`. +/// +/// The iterator is registered in the host environment +/// under an assigned index which is written to the `out` pointer provided. +/// +/// Errors if the table doesn't exist +/// or if memory access violations occurred in association with `out`. +fn _iter_start( + // The ID of the table to start row iteration on. + table_id: u32, + // An out pointer to which an identifier + // to the newly created buffer is written. + out: *mut BufferIter +) -> u16; + +/// Like [`_iter_start`], starts iteration on each row, +/// as bytes, of a table identified by `table_id`. +/// +/// The rows are filtered through `filter`, which is read from WASM memory +/// and is encoded in the embedded language defined by `spacetimedb_lib::filter::Expr`. +/// +/// The iterator is registered in the host environment +/// under an assigned index which is written to the `out` pointer provided. +/// +/// Errors if `table_id` doesn't identify a table, +/// if `(filter, filter_len)` doesn't decode to a filter expression, +/// or if there were memory access violations +/// in association with `filter` or `out`. +fn _iter_start_filtered( + // The ID of the table to start row iteration on. + table_id: u32, + // A pointer to a buffer holding an encoded filter expression. + filter: *const u8, + // The length of the buffer pointed to by `filter`. + filter_len: usize, + // An out pointer to which an identifier + // to the newly created buffer is written. + out: *mut BufferIter +) -> u16; + +/// Advances the registered iterator with the index given by `iter_key`. +/// +/// On success, the next element (the row as bytes) is written to a buffer. +/// The buffer's index is returned and written to the `out` pointer. +/// If there are no elements left, an invalid buffer index is written to `out`. +/// On failure however, the error is returned. +/// +/// Errors if `iter` does not identify a registered `BufferIter`, +/// or if there were memory access violations in association with `out`. +fn _iter_next( + // An identifier for the iterator buffer to advance. + // Ownership of the buffer nor the identifier is moved into the function. + iter: ManuallyDrop, + // An out pointer to write the newly created buffer's identifier to. + out: *mut Buffer +) -> u16; + +/// Drops the entire registered iterator with the index given by `iter_key`. +/// The iterator is effectively de-registered. +/// +/// Returns an error if the iterator does not exist. +fn _iter_drop( + // An identifier for the iterator buffer to unregister / drop. + iter: ManuallyDrop +) -> u16; +``` + +## Appendix, `bindings.h` + +```c +#include +#include +#include +#include +#include + +typedef uint32_t Buffer; +typedef uint32_t BufferIter; + +void _console_log( + uint8_t level, + const uint8_t *target, + size_t target_len, + const uint8_t *filename, + size_t filename_len, + uint32_t line_number, + const uint8_t *text, + size_t text_len +); + + +Buffer _buffer_alloc( + const uint8_t *data, + size_t data_len +); +void _buffer_consume( + Buffer bufh, + uint8_t *into, + size_t len +); +size_t _buffer_len(Buffer bufh); + + +void _schedule_reducer( + const uint8_t *name, + size_t name_len, + const uint8_t *args, + size_t args_len, + uint64_t time, + uint64_t *out +); +void _cancel_reducer(uint64_t id); + + +uint16_t _create_index( + const uint8_t *index_name, + size_t index_name_len, + uint32_t table_id, + uint8_t index_type, + const uint8_t *col_ids, + size_t col_len +); + + +uint16_t _insert( + uint32_t table_id, + uint8_t *row, + size_t row_len +); +uint16_t _delete_by_col_eq( + uint32_t table_id, + uint32_t col_id, + const uint8_t *value, + size_t value_len, + uint32_t *out +); + + +uint16_t _get_table_id( + const uint8_t *name, + size_t name_len, + uint32_t *out +); +uint16_t _iter_by_col_eq( + uint32_t table_id, + uint32_t col_id, + const uint8_t *value, + size_t value_len, + Buffer *out +); +uint16_t _iter_drop(BufferIter iter); +uint16_t _iter_next(BufferIter iter, Buffer *out); +uint16_t _iter_start(uint32_t table_id, BufferIter *out); +uint16_t _iter_start_filtered( + uint32_t table_id, + const uint8_t *filter, + size_t filter_len, + BufferIter *out +); +``` + +[`bindings_sys::raw`]: https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/bindings-sys/src/lib.rs#L44-L215 +[`bindings`]: https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/bindings/src/lib.rs +[module_ref]: /docs/languages/rust/rust-module-reference +[module_quick_start]: /docs/languages/rust/rust-module-quick-start +[wasm_c_abi]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md +[c_header]: #appendix-bindingsh diff --git a/Writerside/topics/ws/ws_index.md b/Writerside/topics/ws/ws_index.md new file mode 100644 index 00000000..4814bb45 --- /dev/null +++ b/Writerside/topics/ws/ws_index.md @@ -0,0 +1,318 @@ +# The SpacetimeDB WebSocket API + +As an extension of the [HTTP API](http-api-reference.), SpacetimeDB offers a WebSocket API. Clients can subscribe to a database via a WebSocket connection to receive streaming updates as the database changes, and send requests to invoke reducers. Messages received from the server over a WebSocket will follow the same total ordering of transactions as are committed to the database. + +The SpacetimeDB SDKs comminicate with their corresponding database using the WebSocket API. + +## Connecting + +To initiate a WebSocket connection, send a `GET` request to the [`/database/subscribe/:name_or_address` endpoint](database#databasesubscribename_or_address-get.) with headers appropriate to upgrade to a WebSocket connection as per [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). + +To re-connect with an existing identity, include its token in a [SpacetimeDB Authorization header](http.). Otherwise, a new identity and token will be generated for the client. + +## Protocols + +Clients connecting via WebSocket can choose between two protocols, [`v1.bin.spacetimedb`](#binary-protocol.) and [`v1.text.spacetimedb`](#text-protocol.). Clients should include one of these protocols in the `Sec-WebSocket-Protocol` header of their request. + +| `Sec-WebSocket-Protocol` header value | Selected protocol | +| ------------------------------------- | -------------------------- | +| `v1.bin.spacetimedb` | [Binary](#binary-protocol.) | +| `v1.text.spacetimedb` | [Text](#text-protocol.) | + +### Binary Protocol + +The SpacetimeDB binary WebSocket protocol, `v1.bin.spacetimedb`, encodes messages using [ProtoBuf 3](https://protobuf.dev), and reducer and row data using [BSATN](bsatn.). + +The binary protocol's messages are defined in [`client_api.proto`](https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/client-api-messages/protobuf/client_api.proto). + +### Text Protocol + +The SpacetimeDB text WebSocket protocol, `v1.text.spacetimedb`, encodes messages, reducer and row data as JSON. Reducer arguments and table rows are JSON-encoded according to the [SATN JSON format](satn.). + +## Messages + +### Client to server + +| Message | Description | +| ------------------------------- | --------------------------------------------------------------------------- | +| [`FunctionCall`](#functioncall.) | Invoke a reducer. | +| [`Subscribe`](#subscribe.) | Register queries to receive streaming updates for a subset of the database. | + +#### `FunctionCall` + +Clients send a `FunctionCall` message to request that the database run a reducer. The message includes the reducer's name and a SATS `ProductValue` of arguments. + +##### Binary: ProtoBuf definition + +```protobuf +message FunctionCall { + string reducer = 1; + bytes argBytes = 2; +} +``` + +| Field | Value | +| ---------- | -------------------------------------------------------- | +| `reducer` | The name of the reducer to invoke. | +| `argBytes` | The reducer arguments encoded as a BSATN `ProductValue`. | + +##### Text: JSON encoding + +```typescript +{ + "call": { + "fn": string, + "args": array, + } +} +``` + +| Field | Value | +| ------ | ---------------------------------------------- | +| `fn` | The name of the reducer to invoke. | +| `args` | The reducer arguments encoded as a JSON array. | + +#### `Subscribe` + +Clients send a `Subscribe` message to register SQL queries in order to receive streaming updates. + +The client will only receive [`TransactionUpdate`s](#transactionupdate.) for rows to which it is subscribed, and for reducer runs which alter at least one subscribed row. As a special exception, the client is always notified when a reducer run it requests via a [`FunctionCall` message](#functioncall.) fails. + +SpacetimeDB responds to each `Subscribe` message with a [`SubscriptionUpdate` message](#subscriptionupdate.) containing all matching rows at the time the subscription is applied. + +Each `Subscribe` message establishes a new set of subscriptions, replacing all previous subscriptions. Clients which want to add a query to an existing subscription must send a `Subscribe` message containing all the previous queries in addition to the new query. In this case, the returned [`SubscriptionUpdate`](#subscriptionupdate.) will contain all previously-subscribed rows in addition to the newly-subscribed rows. + +Each query must be a SQL `SELECT * FROM` statement on a single table with an optional `WHERE` clause. See the [SQL Reference](sql.) for the subset of SQL supported by SpacetimeDB. + +##### Binary: ProtoBuf definition + +```protobuf +message Subscribe { + repeated string query_strings = 1; +} +``` + +| Field | Value | +| --------------- | ----------------------------------------------------------------- | +| `query_strings` | A sequence of strings, each of which contains a single SQL query. | + +##### Text: JSON encoding + +```typescript +{ + "subscribe": { + "query_strings": array + } +} +``` + +| Field | Value | +| --------------- | --------------------------------------------------------------- | +| `query_strings` | An array of strings, each of which contains a single SQL query. | + +### Server to client + +| Message | Description | +| ------------------------------------------- | -------------------------------------------------------------------------- | +| [`IdentityToken`](#identitytoken.) | Sent once upon successful connection with the client's identity and token. | +| [`SubscriptionUpdate`](#subscriptionupdate.) | Initial message in response to a [`Subscribe` message](#subscribe.). | +| [`TransactionUpdate`](#transactionupdate.) | Streaming update after a reducer runs containing altered rows. | + +#### `IdentityToken` + +Upon establishing a WebSocket connection, the server will send an `IdentityToken` message containing the client's identity and token. If the client included a [SpacetimeDB Authorization header](http.) in their connection request, the `IdentityToken` message will contain the same token used to connect, and its corresponding identity. If the client connected anonymously, SpacetimeDB will generate a new identity and token for the client. + +##### Binary: ProtoBuf definition + +```protobuf +message IdentityToken { + bytes identity = 1; + string token = 2; +} +``` + +| Field | Value | +| ---------- | --------------------------------------- | +| `identity` | The client's public Spacetime identity. | +| `token` | The client's private access token. | + +##### Text: JSON encoding + +```typescript +{ + "IdentityToken": { + "identity": array, + "token": string + } +} +``` + +| Field | Value | +| ---------- | --------------------------------------- | +| `identity` | The client's public Spacetime identity. | +| `token` | The client's private access token. | + +#### `SubscriptionUpdate` + +In response to a [`Subscribe` message](#subscribe.), the database sends a `SubscriptionUpdate` containing all of the matching rows which are resident in the database at the time the `Subscribe` was received. + +##### Binary: ProtoBuf definition + +```protobuf +message SubscriptionUpdate { + repeated TableUpdate tableUpdates = 1; +} + +message TableUpdate { + uint32 tableId = 1; + string tableName = 2; + repeated TableRowOperation tableRowOperations = 3; +} + +message TableRowOperation { + enum OperationType { + DELETE = 0; + INSERT = 1; + } + OperationType op = 1; + bytes row = 3; +} +``` + +Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribed rows. Each `TableUpdate` contains a `TableRowOperation` for each subscribed row. `SubscriptionUpdate`, `TableUpdate` and `TableRowOperation` are also used by the [`TransactionUpdate` message](#transactionupdate.) to encode rows altered by a reducer, so `TableRowOperation` includes an `OperationType` which identifies the row alteration as either an insert or a delete. When a client receives a `SubscriptionUpdate` message in response to a [`Subscribe` message](#subscribe.), all of the `TableRowOperation`s will have `op` of `INSERT`. + +| `TableUpdate` field | Value | +| -------------------- | ------------------------------------------------------------------------------------------------------------- | +| `tableId` | An integer identifier for the table. A table's `tableId` is not stable, so clients should not depend on it. | +| `tableName` | The string name of the table. Clients should use this field to identify the table, rather than the `tableId`. | +| `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`. | + +##### Text: JSON encoding + +```typescript +// SubscriptionUpdate: +{ + "SubscriptionUpdate": { + "table_updates": array + } +} + +// TableUpdate: +{ + "table_id": number, + "table_name": string, + "table_row_operations": array +} + +// TableRowOperation: +{ + "op": "insert" | "delete", + "row": array +} +``` + +Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribed rows. Each `TableUpdate` contains a `TableRowOperation` for each subscribed row. `SubscriptionUpdate`, `TableUpdate` and `TableRowOperation` are also used by the [`TransactionUpdate` message](#transactionupdate.) to encode rows altered by a reducer, so `TableRowOperation` includes an `"op"` field which identifies the row alteration as either an insert or a delete. When a client receives a `SubscriptionUpdate` message in response to a [`Subscribe` message](#subscribe.), all of the `TableRowOperation`s will have `"op"` of `"insert"`. + +| `TableUpdate` field | Value | +| ---------------------- | -------------------------------------------------------------------------------------------------------------- | +| `table_id` | An integer identifier for the table. A table's `table_id` is not stable, so clients should not depend on it. | +| `table_name` | The string name of the table. Clients should use this field to identify the table, rather than the `table_id`. | +| `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. | + +#### `TransactionUpdate` + +Upon a reducer run, a client will receive a `TransactionUpdate` containing information about the reducer which ran and the subscribed rows which it altered. Clients will only receive a `TransactionUpdate` for a reducer invocation if either of two criteria is met: + +1. The reducer ran successfully and altered at least one row to which the client subscribes. +2. The reducer was invoked by the client, and either failed or was terminated due to insufficient energy. + +Each `TransactionUpdate` contains a [`SubscriptionUpdate`](#subscriptionupdate.) with all rows altered by the reducer, including inserts and deletes; and an `Event` with information about the reducer itself, including a [`FunctionCall`](#functioncall.) containing the reducer's name and arguments. + +##### Binary: ProtoBuf definition + +```protobuf +message TransactionUpdate { + Event event = 1; + SubscriptionUpdate subscriptionUpdate = 2; +} + +message Event { + enum Status { + committed = 0; + failed = 1; + out_of_energy = 2; + } + uint64 timestamp = 1; + bytes callerIdentity = 2; + FunctionCall functionCall = 3; + Status status = 4; + string message = 5; + int64 energy_quanta_used = 6; + uint64 host_execution_duration_micros = 7; +} +``` + +| Field | Value | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `event` | An `Event` containing information about the reducer run. | +| `subscriptionUpdate` | A [`SubscriptionUpdate`](#subscriptionupdate.) containing all the row insertions and deletions committed by the transaction. | + +| `Event` field | Value | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `timestamp` | The time when the reducer started, as microseconds since the Unix epoch. | +| `callerIdentity` | The identity of the client which requested the reducer invocation. For event-driven and scheduled reducers, this is the identity of the database owner. | +| `functionCall` | A [`FunctionCall`](#functioncall.) containing the name of the reducer and the arguments passed to it. | +| `status` | `committed` if the reducer ran successfully and its changes were committed to the database; `failed` if the reducer signaled an error; `out_of_energy` if the reducer was canceled due to insufficient energy. | +| `message` | The error message with which the reducer failed if `status` is `failed`, or the empty string otherwise. | +| `energy_quanta_used` | The amount of energy consumed by running the reducer. | +| `host_execution_duration_micros` | The duration of the reducer's execution, in microseconds. | + +##### Text: JSON encoding + +```typescript +// TransactionUpdate: +{ + "TransactionUpdate": { + "event": Event, + "subscription_update": SubscriptionUpdate + } +} + +// Event: +{ + "timestamp": number, + "status": "committed" | "failed" | "out_of_energy", + "caller_identity": string, + "function_call": { + "reducer": string, + "args": array, + }, + "energy_quanta_used": number, + "message": string +} +``` + +| Field | Value | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `event` | An `Event` containing information about the reducer run. | +| `subscription_update` | A [`SubscriptionUpdate`](#subscriptionupdate.) containing all the row insertions and deletions committed by the transaction. | + +| `Event` field | Value | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `timestamp` | The time when the reducer started, as microseconds since the Unix epoch. | +| `status` | `committed` if the reducer ran successfully and its changes were committed to the database; `failed` if the reducer signaled an error; `out_of_energy` if the reducer was canceled due to insufficient energy. | +| `caller_identity` | The identity of the client which requested the reducer invocation. For event-driven and scheduled reducers, this is the identity of the database owner. | +| `function_call.reducer` | The name of the reducer. | +| `function_call.args` | The reducer arguments encoded as a JSON array. | +| `energy_quanta_used` | The amount of energy consumed by running the reducer. | +| `message` | The error message with which the reducer failed if `status` is `failed`, or the empty string otherwise. | diff --git a/Writerside/v.list b/Writerside/v.list new file mode 100644 index 00000000..2d12cb39 --- /dev/null +++ b/Writerside/v.list @@ -0,0 +1,5 @@ + + + + + diff --git a/Writerside/writerside.cfg b/Writerside/writerside.cfg new file mode 100644 index 00000000..9b301c2a --- /dev/null +++ b/Writerside/writerside.cfg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Writerside2/c.list b/Writerside2/c.list new file mode 100644 index 00000000..c4c77a29 --- /dev/null +++ b/Writerside2/c.list @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Writerside2/cfg/buildprofiles.xml b/Writerside2/cfg/buildprofiles.xml new file mode 100644 index 00000000..a72eec8e --- /dev/null +++ b/Writerside2/cfg/buildprofiles.xml @@ -0,0 +1,13 @@ + + + + + + + true + false + + + + diff --git a/Writerside2/s.tree b/Writerside2/s.tree new file mode 100644 index 00000000..ebb0279d --- /dev/null +++ b/Writerside2/s.tree @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Writerside2/topics/bsatn.md b/Writerside2/topics/bsatn.md new file mode 100644 index 00000000..f8aeca7f --- /dev/null +++ b/Writerside2/topics/bsatn.md @@ -0,0 +1,115 @@ +# SATN Binary Format (BSATN) + +The Spacetime Algebraic Type Notation binary (BSATN) format defines +how Spacetime `AlgebraicValue`s and friends are encoded as byte strings. + +Algebraic values and product values are BSATN-encoded for e.g., +module-host communication and for storing row data in the database. + +## Notes on notation + +In this reference, we give a formal definition of the format. +To do this, we use inductive definitions, and define the following notation: + +- `bsatn(x)` denotes a function converting some value `x` to a list of bytes. +- `a: B` means that `a` is of type `B`. +- `Foo(x)` denotes extracting `x` out of some variant or type `Foo`. +- `a ++ b` denotes concatenating two byte lists `a` and `b`. +- `bsatn(A) = bsatn(B) | ... | bsatn(Z)` where `B` to `Z` are variants of `A` + means that `bsatn(A)` is defined as e.g., + `bsatn(B)`, `bsatn(C)`, .., `bsatn(Z)` depending on what variant of `A` it was. +- `[]` denotes the empty list of bytes. + +## Values + +### At a glance + +| Type | Description | +| ---------------- | ---------------------------------------------------------------- | +| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype.). | +| `SumValue` | A value whose type is a [`SumType`](#sumtype.). | +| `ProductValue` | A value whose type is a [`ProductType`](#producttype.). | +| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype.). | + +### `AlgebraicValue` + +The BSATN encoding of an `AlgebraicValue` defers to the encoding of each variant: + +```fsharp +bsatn(AlgebraicValue) = bsatn(SumValue) | bsatn(ProductValue) | bsatn(BuiltinValue) +``` + +### `SumValue` + +An instance of a [`SumType`](#sumtype.). +`SumValue`s are binary-encoded as `bsatn(tag) ++ bsatn(variant_data)` +where `tag: u8` is an index into the [`SumType.variants`](#sumtype.) +array of the value's [`SumType`](#sumtype.), +and where `variant_data` is the data of the variant. +For variants holding no data, i.e., of some zero sized type, +`bsatn(variant_data) = []`. + +### `ProductValue` + +An instance of a [`ProductType`](#producttype.). +`ProductValue`s are binary encoded as: + +```fsharp +bsatn(elems) = bsatn(elem_0) ++ .. ++ bsatn(elem_n) +``` + +Field names are not encoded. + +### `BuiltinValue` + +An instance of a [`BuiltinType`](#builtintype.). +The BSATN encoding of `BuiltinValue`s defers to the encoding of each variant: + +```fsharp +bsatn(BuiltinValue) + = bsatn(Bool) + | bsatn(U8) | bsatn(U16) | bsatn(U32) | bsatn(U64) | bsatn(U128) + | bsatn(I8) | bsatn(I16) | bsatn(I32) | bsatn(I64) | bsatn(I128) + | bsatn(F32) | bsatn(F64) + | bsatn(String) + | bsatn(Array) + | bsatn(Map) + +bsatn(Bool(b)) = bsatn(b as u8) +bsatn(U8(x)) = [x] +bsatn(U16(x: u16)) = to_little_endian_bytes(x) +bsatn(U32(x: u32)) = to_little_endian_bytes(x) +bsatn(U64(x: u64)) = to_little_endian_bytes(x) +bsatn(U128(x: u128)) = to_little_endian_bytes(x) +bsatn(I8(x: i8)) = to_little_endian_bytes(x) +bsatn(I16(x: i16)) = to_little_endian_bytes(x) +bsatn(I32(x: i32)) = to_little_endian_bytes(x) +bsatn(I64(x: i64)) = to_little_endian_bytes(x) +bsatn(I128(x: i128)) = to_little_endian_bytes(x) +bsatn(F32(x: f32)) = bsatn(f32_to_raw_bits(x)) // lossless conversion +bsatn(F64(x: f64)) = bsatn(f64_to_raw_bits(x)) // lossless conversion +bsatn(String(s)) = bsatn(len(s) as u32) ++ bsatn(bytes(s)) +bsatn(Array(a)) = bsatn(len(a) as u32) + ++ bsatn(normalize(a)_0) ++ .. ++ bsatn(normalize(a)_n) +bsatn(Map(map)) = bsatn(len(m) as u32) + ++ bsatn(key(map_0)) ++ bsatn(value(map_0)) + .. + ++ bsatn(key(map_n)) ++ bsatn(value(map_n)) +``` + +Where + +- `f32_to_raw_bits(x)` is the raw transmute of `x: f32` to `u32` +- `f64_to_raw_bits(x)` is the raw transmute of `x: f64` to `u64` +- `normalize(a)` for `a: ArrayValue` converts `a` to a list of `AlgebraicValue`s +- `key(map_i)` extracts the key of the `i`th entry of `map` +- `value(map_i)` extracts the value of the `i`th entry of `map` + +## Types + +All SATS types are BSATN-encoded by converting them to an `AlgebraicValue`, +then BSATN-encoding that meta-value. + +See [the SATN JSON Format](satn-reference-json-format.) +for more details of the conversion to meta values. +Note that these meta values are converted to BSATN and _not JSON_. diff --git a/Writerside2/topics/deploying/deploying_index.md b/Writerside2/topics/deploying/deploying_index.md new file mode 100644 index 00000000..658df48d --- /dev/null +++ b/Writerside2/topics/deploying/deploying_index.md @@ -0,0 +1,48 @@ +# Deploying Overview + +SpacetimeDB supports both hosted and self-hosted publishing in multiple ways. Below, we will: + +1. Generally introduce Identities. +1. Generally introduce Servers. +1Choose to proceed with either a [Hosted](hosted1.md) or [Self-Hosted](self-hosted1.md) deployment. + +💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. + +## About Identities + +An `Identity` is a hash attached to a `Nickname` and `Email`, allowing you to manage your app (such as `Publishing` your app). + +Each `Identity` is bound to one, single `Server`: Unlike GitHub, for example, you would require one identity per server. + +By default, there are no identities created. While the next tutorial will go more in-depth, you may create a new one via CLI: +```bash +spacetime identity new --name {Nickname} --email {Email} +``` + +See the verbose [overview identity explanation](https://spacetimedb.com/docs#identities), [API reference](identity1.md) or CLI help (below) for further managing `Identities`: +```bash +spacetime identity --help +``` + +## About Servers + +You `publish` your app to a target `Server` database: While we recommend to **host** your SpacetimeDB app with us for simplicity and scalability, you may also **self-host** (such as locally). + +By default, there are already two default servers added ([testnet](hosted1.md) and [local](self-hosted1.md)). While the next tutorial will go more in-depth, you may list your servers via CLI: +```bash +spacetime server list +``` + +See the [API reference](database1.md) or CLI help (below) for further managing `Servers`: +```bash +spacetime server --help +``` + +--- + +## Deploying via CLI + +Choose a server for your hosting tutorial path to set a server as default, create an identity, and deploy (`publish`) your app: + +1. [testnet](hosted1.md) (hosted) +2. [local](self-hosted1.md) (self-hosted) diff --git a/Writerside2/topics/deploying/hosted.md b/Writerside2/topics/deploying/hosted.md new file mode 100644 index 00000000..187eec4c --- /dev/null +++ b/Writerside2/topics/deploying/hosted.md @@ -0,0 +1,74 @@ +# Deploying - Hosted + +This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. Via CLI, we will then: + +1. Ensure our hosted server named `testnet` exists as the default. +1. Create an `Identity`. +1. `Publish` your app. + +💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI and that you already have `testnet` server added (exists by default). If you accidentally removed `testnet`, add it back via CLI: + +```bash +spacetime server add "https://testnet.spacetimedb.com" testnet +``` + +## SpacetimeDB Cloud (Hosted) Deployment + +Currently, for hosted deployment, only the `testnet` server is available for SpacetimeDB cloud, which is subject to wipes. + +📢 Stay tuned (such as [via Discord](https://discord.com/invite/SpacetimeDB)) for `mainnet` coming soon! + +## Set the Server Default + +To make CLI commands easier so that we don't need to keep specifying `testnet` as the target server, let's set it as default: + +```bash +spacetime server set-default testnet +``` + +## Creating an Identity + +By default, there are no identities created. Let's create a new one via CLI: +```bash +spacetime identity new --name {Nickname} --email {Email} +``` + +💡If you already created an identity but forgot to attach an email, add it via CLI: +```bash +spacetime identity set-email {Email} +``` + +## Create and Publish a Module + +Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: + +```bash +cd ~ +spacetime init --lang rust HelloSpacetimeDB +cd HelloSpacetimeDB +spacetime publish HelloSpacetimeDB +``` + +## Hosted Web Dashboard + +By earlier associating an email with your CLI identity, you can now view your published modules on the web dashboard. For multiple identities, first list them and copy the hash you want to use: + +```bash +spacetime identity list +``` + +1. Open the SpacetimeDB [login page](https://spacetimedb.com/login) using the same email above. +1. Choose your identity from the dropdown menu. + - \[For multiple identities\] `CTRL+F` to highlight the correct identity you copied earlier. +1. Check your email for a validation link. + +You should now be able to see your published modules on the web dashboard! + +--- + +## Summary + +- We ensured the hosted `testnet` server existed, then set it as the default. +- We added an `identity` to bind with our hosted `testnet` server, ensuring it contained both a Nickname and Email. +- We then logged in the web dashboard via an email `one-time password (OTP)` and were then able to view our published apps. +- With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment. diff --git a/Writerside2/topics/deploying/self-hosted.md b/Writerside2/topics/deploying/self-hosted.md new file mode 100644 index 00000000..9c47282f --- /dev/null +++ b/Writerside2/topics/deploying/self-hosted.md @@ -0,0 +1,60 @@ +# Deploying - Self-Hosted + +This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. Via CLI, we will then: + +1. Ensure our localhost server named `local` exists as the default. +1. Start our localhost server in a separate terminal window. +1. Create an `Identity` with at least a Nickname. +1. `Publish` your app. + +💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI and that you already have `local` server added (exists by default). If you accidentally removed `local`, add it back via CLI with the `--no-fingerprint` flag (since our server is not yet running): + +```bash +spacetime server add "http://127.0.0.1:3000" local --no-fingerprint +``` + +## Set the Server Default + +To make CLI commands easier so that we don't need to keep specifying `local` as the target server, let's set it as default: + +```bash +spacetime server set-default local +``` + +## Start the Local Server + +In a **separate** terminal window, start the local listen server in the foreground: +```bash +spacetime start +``` + +## Creating an Identity + +By default, there are no identities created. Let's create a new one via CLI: +```bash +spacetime identity new --name {Nickname} +``` + +💡We could optionally add `--email {Email}` to the above command, but is currently unnecessary for local deployment since there's no web dashboard. If you already created an identity but forgot to attach a Nickname, add it via CLI to easier identify your modules: +```bash +spacetime identity set-name {Nickname} +``` + +## Create and Publish a Module + +Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: + +```bash +cd ~ +spacetime init --lang rust HelloSpacetimeDB +cd HelloSpacetimeDB +spacetime publish HelloSpacetimeDB +``` + +--- + +## Summary + +- We ensured the self-hosted `local` server existed, then set it as the default. +- We then opened a separate terminal to run the self-hosted `local` server in the foreground. +- We added an `identity` to bind with our self-hosted `local` server set to default, ensuring it contained a Nickname. diff --git a/Writerside2/topics/getting-started.md b/Writerside2/topics/getting-started.md new file mode 100644 index 00000000..31e2fc90 --- /dev/null +++ b/Writerside2/topics/getting-started.md @@ -0,0 +1,34 @@ +# Getting Started + +To develop SpacetimeDB applications locally, you will need to run the Standalone version of the server. + +1. [Install](install.) the SpacetimeDB CLI (Command Line Interface) +2. Run the start command: + +```bash +spacetime start +``` + +The server listens on port `3000` by default, customized via `--listen-addr`. + +💡 Standalone mode will run in the foreground. +⚠️ SSL is not supported in standalone mode. + +## What's Next? + +You are ready to start developing SpacetimeDB modules. See below for a quickstart guide for both client and server (module) languages/frameworks. + +### Server (Module) + +- [Rust](quickstart.) +- [C#](quickstart1.) + +⚡**Note:** Rust is [roughly 2x faster](https://faun.dev/c/links/faun/c-vs-rust-vs-go-a-performance-benchmarking-in-kubernetes/) than C# + +### Client + +- [Rust](quickstart2.) +- [C# (Standalone)](quickstart3.) +- [C# (Unity)](part-1.) +- [Typescript](quickstart4.) +- [Python](quickstart5.) \ No newline at end of file diff --git a/Writerside2/topics/http/database.md b/Writerside2/topics/http/database.md new file mode 100644 index 00000000..0e7fbe89 --- /dev/null +++ b/Writerside2/topics/http/database.md @@ -0,0 +1,589 @@ +# `/database` HTTP API + +The HTTP endpoints in `/database` allow clients to interact with Spacetime databases in a variety of ways, including retrieving information, creating and deleting databases, invoking reducers and evaluating SQL queries. + +## At a glance + +| Route | Description | +| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| [`/database/dns/:name GET`](#databasednsname-get.) | Look up a database's address by its name. | +| [`/database/reverse_dns/:address GET`](#databasereverse_dnsaddress-get.) | Look up a database's name by its address. | +| [`/database/set_name GET`](#databaseset_name-get.) | Set a database's name, given its address. | +| [`/database/ping GET`](#databaseping-get.) | No-op. Used to determine whether a client can connect. | +| [`/database/register_tld GET`](#databaseregister_tld-get.) | Register a top-level domain. | +| [`/database/request_recovery_code GET`](#databaserequest_recovery_code-get.) | Request a recovery code to the email associated with an identity. | +| [`/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](ws.). | +| [`/database/call/:name_or_address/:reducer POST`](#databasecallname_or_addressreducer-post.) | Invoke a reducer in a database. | +| [`/database/schema/:name_or_address GET`](#databaseschemaname_or_address-get.) | Get the schema for a database. | +| [`/database/schema/:name_or_address/:entity_type/:entity GET`](#databaseschemaname_or_addressentity_typeentity-get.) | Get a schema for a particular table or reducer. | +| [`/database/info/:name_or_address GET`](#databaseinfoname_or_address-get.) | Get a JSON description of a database. | +| [`/database/logs/:name_or_address GET`](#databaselogsname_or_address-get.) | Retrieve logs from a database. | +| [`/database/sql/:name_or_address POST`](#databasesqlname_or_address-post.) | Run a SQL query against a database. | + +## `/database/dns/:name GET` + +Look up a database's address by its name. + +Accessible through the CLI as `spacetime dns lookup `. + +#### Parameters + +| Name | Value | +| ------- | ------------------------- | +| `:name` | The name of the database. | + +#### Returns + +If a database with that name exists, returns JSON in the form: + +```typescript +{ "Success": { + "domain": string, + "address": string +} } +``` + +If no database with that name exists, returns JSON in the form: + +```typescript +{ "Failure": { + "domain": string +} } +``` + +## `/database/reverse_dns/:address GET` + +Look up a database's name by its address. + +Accessible through the CLI as `spacetime dns reverse-lookup
`. + +#### Parameters + +| Name | Value | +| ---------- | ---------------------------- | +| `:address` | The address of the database. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ "names": array } +``` + +where `` is a JSON array of strings, each of which is a name which refers to the database. + +## `/database/set_name GET` + +Set the name associated with a database. + +Accessible through the CLI as `spacetime dns set-name
`. + +#### Query Parameters + +| Name | Value | +| -------------- | ------------------------------------------------------------------------- | +| `address` | The address of the database to be named. | +| `domain` | The name to register. | +| `register_tld` | A boolean; whether to register the name as a TLD. Should usually be true. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +If the name was successfully set, returns JSON in the form: + +```typescript +{ "Success": { + "domain": string, + "address": string +} } +``` + +If the top-level domain is not registered, and `register_tld` was not specified, returns JSON in the form: + +```typescript +{ "TldNotRegistered": { + "domain": string +} } +``` + +If the top-level domain is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form: + +```typescript +{ "PermissionDenied": { + "domain": string +} } +``` + +> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. + +## `/database/ping GET` + +Does nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB. + +## `/database/register_tld GET` + +Register a new Spacetime top-level domain. A TLD is the part of a database name before the first `/`. For example, in the name `tyler/bitcraft`, the TLD is `tyler`. Each top-level domain is owned by at most one identity, and only the owner can publish databases with that TLD. + +> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. + +Accessible through the CLI as `spacetime dns register-tld `. + +#### Query Parameters + +| Name | Value | +| ----- | -------------------------------------- | +| `tld` | New top-level domain name to register. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +If the domain is successfully registered, returns JSON in the form: + +```typescript +{ "Success": { + "domain": string +} } +``` + +If the domain is already registered to the caller, returns JSON in the form: + +```typescript +{ "AlreadyRegistered": { + "domain": string +} } +``` + +If the domain is already registered to another identity, returns JSON in the form: + +```typescript +{ "Unauthorized": { + "domain": string +} } +``` + +## `/database/request_recovery_code GET` + +Request a recovery code or link via email, in order to recover the token associated with an identity. + +Accessible through the CLI as `spacetime identity recover `. + +#### Query Parameters + +| 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`](identity#identity-post.) or afterwards via [`/identity/:identity/set-email`](identity#identityidentityset_email-post.). | +| `link` | A boolean; whether to send a clickable link rather than a recovery code. | + +## `/database/confirm_recovery_code GET` + +Confirm a recovery code received via email following a [`/database/request_recovery_code GET`](#-database-request_recovery_code-get.) request, and retrieve the identity's token. + +Accessible through the CLI as `spacetime identity recover `. + +#### Query Parameters + +| Name | Value | +| ---------- | --------------------------------------------- | +| `identity` | The identity whose token should be recovered. | +| `email` | The email which received the recovery code. | +| `code` | The recovery code received via email. | + +On success, returns JSON in the form: + +```typescript +{ + "identity": string, + "token": string +} +``` + +## `/database/publish POST` + +Publish a database. + +Accessible through the CLI as `spacetime publish`. + +#### Query Parameters + +| Name | Value | +| ----------------- | ------------------------------------------------------------------------------------------------ | +| `host_type` | Optional; a SpacetimeDB module host type. Currently, only `"wasmer"` is supported. | +| `clear` | A boolean; whether to clear any existing data when updating an existing database. | +| `name_or_address` | The name of the database to publish or update, or the address of an existing database to update. | +| `register_tld` | A boolean; whether to register the database's top-level domain. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Data + +A WebAssembly module in the [binary format](https://webassembly.github.io/spec/core/binary/index.html). + +#### Returns + +If the database was successfully published, returns JSON in the form: + +```typescript +{ "Success": { + "domain": null | string, + "address": string, + "op": "created" | "updated" +} } +``` + +If the top-level domain for the requested name is not registered, returns JSON in the form: + +```typescript +{ "TldNotRegistered": { + "domain": string +} } +``` + +If the top-level domain for the requested name is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form: + +```typescript +{ "PermissionDenied": { + "domain": string +} } +``` + +> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. + +## `/database/delete/:address POST` + +Delete a database. + +Accessible through the CLI as `spacetime delete
`. + +#### Parameters + +| Name | Address | +| ---------- | ---------------------------- | +| `:address` | The address of the database. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +## `/database/subscribe/:name_or_address GET` + +Begin a [WebSocket connection](ws.) with a database. + +#### Parameters + +| Name | Value | +| ------------------ | ---------------------------- | +| `:name_or_address` | The address of the database. | + +#### Required Headers + +For more information about WebSocket headers, see [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). + +| Name | Value | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `Sec-WebSocket-Protocol` | [`v1.bin.spacetimedb`](ws#binary-protocol.) or [`v1.text.spacetimedb`](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. | + +#### Optional Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +## `/database/call/:name_or_address/:reducer POST` + +Invoke a reducer in a database. + +#### Parameters + +| Name | Value | +| ------------------ | ------------------------------------ | +| `:name_or_address` | The name or address of the database. | +| `:reducer` | The name of the reducer. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Data + +A JSON array of arguments to the reducer. + +## `/database/schema/:name_or_address GET` + +Get a schema for a database. + +Accessible through the CLI as `spacetime describe `. + +#### Parameters + +| Name | Value | +| ------------------ | ------------------------------------ | +| `:name_or_address` | The name or address of the database. | + +#### Query Parameters + +| Name | Value | +| -------- | ----------------------------------------------------------- | +| `expand` | A boolean; whether to include full schemas for each entity. | + +#### Returns + +Returns a JSON object with two properties, `"entities"` and `"typespace"`. For example, on the default module generated by `spacetime init` with `expand=true`, returns: + +```typescript +{ + "entities": { + "Person": { + "arity": 1, + "schema": { + "elements": [ + { + "algebraic_type": { + "Builtin": { + "String": [] + } + }, + "name": { + "some": "name" + } + } + ] + }, + "type": "table" + }, + "__init__": { + "arity": 0, + "schema": { + "elements": [], + "name": "__init__" + }, + "type": "reducer" + }, + "add": { + "arity": 1, + "schema": { + "elements": [ + { + "algebraic_type": { + "Builtin": { + "String": [] + } + }, + "name": { + "some": "name" + } + } + ], + "name": "add" + }, + "type": "reducer" + }, + "say_hello": { + "arity": 0, + "schema": { + "elements": [], + "name": "say_hello" + }, + "type": "reducer" + } + }, + "typespace": [ + { + "Product": { + "elements": [ + { + "algebraic_type": { + "Builtin": { + "String": [] + } + }, + "name": { + "some": "name" + } + } + ] + } + } + ] +} +``` + +The `"entities"` will be an object whose keys are table and reducer names, and whose values are objects of the form: + +```typescript +{ + "arity": number, + "type": "table" | "reducer", + "schema"?: ProductType +} +``` + +| Entity field | Value | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `arity` | For tables, the number of colums; for reducers, the number of arguments. | +| `type` | For tables, `"table"`; for reducers, `"reducer"`. | +| `schema` | A [JSON-encoded `ProductType`](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](satn.) referenced by the module. This can be used to resolve `Ref` types within the schema; the type `{ "Ref": n }` refers to `response["typespace"][n]`. + +## `/database/schema/:name_or_address/:entity_type/:entity GET` + +Get a schema for a particular table or reducer in a database. + +Accessible through the CLI as `spacetime describe `. + +#### Parameters + +| Name | Value | +| ------------------ | ---------------------------------------------------------------- | +| `:name_or_address` | The name or address of the database. | +| `:entity_type` | `reducer` to describe a reducer, or `table` to describe a table. | +| `:entity` | The name of the reducer or table. | + +#### Query Parameters + +| Name | Value | +| -------- | ------------------------------------------------------------- | +| `expand` | A boolean; whether to include the full schema for the entity. | + +#### Returns + +Returns a single entity in the same format as in the `"entities"` returned by [the `/database/schema/:name_or_address GET` endpoint](#databaseschemaname_or_address-get.): + +```typescript +{ + "arity": number, + "type": "table" | "reducer", + "schema"?: ProductType, +} +``` + +| Field | Value | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `arity` | For tables, the number of colums; for reducers, the number of arguments. | +| `type` | For tables, `"table"`; for reducers, `"reducer"`. | +| `schema` | A [JSON-encoded `ProductType`](satn.); for tables, the table schema; for reducers, the argument schema. Only present if `expand` is supplied and true. | + +## `/database/info/:name_or_address GET` + +Get a database's address, owner identity, host type, number of replicas and a hash of its WASM module. + +#### Parameters + +| Name | Value | +| ------------------ | ------------------------------------ | +| `:name_or_address` | The name or address of the database. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "address": string, + "identity": string, + "host_type": "wasmer", + "num_replicas": number, + "program_bytes_address": string +} +``` + +| Field | Type | Meaning | +| ------------------------- | ------ | ----------------------------------------------------------- | +| `"address"` | String | The address of the database. | +| `"identity"` | String | The Spacetime identity of the database's owner. | +| `"host_type"` | String | The module host type; currently always `"wasmer"`. | +| `"num_replicas"` | Number | The number of replicas of the database. Currently always 1. | +| `"program_bytes_address"` | String | Hash of the WASM module for the database. | + +## `/database/logs/:name_or_address GET` + +Retrieve logs from a database. + +Accessible through the CLI as `spacetime logs `. + +#### Parameters + +| Name | Value | +| ------------------ | ------------------------------------ | +| `:name_or_address` | The name or address of the database. | + +#### Query Parameters + +| Name | Value | +| ----------- | --------------------------------------------------------------- | +| `num_lines` | Number of most-recent log lines to retrieve. | +| `follow` | A boolean; whether to continue receiving new logs via a stream. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +Text, or streaming text if `follow` is supplied, containing log lines. + +## `/database/sql/:name_or_address POST` + +Run a SQL query against a database. + +Accessible through the CLI as `spacetime sql `. + +#### Parameters + +| Name | Value | +| ------------------ | --------------------------------------------- | +| `:name_or_address` | The name or address of the database to query. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Data + +SQL queries, separated by `;`. + +#### Returns + +Returns a JSON array of statement results, each of which takes the form: + +```typescript +{ + "schema": ProductType, + "rows": array +} +``` + +The `schema` will be a [JSON-encoded `ProductType`](satn.) describing the type of the returned rows. + +The `rows` will be an array of [JSON-encoded `ProductValue`s](satn.), each of which conforms to the `schema`. diff --git a/Writerside2/topics/http/energy.md b/Writerside2/topics/http/energy.md new file mode 100644 index 00000000..fabecc30 --- /dev/null +++ b/Writerside2/topics/http/energy.md @@ -0,0 +1,76 @@ +# `/energy` HTTP API + +The HTTP endpoints in `/energy` allow clients to query identities' energy balances. Spacetime databases expend energy from their owners' balances while executing reducers. + +## At a glance + +| Route | Description | +| ------------------------------------------------ | --------------------------------------------------------- | +| [`/energy/:identity GET`](#energyidentity-get.) | Get the remaining energy balance for the user `identity`. | +| [`/energy/:identity POST`](#energyidentity-post.) | Set the energy balance for the user `identity`. | + +## `/energy/:identity GET` + +Get the energy balance of an identity. + +Accessible through the CLI as `spacetime energy status `. + +#### Parameters + +| Name | Value | +| ----------- | ----------------------- | +| `:identity` | The Spacetime identity. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "balance": string +} +``` + +| Field | Value | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `balance` | The identity's energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. | + +## `/energy/:identity POST` + +Set the energy balance for an identity. + +Note that in the SpacetimeDB 0.6 Testnet, this endpoint always returns code 401, `UNAUTHORIZED`. Testnet energy balances cannot be refilled. + +Accessible through the CLI as `spacetime energy set-balance `. + +#### Parameters + +| Name | Value | +| ----------- | ----------------------- | +| `:identity` | The Spacetime identity. | + +#### Query Parameters + +| Name | Value | +| --------- | ------------------------------------------ | +| `balance` | A decimal integer; the new balance to set. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "balance": number +} +``` + +| Field | Value | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `balance` | The identity's new energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. | diff --git a/Writerside2/topics/http/http_index.md b/Writerside2/topics/http/http_index.md new file mode 100644 index 00000000..e9a3d21e --- /dev/null +++ b/Writerside2/topics/http/http_index.md @@ -0,0 +1,51 @@ +# SpacetimeDB HTTP Authorization + +Rather than a password, each Spacetime identity is associated with a private token. These tokens are generated by SpacetimeDB when the corresponding identity is created, and cannot be changed. + +> Do not share your SpacetimeDB token with anyone, ever. + +### Generating identities and tokens + +Clients can request a new identity and token via [the `/identity POST` HTTP endpoint](identity#identity-post.). + +Alternately, a new identity and token will be generated during an anonymous connection via the [WebSocket API](ws.), and passed to the client as [an `IdentityToken` message](ws#identitytoken.). + +### Encoding `Authorization` headers + +Many SpacetimeDB HTTP endpoints either require or optionally accept a token in the `Authorization` header. SpacetimeDB authorization headers use `Basic` authorization with the username `token` and the token as the password. Because Spacetime tokens are not passwords, and SpacetimeDB Cloud uses TLS, usual security concerns about HTTP `Basic` authorization do not apply. + +To construct an appropriate `Authorization` header value for a `token`: + +1. Prepend the string `token:`. +2. Base64-encode. +3. Prepend the string `Basic `. + +#### Python + +```python +def auth_header_value(token): + username_and_password = f"token:{token}".encode("utf-8") + base64_encoded = base64.b64encode(username_and_password).decode("utf-8") + return f"Basic {base64_encoded}" +``` + +#### Rust + +```rust +fn auth_header_value(token: &str) -> String { + let username_and_password = format!("token:{}", token); + let base64_encoded = base64::prelude::BASE64_STANDARD.encode(username_and_password); + format!("Basic {}", encoded) +} +``` + +#### C# + +```csharp +public string AuthHeaderValue(string token) +{ + var username_and_password = Encoding.UTF8.GetBytes($"token:{auth}"); + var base64_encoded = Convert.ToBase64String(username_and_password); + return "Basic " + base64_encoded; +} +``` diff --git a/Writerside2/topics/http/identity.md b/Writerside2/topics/http/identity.md new file mode 100644 index 00000000..544d5d11 --- /dev/null +++ b/Writerside2/topics/http/identity.md @@ -0,0 +1,160 @@ +# `/identity` HTTP API + +The HTTP endpoints in `/identity` allow clients to generate and manage Spacetime public identities and private tokens. + +## At a glance + +| Route | Description | +| ----------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [`/identity GET`](#identity-get.) | Look up an identity by email. | +| [`/identity POST`](#identity-post.) | Generate a new identity and token. | +| [`/identity/websocket_token POST`](#identitywebsocket_token-post.) | Generate a short-lived access token for use in untrusted contexts. | +| [`/identity/:identity/set-email POST`](#identityidentityset-email-post.) | Set the email for an identity. | +| [`/identity/:identity/databases GET`](#identityidentitydatabases-get.) | List databases owned by an identity. | +| [`/identity/:identity/verify GET`](#identityidentityverify-get.) | Verify an identity and token. | + +## `/identity GET` + +Look up Spacetime identities associated with an email. + +Accessible through the CLI as `spacetime identity find `. + +#### Query Parameters + +| Name | Value | +| ------- | ------------------------------- | +| `email` | An email address to search for. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "identities": [ + { + "identity": string, + "email": string + } + ] +} +``` + +The `identities` value is an array of zero or more objects, each of which has an `identity` and an `email`. Each `email` will be the same as the email passed as a query parameter. + +## `/identity POST` + +Create a new identity. + +Accessible through the CLI as `spacetime identity new`. + +#### Query Parameters + +| Name | Value | +| ------- | ----------------------------------------------------------------------------------------------------------------------- | +| `email` | An email address to associate with the new identity. If unsupplied, the new identity will not have an associated email. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "identity": string, + "token": string +} +``` + +## `/identity/websocket_token POST` + +Generate a short-lived access token which can be used in untrusted contexts, e.g. embedded in URLs. + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "token": string +} +``` + +The `token` value is a short-lived [JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519). + +## `/identity/:identity/set-email POST` + +Associate an email with a Spacetime identity. + +Accessible through the CLI as `spacetime identity set-email `. + +#### Parameters + +| Name | Value | +| ----------- | ----------------------------------------- | +| `:identity` | The identity to associate with the email. | + +#### Query Parameters + +| Name | Value | +| ------- | ----------------- | +| `email` | An email address. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +## `/identity/:identity/databases GET` + +List all databases owned by an identity. + +#### Parameters + +| Name | Value | +| ----------- | --------------------- | +| `:identity` | A Spacetime identity. | + +#### Returns + +Returns JSON in the form: + +```typescript +{ + "addresses": array +} +``` + +The `addresses` value is an array of zero or more strings, each of which is the address of a database owned by the identity passed as a parameter. + +## `/identity/:identity/verify GET` + +Verify the validity of an identity/token pair. + +#### Parameters + +| Name | Value | +| ----------- | ----------------------- | +| `:identity` | The identity to verify. | + +#### Required Headers + +| Name | Value | +| --------------- | ------------------------------------------------------------------------------------------- | +| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | + +#### Returns + +Returns no data. + +If the token is valid and matches the identity, returns `204 No Content`. + +If the token is valid but does not match the identity, returns `400 Bad Request`. + +If the token is invalid, or no `Authorization` header is included in the request, returns `401 Unauthorized`. diff --git a/Writerside2/topics/index.md b/Writerside2/topics/index.md new file mode 100644 index 00000000..8426e256 --- /dev/null +++ b/Writerside2/topics/index.md @@ -0,0 +1,120 @@ +# SpacetimeDB Documentation + +## Installation + +You can run SpacetimeDB as a standalone database server via the `spacetime` CLI tool. + +You can find the instructions to install the CLI tool for your platform [here](install.). + + + +To get started running your own standalone instance of SpacetimeDB check out our [Getting Started Guide](getting-started.). + + + +## What is SpacetimeDB? + +You can think of SpacetimeDB as a database that is also a server. + +It is a relational database system that lets you upload your application logic directly into the database by way of very fancy stored procedures called "modules". + +Instead of deploying a web or game server that sits in between your clients and your database, your clients connect directly to the database and execute your application logic inside the database itself. You can write all of your permission and authorization logic right inside your module just as you would in a normal server. + +This means that you can write your entire application in a single language, Rust, and deploy it as a single binary. No more microservices, no more containers, no more Kubernetes, no more Docker, no more VMs, no more DevOps, no more infrastructure, no more ops, no more servers. + +
+ SpacetimeDB Architecture +
+ SpacetimeDB application architecture + (elements in white are provided by SpacetimeDB) +
+
+ +It's actually similar to the idea of smart contracts, except that SpacetimeDB is a database, has nothing to do with blockchain, and it's a lot faster than any smart contract system. + +So fast, in fact, that the entire backend our MMORPG [BitCraft Online](https://bitcraftonline.com) is just a SpacetimeDB module. We don't have any other servers or services running, which means that everything in the game, all of the chat messages, items, resources, terrain, and even the locations of the players are stored and processed by the database before being synchronized out to all of the clients in real-time. + +SpacetimeDB is optimized for maximum speed and minimum latency rather than batch processing or OLAP workloads. It is designed to be used for real-time applications like games, chat, and collaboration tools. + +This speed and latency is achieved by holding all of application state in memory, while persisting the data in a write-ahead-log (WAL) which is used to recover application state. + +## State Synchronization + +SpacetimeDB syncs client and server state for you so that you can just write your application as though you're accessing the database locally. No more messing with sockets for a week before actually writing your game. + +## Identities + +A SpacetimeDB `Identity` is a unique identifier that is used to authenticate and authorize access to the database. Importantly, while it represents who someone is, does NOT represent what they can do. Your application's logic will determine what a given identity is able to do by allowing or disallowing a transaction based on the caller's `Identity` along with any module-defined data and logic. + +SpacetimeDB associates each user with a 256-bit (32-byte) integer `Identity`. These identities are usually formatted as 64-digit hexadecimal strings. Identities are public information, and applications can use them to identify users. Identities are a global resource, so a user can use the same identity with multiple applications, so long as they're hosted by the same SpacetimeDB instance. + +Each identity has a corresponding authentication token. The authentication token is private, and should never be shared with anyone. Specifically, authentication tokens are [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519) signed by a secret unique to the SpacetimeDB instance. + +Additionally, each database has an owner `Identity`. Many database maintenance operations, like publishing a new version or evaluating arbitrary SQL queries, are restricted to only authenticated connections by the owner. + +SpacetimeDB provides tools in the CLI and the [client SDKs](sdks.) for managing credentials. + +## Addresses + +A SpacetimeDB `Address` is an opaque identifier for a database or a client connection. An `Address` is a 128-bit integer, usually formatted as a 32-character (16-byte) hexadecimal string. + +Each SpacetimeDB database has an `Address`, generated by the SpacetimeDB host, which can be used to connect to the database or to request information about it. Databases may also have human-readable names, which are mapped to addresses internally. + +Each client connection has an `Address`. These addresses are opaque, and do not correspond to any metadata about the client. They are notably not IP addresses or device identifiers. A client connection can be uniquely identified by its `(Identity, Address)` pair, but client addresses may not be globally unique; it is possible for multiple connections with the same `Address` but different identities to co-exist. SpacetimeDB modules should treat `Identity` as differentiating users, and `Address` as differentiating connections by the same user. + +## Language Support + +### Server-side Libraries + +Currently, Rust is the best-supported language for writing SpacetimeDB modules. Support for lots of other languages is in the works! + +- [Rust](rust.) - [(Quickstart)](quickstart.) +- [C#](c-sharp.) - [(Quickstart)](quickstart1.) +- Python (Coming soon) +- C# (Coming soon) +- Typescript (Coming soon) +- C++ (Planned) +- Lua (Planned) + +### Client-side SDKs + +- [Rust](rust1.) - [(Quickstart)](quickstart2.) +- [C#](c-sharp1.) - [(Quickstart)](quickstart3.) +- [TypeScript](typescript.) - [(Quickstart)](quickstart4.) +- [Python](python.) - [(Quickstart)](quickstart5.) +- C++ (Planned) +- Lua (Planned) + +### Unity + +SpacetimeDB was designed first and foremost as the backend for multiplayer Unity games. To learn more about using SpacetimeDB with Unity, jump on over to the [SpacetimeDB Unity Tutorial](part-1.). + +## FAQ + +1. What is SpacetimeDB? + It's a whole cloud platform within a database that's fast enough to run real-time games. + +1. How do I use SpacetimeDB? + Install the `spacetime` command line tool, choose your favorite language, import the SpacetimeDB library, write your application, compile it to WebAssembly, and upload it to the SpacetimeDB cloud platform. Once it's uploaded you can call functions directly on your application and subscribe to changes in application state. + +1. How do I get/install SpacetimeDB? + Just install our command line tool and then upload your application to the cloud. + +4. How do I create a new database with SpacetimeDB? + Follow our [Quick Start](getting-started.) guide! + +TL;DR in an empty directory, init and publish a barebones app named HelloWorld. + +```bash +spacetime init --lang=rust +spacetime publish HelloWorld +``` + +5. How do I create a Unity game with SpacetimeDB? + Follow our [Unity Project](unity-project.) guide! + +TL;DR after already initializing and publishing (see FAQ #5), generate the SDK: + +```bash +spacetime generate --out-dir --lang=csharp +``` diff --git a/Writerside2/topics/modules/c-sharp/c-sharp_index.md b/Writerside2/topics/modules/c-sharp/c-sharp_index.md new file mode 100644 index 00000000..31ebd1d4 --- /dev/null +++ b/Writerside2/topics/modules/c-sharp/c-sharp_index.md @@ -0,0 +1,307 @@ +# SpacetimeDB C# Modules + +You can use the [C# SpacetimeDB library](https://github.com/clockworklabs/SpacetimeDBLibCSharp) to write modules in C# which interact with the SpacetimeDB database. + +It uses [Roslyn incremental generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) to add extra static methods to types, tables and reducers marked with special attributes and registers them with the database runtime. + +## Example + +Let's start with a heavily commented version of the default example from the landing page: + +```csharp +// These imports bring into the scope common APIs you'll need to expose items from your module and to interact with the database runtime. +using SpacetimeDB.Module; +using static SpacetimeDB.Runtime; + +// Roslyn generators are statically generating extra code as-if they were part of the source tree, so, +// in order to inject new methods, types they operate on as well as their parents have to be marked as `partial`. +// +// We start with the top-level `Module` class for the module itself. +static partial class Module +{ + // `[SpacetimeDB.Table]` registers a struct or a class as a SpacetimeDB table. + // + // It generates methods to insert, filter, update, and delete rows of the given type in the table. + [SpacetimeDB.Table] + public partial struct Person + { + // `[SpacetimeDB.Column]` allows to specify column attributes / constraints such as + // "this field should be unique" or "this field should get automatically assigned auto-incremented value". + [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)] + public int Id; + public string Name; + public int Age; + } + + // `[SpacetimeDB.Reducer]` marks a static method as a SpacetimeDB reducer. + // + // Reducers are functions that can be invoked from the database runtime. + // They can't return values, but can throw errors that will be caught and reported back to the runtime. + [SpacetimeDB.Reducer] + public static void Add(string name, int age) + { + // We can skip (or explicitly set to zero) auto-incremented fields when creating new rows. + var person = new Person { Name = name, Age = age }; + + // `Insert()` method is auto-generated and will insert the given row into the table. + person.Insert(); + // After insertion, the auto-incremented fields will be populated with their actual values. + // + // `Log()` function is provided by the runtime and will print the message to the database log. + // It should be used instead of `Console.WriteLine()` or similar functions. + Log($"Inserted {person.Name} under #{person.Id}"); + } + + [SpacetimeDB.Reducer] + public static void SayHello() + { + // Each table type gets a static Iter() method that can be used to iterate over the entire table. + foreach (var person in Person.Iter()) + { + Log($"Hello, {person.Name}!"); + } + Log("Hello, World!"); + } +} +``` + +## API reference + +Now we'll get into details on all the APIs SpacetimeDB provides for writing modules in C#. + +### Logging + +First of all, logging as we're likely going to use it a lot for debugging and reporting errors. + +`SpacetimeDB.Runtime` provides a `Log` function that will print the given message to the database log, along with the source location and a log level it was provided. + +Supported log levels are provided by the `LogLevel` enum: + +```csharp +public enum LogLevel +{ + Error, + Warn, + Info, + Debug, + Trace, + Panic +} +``` + +If omitted, the log level will default to `Info`, so these two forms are equivalent: + +```csharp +Log("Hello, World!"); +Log("Hello, World!", LogLevel.Info); +``` + +### Supported types + +#### Built-in types + +The following types are supported out of the box and can be stored in the database tables directly or as part of more complex types: + +- `bool` +- `byte`, `sbyte` +- `short`, `ushort` +- `int`, `uint` +- `long`, `ulong` +- `float`, `double` +- `string` +- [`Int128`](https://learn.microsoft.com/en-us/dotnet/api/system.int128), [`UInt128`](https://learn.microsoft.com/en-us/dotnet/api/system.uint128) +- `T[]` - arrays of supported values. +- [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1) +- [`Dictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2) + +And a couple of special custom types: + +- `SpacetimeDB.SATS.Unit` - semantically equivalent to an empty struct, sometimes useful in generic contexts where C# doesn't permit `void`. +- `Identity` (`SpacetimeDB.Runtime.Identity`) - a unique identifier for each user; internally a byte blob but can be printed, hashed and compared for equality. +- `Address` (`SpacetimeDB.Runtime.Address`) - an identifier which disamgibuates connections by the same `Identity`; internally a byte blob but can be printed, hashed and compared for equality. + + +#### 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. + +Any `struct` or `class` marked with this attribute, as well as their respective parents, must be `partial`, as the code generator will add methods to them. + +```csharp +[SpacetimeDB.Type] +public partial struct Point +{ + public int x; + public int y; +} +``` + +`enum`s marked with this attribute must not use custom discriminants, as the runtime expects them to be always consecutive starting from zero. Unlike structs and classes, they don't use `partial` as C# doesn't allow to add methods to `enum`s. + +```csharp +[SpacetimeDB.Type] +public enum Color +{ + Red, + Green, + Blue, +} +``` + +#### Tagged enums + +SpacetimeDB has support for tagged enums which can be found in languages like Rust, but not C#. + +To bridge the gap, a special marker interface `SpacetimeDB.TaggedEnum` can be used on any `SpacetimeDB.Type`-marked `struct` or `class` to mark it as a SpacetimeDB tagged enum. It accepts a tuple of 2 or more named items and will generate methods to check which variant is currently active, as well as accessors for each variant. + +It is expected that you will use the `Is*` methods to check which variant is active before accessing the corresponding field, as the accessor will throw an exception on a state mismatch. + +```csharp +// Example declaration: +[SpacetimeDB.Type] +partial struct Option : SpacetimeDB.TaggedEnum<(T Some, Unit None)> { } + +// Usage: +var option = new Option { Some = 42 }; +if (option.IsSome) +{ + Log($"Value: {option.Some}"); +} +``` + +### Tables + +`[SpacetimeDB.Table]` attribute can be used on any `struct` or `class` to mark it as a SpacetimeDB table. It will register a table in the database with the given name and fields as well as will generate C# methods to insert, filter, update, and delete rows of the given type. + +It implies `[SpacetimeDB.Type]`, so you must not specify both attributes on the same type. + +```csharp +[SpacetimeDB.Table] +public partial struct Person +{ + [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)] + public int Id; + public string Name; + public int Age; +} +``` + +The example above will generate the following extra methods: + +```csharp +public partial struct Person +{ + // Inserts current instance as a new row into the table. + public void Insert(); + + // Returns an iterator over all rows in the table, e.g.: + // `for (var person in Person.Iter()) { ... }` + public static IEnumerable Iter(); + + // Returns an iterator over all rows in the table that match the given filter, e.g.: + // `for (var person in Person.Query(p => p.Age >= 18)) { ... }` + public static IEnumerable Query(Expression> filter); + + // Generated for each column: + + // Returns an iterator over all rows in the table that have the given value in the `Name` column. + public static IEnumerable FilterByName(string name); + public static IEnumerable FilterByAge(int age); + + // Generated for each unique column: + + // 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); +} +``` + +#### Column attributes + +Attribute `[SpacetimeDB.Column]` can be used on any field of a `SpacetimeDB.Table`-marked `struct` or `class` to customize column attributes as seen above. + +The supported column attributes are: + +- `ColumnAttrs.AutoInc` - this column should be auto-incremented. +- `ColumnAttrs.Unique` - this column should be unique. +- `ColumnAttrs.PrimaryKey` - this column should be a primary key, it implies `ColumnAttrs.Unique` but also allows clients to subscribe to updates via `OnUpdate` which will use this field to match the old and the new version of the row with each other. + +These attributes are bitflags and can be combined together, but you can also use some predefined shortcut aliases: + +- `ColumnAttrs.Identity` - same as `ColumnAttrs.Unique | ColumnAttrs.AutoInc`. +- `ColumnAttrs.PrimaryKeyAuto` - same as `ColumnAttrs.PrimaryKey | ColumnAttrs.AutoInc`. + +### Reducers + +Attribute `[SpacetimeDB.Reducer]` can be used on any `static void` method to register it as a SpacetimeDB reducer. The method must accept only supported types as arguments. If it throws an exception, those will be caught and reported back to the database runtime. + +```csharp +[SpacetimeDB.Reducer] +public static void Add(string name, int age) +{ + var person = new Person { Name = name, Age = age }; + person.Insert(); + Log($"Inserted {person.Name} under #{person.Id}"); +} +``` + +If a reducer has an argument with a type `DbEventArgs` (`SpacetimeDB.Runtime.DbEventArgs`), it will be provided with event details such as the sender identity (`SpacetimeDB.Runtime.Identity`), sender address (`SpacetimeDB.Runtime.Address?`) and the time (`DateTimeOffset`) of the invocation: + +```csharp +[SpacetimeDB.Reducer] +public static void PrintInfo(DbEventArgs e) +{ + Log($"Sender identity: {e.Sender}"); + Log($"Sender address: {e.Address}"); + Log($"Time: {e.Time}"); +} +``` + +`[SpacetimeDB.Reducer]` also generates a function to schedule the given reducer in the future. + +Since it's not possible to generate extension methods on existing methods, the codegen will instead add a `Schedule`-prefixed method colocated in the same namespace as the original method instead. The generated method will accept `DateTimeOffset` argument for the time when the reducer should be invoked, followed by all the arguments of the reducer itself, except those that have type `DbEventArgs`. + +```csharp +// Example reducer: +[SpacetimeDB.Reducer] +public static void Add(string name, int age) { ... } + +// Auto-generated by the codegen: +public static void ScheduleAdd(DateTimeOffset time, string name, int age) { ... } + +// Usage from another reducer: +[SpacetimeDB.Reducer] +public static void AddIn5Minutes(DbEventArgs e, string name, int age) +{ + // Note that we're using `e.Time` instead of `DateTimeOffset.Now` which is not allowed in modules. + var scheduleToken = ScheduleAdd(e.Time.AddMinutes(5), name, age); + + // We can cancel the scheduled reducer by calling `Cancel()` on the returned token. + scheduleToken.Cancel(); +} +``` + +#### Special reducers + +These are two special kinds of reducers that can be used to respond to module lifecycle events. They're stored in the `SpacetimeDB.Module.ReducerKind` class and can be used as an argument to the `[SpacetimeDB.Reducer]` attribute: + +- `ReducerKind.Init` - this reducer will be invoked when the module is first published. +- `ReducerKind.Update` - this reducer will be invoked when the module is updated. +- `ReducerKind.Connect` - this reducer will be invoked when a client connects to the database. +- `ReducerKind.Disconnect` - this reducer will be invoked when a client disconnects from the database. + + +Example: + +```csharp +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void Init() +{ + Log("...and we're live!"); +} +``` diff --git a/Writerside2/topics/modules/c-sharp/quickstart.md b/Writerside2/topics/modules/c-sharp/quickstart.md new file mode 100644 index 00000000..fedd7851 --- /dev/null +++ b/Writerside2/topics/modules/c-sharp/quickstart.md @@ -0,0 +1,312 @@ +# C# Module Quickstart + +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. + +Each SpacetimeDB module defines a set of tables and a set of reducers. + +Each table is defined as a C# `class` annotated with `[SpacetimeDB.Table]`, where an instance represents a row, and each field represents a column. + +A reducer is a function which traverses and updates the database. Each reducer call runs in its own transaction, and its updates to the database are only committed if the reducer returns successfully. In C#, reducers are defined as functions annotated with `[SpacetimeDB.Reducer]`. If an exception is thrown, the reducer call fails, the database is not updated, and a failed message is reported to the client. + +## Install SpacetimeDB + +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. + +## Install .NET 8 + +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 +``` + +.NET 8.0 is the earliest to have the `wasi-experimental` workload that we rely on, but requires manual activation: + +```bash +dotnet workload install wasi-experimental +``` + +## Project structure + +Create and enter a directory `quickstart-chat`: + +```bash +mkdir quickstart-chat +cd quickstart-chat +``` + +Now create `server`, our module, which runs in the database: + +```bash +spacetime init --lang csharp server +``` + +## Declare imports + +`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. +3. Clear it out, so we can write a new module that's still pretty simple: a bare-bones chat server. + +To the top of `server/Lib.cs`, add some imports we'll be using: + +```csharp +using System.Runtime.CompilerServices; +using SpacetimeDB.Module; +using static SpacetimeDB.Runtime; +``` + +- `System.Runtime.CompilerServices` +- `SpacetimeDB.Module` + - Contains the special attributes we'll use to define our module. + - Allows us to use the `ModuleInitializer` attribute, which we'll use to register our `OnConnect` and `OnDisconnect` callbacks. +- `SpacetimeDB.Runtime` contains the raw API bindings SpacetimeDB uses to communicate with the database. + +We also need to create our static module class which all of the module code will live in. In `server/Lib.cs`, add: + +```csharp +static partial class Module +{ +} +``` + +## Define tables + +To get our chat server running, we'll need to store two kinds of data: information about each user, and records of all the messages that have been sent. + +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 +[SpacetimeDB.Table] +public partial class User +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public Identity Identity; + public string? Name; + public bool Online; +} +``` + +For each `Message`, we'll store the `Identity` of the user who sent it, the `Timestamp` when it was sent, and the text of the message. + +In `server/Lib.cs`, add the definition of the table `Message` to the `Module` class: + +```csharp +[SpacetimeDB.Table] +public partial class Message +{ + public Identity Sender; + public long Sent; + public string Text = ""; +} +``` + +## Set users' names + +We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `SetName` which clients can invoke to set their `User.Name`. It will validate the caller's chosen name, using a function `ValidateName` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail. + +Each reducer may accept as its first argument a `DbEventArgs`, 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`, `dbEvent.Sender`. + +It's also possible to call `SetName` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though. + +In `server/Lib.cs`, add to the `Module` class: + +```csharp +[SpacetimeDB.Reducer] +public static void SetName(DbEventArgs dbEvent, string name) +{ + name = ValidateName(name); + + var user = User.FindByIdentity(dbEvent.Sender); + if (user is not null) + { + user.Name = name; + User.UpdateByIdentity(dbEvent.Sender, user); + } +} +``` + +For now, we'll just do a bare minimum of validation, rejecting the empty name. You could extend this in various ways, like: + +- Comparing against a blacklist for moderation purposes. +- Unicode-normalizing names. +- Rejecting names that contain non-printable characters, or removing characters or replacing them with a placeholder. +- Rejecting or truncating long names. +- Rejecting duplicate names. + +In `server/Lib.cs`, add to the `Module` class: + +```csharp +/// Takes a name and checks if it's acceptable as a user's name. +public static string ValidateName(string name) +{ + if (string.IsNullOrEmpty(name)) + { + throw new Exception("Names must not be empty"); + } + return name; +} +``` + +## Send messages + +We define a reducer `SendMessage`, 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 `Time` timestamp taken from the `DbEventArgs`. + +In `server/Lib.cs`, add to the `Module` class: + +```csharp +[SpacetimeDB.Reducer] +public static void SendMessage(DbEventArgs dbEvent, string text) +{ + text = ValidateMessage(text); + Log(text); + new Message + { + Sender = dbEvent.Sender, + Text = text, + Sent = dbEvent.Time.ToUnixTimeMilliseconds(), + }.Insert(); +} +``` + +We'll want to validate messages' texts in much the same way we validate users' chosen names. As above, we'll do the bare minimum, rejecting only empty messages. + +In `server/Lib.cs`, add to the `Module` class: + +```csharp +/// Takes a message's text and checks if it's acceptable to send. +public static string ValidateMessage(string text) +{ + if (string.IsNullOrEmpty(text)) + { + throw new ArgumentException("Messages must not be empty"); + } + return text; +} +``` + +You could extend the validation in `ValidateMessage` in similar ways to `ValidateName`, or add additional checks to `SendMessage`, like: + +- Rejecting messages from senders who haven't set their names. +- Rate-limiting users so they can't send new messages too quickly. + +## Set users' online status + +In C# modules, you can register for `Connect` and `Disconnect` events by using a special `ReducerKind`. We'll use the `Connect` event to create a `User` record for the client if it doesn't yet exist, and to set its online status. + +We'll use `User.FindByIdentity` to look up a `User` row for `dbEvent.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. + +In `server/Lib.cs`, add the definition of the connect reducer to the `Module` class: + +```csharp +[SpacetimeDB.Reducer(ReducerKind.Connect)] +public static void OnConnect(DbEventArgs dbEventArgs) +{ + Log($"Connect {dbEventArgs.Sender}"); + var user = User.FindByIdentity(dbEventArgs.Sender); + + if (user is not null) + { + // If this is a returning user, i.e., we already have a `User` with this `Identity`, + // set `Online: true`, but leave `Name` and `Identity` unchanged. + user.Online = true; + User.UpdateByIdentity(dbEventArgs.Sender, user); + } + else + { + // If this is a new user, create a `User` object for the `Identity`, + // which is online, but hasn't set a name. + new User + { + Name = null, + Identity = dbEventArgs.Sender, + Online = true, + }.Insert(); + } +} +``` + +Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.Disconnect`. We'll use it to un-set the `Online` status of the `User` for the disconnected client. + +Add the following code after the `OnConnect` lambda: + +```csharp +[SpacetimeDB.Reducer(ReducerKind.Disconnect)] +public static void OnDisconnect(DbEventArgs dbEventArgs) +{ + var user = User.FindByIdentity(dbEventArgs.Sender); + + if (user is not null) + { + // This user should exist, so set `Online: false`. + user.Online = false; + User.UpdateByIdentity(dbEventArgs.Sender, user); + } + else + { + // User does not exist, log warning + Log("Warning: No user found for disconnected client."); + } +} +``` + +## Publish the module + +And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more pleasant. Come up with a unique name, and fill it in where we've written ``. + +From the `quickstart-chat` directory, run: + +```bash +spacetime publish --project-path server +``` + +```bash +npm i wasm-opt -g +``` + +## Call Reducers + +You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. + +```bash +spacetime call send_message "Hello, World!" +``` + +Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. + +```bash +spacetime logs +``` + +You should now see the output that your module printed in the database. + +```bash +info: Hello, World! +``` + +## SQL Queries + +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" +``` + +```bash + text +--------- + "Hello, World!" +``` + +## What's next? + +You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](rust-sdk-quickstart-guide.), [C#](csharp-sdk-quickstart-guide.), [TypeScript](typescript-sdk-quickstart-guide.) or [Python](python-sdk-quickstart-guide.). + +If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](part-1.) or check out our example game, [BitcraftMini](part-3.). diff --git a/Writerside2/topics/modules/modules_index.md b/Writerside2/topics/modules/modules_index.md new file mode 100644 index 00000000..fd1a7e62 --- /dev/null +++ b/Writerside2/topics/modules/modules_index.md @@ -0,0 +1,30 @@ +# Server Module Overview + +Server modules are the core of a SpacetimeDB application. They define the structure of the database and the server-side logic that processes and handles client requests. These functions are called reducers and are transactional, meaning they ensure data consistency and integrity. Reducers can perform operations such as inserting, updating, and deleting data in the database. + +In the following sections, we'll cover the basics of server modules and how to create and deploy them. + +## Supported Languages + +### Rust + +As of SpacetimeDB 0.6, Rust is the only fully supported language for server modules. Rust is a great option for server modules because it is fast, safe, and has a small runtime. + +- [Rust Module Reference](rust.) +- [Rust Module Quickstart Guide](quickstart.) + +### C# + +We have C# support available in experimental status. C# can be a good choice for developers who are already using Unity or .net for their client applications. + +- [C# Module Reference](c-sharp.) +- [C# Module Quickstart Guide](quickstart1.) + +### Coming Soon + +We have plans to support additional languages in the future. + +- Python +- Typescript +- C++ +- Lua diff --git a/Writerside2/topics/modules/rust/rust_index.md b/Writerside2/topics/modules/rust/rust_index.md new file mode 100644 index 00000000..05d62bdc --- /dev/null +++ b/Writerside2/topics/modules/rust/rust_index.md @@ -0,0 +1,454 @@ +# SpacetimeDB Rust Modules + +Rust clients of SpacetimeDB use the [Rust SpacetimeDB module library][module library] to write modules which interact with the SpacetimeDB database. + +First, the `spacetimedb` library provides a number of macros for creating tables and Rust `struct`s corresponding to rows in those tables. + +Then the client API allows interacting with the database inside special functions called reducers. + +This guide assumes you are familiar with some basics of Rust. At the very least, you should be familiar with the idea of using attribute macros. An extremely common example is `derive` macros. + +Derive macros look at the type they are attached to and generate some related code. In this example, `#[derive(Debug)]` generates the formatting code needed to print out a `Location` for debugging purposes. + +```rust +#[derive(Debug)] +struct Location { + x: u32, + y: u32, +} +``` + +## SpacetimeDB Macro basics + +Let's start with a highly commented example, straight from the [demo]. This Rust package defines a SpacetimeDB module, with types we can operate on and functions we can run. + +```rust +// In this small example, we have two rust imports: +// |spacetimedb::spacetimedb| is the most important attribute we'll be using. +// |spacetimedb::println| is like regular old |println|, but outputting to the module's logs. +use spacetimedb::{spacetimedb, println}; + +// This macro lets us interact with a SpacetimeDB table of Person rows. +// We can insert and delete into, and query, this table by the collection +// of functions generated by the macro. +#[spacetimedb(table)] +pub struct Person { + name: String, +} + +// This is the other key macro we will be using. A reducer is a +// stored procedure that lives in the database, and which can +// be invoked remotely. +#[spacetimedb(reducer)] +pub fn add(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) +} + +// 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 +// functions, not methods. +#[spacetimedb(reducer)] +pub fn say_hello() { + // 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() { + // Reducers run in a very constrained and sandboxed environment, + // and in particular, can't do most I/O from the Rust standard library. + // We provide an alternative |spacetimedb::println| which is just like + // the std version, excepted it is redirected out to the module's logs. + println!("Hello, {}!", person.name); + } + println!("Hello, World!"); +} + +// Reducers can't return values, but can return errors. To do so, +// the reducer must have a return type of `Result<(), T>`, for any `T` that +// implements `Debug`. Such errors returned from reducers will be formatted and +// printed out to logs. +#[spacetimedb(reducer)] +pub fn add_person(name: String) -> Result<(), String> { + if name.is_empty() { + return Err("Name cannot be empty"); + } + + Person::insert(Person { name }) +} +``` + +## Macro API + +Now we'll get into details on all the macro APIs SpacetimeDB provides, starting with all the variants of the `spacetimedb` attribute. + +### Defining tables + +`#[spacetimedb(table)]` takes no further arguments, and is applied to a Rust struct with named fields: + +```rust +#[spacetimedb(table)] +struct Table { + field1: String, + field2: u32, +} +``` + +This attribute is applied to Rust structs in order to create corresponding tables in SpacetimeDB. Fields of the Rust struct correspond to columns of the database table. + +The fields of the struct have to be types that spacetimedb knows how to encode into the database. This is captured in Rust by the `SpacetimeType` trait. + +This is automatically defined for built in numeric types: + +- `bool` +- `u8`, `u16`, `u32`, `u64`, `u128` +- `i8`, `i16`, `i32`, `i64`, `i128` +- `f32`, `f64` + +And common data structures: + +- `String` and `&str`, utf-8 string data +- `()`, the unit type +- `Option where T: SpacetimeType` +- `Vec where T: SpacetimeType` + +All `#[spacetimedb(table)]` types are `SpacetimeType`s, and accordingly, all of their fields have to be. + +```rust +#[spacetimedb(table)] +struct AnotherTable { + // Fine, some builtin types. + id: u64, + name: Option, + + // Fine, another table type. + table: Table, + + // Fine, another type we explicitly make serializable. + serial: Serial, +} +``` + +If you want to have a field that is not one of the above primitive types, and not a table of its own, you can derive the `SpacetimeType` attribute on it. + +We can derive `SpacetimeType` on `struct`s and `enum`s with members that are themselves `SpacetimeType`s. + +```rust +#[derive(SpacetimeType)] +enum Serial { + Builtin(f64), + Compound { + s: String, + bs: Vec, + } +} +``` + +Once the table is created via the macro, other attributes described below can control more aspects of the table. For instance, a particular column can be indexed, or take on values of an automatically incremented counter. These are described in detail below. + +```rust +#[spacetimedb(table)] +struct Person { + #[unique] + id: u64, + + name: String, + address: String, +} +``` + +### Defining reducers + +`#[spacetimedb(reducer)]` optionally takes a single argument, which is a frequency at which the reducer will be automatically called by the database. + +`#[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>`. + +```rust +#[spacetimedb(reducer)] +fn give_player_item(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::filter_by_item_id(id).ok_or(GameErr::InvalidId)?; + item.owner = Some(player_id); + Item::update_by_id(id, item); + Ok(()) +} + +struct Item { + #[unique] + item_id: u64, + + owner: Option, +} +``` + +Note that reducers can call non-reducer functions, including standard library functions. + +Reducers that are called periodically take an additional macro argument specifying the frequency at which they will be invoked. Durations are parsed according to https://docs.rs/humantime/latest/humantime/fn.parse_duration.html and will usually be a number of milliseconds or seconds. + +Both of these examples are invoked every second. + +```rust +#[spacetimedb(reducer, repeat = 1s)] +fn every_second() {} + +#[spacetimedb(reducer, repeat = 1000ms)] +fn every_thousand_milliseconds() {} +``` + +Finally, reducers can also receive a ReducerContext object, or the Timestamp at which they are invoked, just by taking parameters of those types first. + +```rust +#[spacetimedb(reducer, repeat = 1s)] +fn tick_timestamp(time: Timestamp) { + println!("tick at {time}"); +} + +#[spacetimedb(reducer, repeat = 500ms)] +fn tick_ctx(ctx: ReducerContext) { + println!("tick at {}", ctx.timestamp) +} +``` + +Note that each distinct time a repeating reducer is invoked, a seperate schedule is created for that reducer. So invoking `every_second` three times from the spacetimedb cli will result in the reducer being called times times each second. + +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. + +#[SpacetimeType] + +#[sats] + +## Client API + +Besides the macros for creating tables and reducers, there's two other parts of the Rust SpacetimeDB library. One is a collection of macros for logging, and the other is all the automatically generated functions for operating on those tables. + +### `println!` and friends + +Because reducers run in a WASM sandbox, they don't have access to general purpose I/O from the Rust standard library. There's no filesystem or network access, and no input or output. This means no access to things like `std::println!`, which prints to standard output. + +SpacetimeDB modules have access to logging output. These are exposed as macros, just like their `std` equivalents. The names, and all the Rust formatting machinery, work the same; just the location of the output is different. + +Logs for a module can be viewed with the `spacetime logs` command from the CLI. + +```rust +use spacetimedb::{ + println, + print, + eprintln, + eprint, + dbg, +}; + +#[spacetimedb(reducer)] +fn output(i: i32) { + // These will be logged at log::Level::Info. + println!("an int with a trailing newline: {i}"); + print!("some more text...\n"); + + // These log at log::Level::Error. + eprint!("Oops..."); + eprintln!(", we hit an error"); + + // Just like std::dbg!, this prints its argument and returns the value, + // as a drop-in way to print expressions. So this will print out |i| + // before passing the value of |i| along to the calling function. + // + // The output is logged log::Level::Debug. + OutputtedNumbers::insert(dbg!(i)); +} +``` + +### Generated functions on a SpacetimeDB table + +We'll work off these structs to see what functions SpacetimeDB generates: + +This table has a plain old column. + +```rust +#[spacetimedb(table)] +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. + +```rust +#[spacetimedb(table)] +struct Unique { + // A unique column: + #[unique] + unique_field: u64, +} +``` + +This table has an automatically incrementing column. SpacetimeDB automatically provides an incrementing sequence of values for this field, and sets the field to that value when you insert the row. + +Only integer types can be `#[unique]`: `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64` and `i128`. + +```rust +#[spacetimedb(table)] +struct Autoinc { + #[autoinc] + autoinc_field: u64, +} +``` + +These attributes can be combined, to create an automatically assigned ID usable for filtering. + +```rust +#[spacetimedb(table)] +struct Identity { + #[autoinc] + #[unique] + id_field: u64, +} +``` + +### Insertion + +We'll talk about insertion first, as there a couple of special semantics to know about. + +When we define |Ordinary| as a spacetimedb table, we get the ability to insert into it with the generated `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) { + let ordinary = Ordinary { ordinary_field: value }; + let result = Ordinary::insert(ordinary); + assert_eq!(ordinary.ordinary_field, result.ordinary_field); +} +``` + +When there is a unique column constraint on the table, insertion can fail if a uniqueness constraint is violated. + +If we insert two rows which have the same value of a unique column, the second will fail. + +```rust +#[spacetimedb(reducer)] +fn insert_unique(value: u64) { + let result = Ordinary::insert(Unique { unique_field: value }); + assert!(result.is_ok()); + + let result = Ordinary::insert(Unique { unique_field: value }); + assert!(result.is_err()); +} +``` + +When inserting a table with an `#[autoinc]` column, the database will automatically overwrite whatever we give it with an atomically increasing value. + +The returned row has the `autoinc` column set to the value that was actually written into the database. + +```rust +#[spacetimedb(reducer)] +fn insert_autoinc() { + 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 }) + assert_eq!(actual.autoinc_field, i); + } +} + +#[spacetimedb(reducer)] +fn insert_id() { + 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 }) + } +} +``` + +### Iterating + +Given a table, we can iterate over all the rows in it. + +```rust +#[spacetimedb(table)] +struct Person { + #[unique] + id: u64, + + age: u32, + name: String, + address: String, +} +``` + +// Every table structure an iter function, like: + +```rust +fn MyTable::iter() -> TableIter +``` + +`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() { + let mut addresses = HashSet::new(); + + for person in Person::iter() { + addresses.insert(person.address); + } + + for address in addresses.iter() { + println!("{address}"); + } +} +``` + +### Filtering + +Often, we don't need to look at the entire table, and instead are looking for rows with specific values in certain columns. + +Our `Person` table has a unique id column, so we can filter for a row matching that ID. Since it is unique, we will find either 0 or 1 matching rows in the database. This gets represented naturally as an `Option` in Rust. SpacetimeDB automatically creates and uses indexes for filtering on unique columns, so it is very efficient. + +The name of the filter method just corresponds to the column name. + +```rust +#[spacetimedb(reducer)] +fn filtering(id: u64) { + match Person::filter_by_id(&id) { + Some(person) => println!("Found {person}"), + None => println!("No person with id {id}"), + } +} +``` + +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::filter_by_age(&21) { + println!("{person} has turned 21"); + } +} +``` + +### Deleting + +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) +} +``` + +[macro library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/bindings-macro +[module library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/lib +[demo]: /#demo diff --git a/Writerside2/topics/modules/rust/rust_quickstart.md b/Writerside2/topics/modules/rust/rust_quickstart.md new file mode 100644 index 00000000..baa62a0d --- /dev/null +++ b/Writerside2/topics/modules/rust/rust_quickstart.md @@ -0,0 +1,272 @@ +# Rust Module Quickstart + +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. + +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. + +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. + +## 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. + +## 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: + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +If you're on Windows, go [here](https://learn.microsoft.com/en-us/windows/dev-environment/rust/setup). + +## Project structure + +Create and enter a directory `quickstart-chat`: + +```bash +mkdir quickstart-chat +cd quickstart-chat +``` + +Now create `server`, our module, which runs in the database: + +```bash +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. + +To the top of `server/src/lib.rs`, add some imports we'll be using: + +```rust +use spacetimedb::{spacetimedb, ReducerContext, Identity, Timestamp}; +``` + +From `spacetimedb`, we import: + +- `spacetimedb`, an attribute macro we'll use to define tables and reducers. +- `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. + +## Define tables + +To get our chat server running, we'll need to store two kinds of data: information about each user, and records of all the messages that have been sent. + +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. + +To `server/src/lib.rs`, add the definition of the table `User`: + +```rust +#[spacetimedb(table)] +pub struct User { + #[primarykey] + identity: Identity, + name: Option, + online: bool, +} +``` + +For each `Message`, we'll store the `Identity` of the user who sent it, the `Timestamp` when it was sent, and the text of the message. + +To `server/src/lib.rs`, add the definition of the table `Message`: + +```rust +#[spacetimedb(table)] +pub struct Message { + sender: Identity, + sent: Timestamp, + text: String, +} +``` + +## Set users' names + +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`. + +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> { + 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 }); + Ok(()) + } else { + Err("Cannot set name for unknown user".to_string()) + } +} +``` + +For now, we'll just do a bare minimum of validation, rejecting the empty name. You could extend this in various ways, like: + +- Comparing against a blacklist for moderation purposes. +- Unicode-normalizing names. +- Rejecting names that contain non-printable characters, or removing characters or replacing them with a placeholder. +- Rejecting or truncating long names. +- Rejecting duplicate names. + +To `server/src/lib.rs`, add: + +```rust +/// Takes a name and checks if it's acceptable as a user's name. +fn validate_name(name: String) -> Result { + if name.is_empty() { + Err("Names must not be empty".to_string()) + } else { + Ok(name) + } +} +``` + +## 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`. + +To `server/src/lib.rs`, add: + +```rust +#[spacetimedb(reducer)] +/// Clients invoke this reducer to send messages. +pub fn send_message(ctx: ReducerContext, text: String) -> Result<(), String> { + let text = validate_message(text)?; + log::info!("{}", text); + Message::insert(Message { + sender: ctx.sender, + text, + sent: ctx.timestamp, + }); + Ok(()) +} +``` + +We'll want to validate messages' texts in much the same way we validate users' chosen names. As above, we'll do the bare minimum, rejecting only empty messages. + +To `server/src/lib.rs`, add: + +```rust +/// Takes a message's text and checks if it's acceptable to send. +fn validate_message(text: String) -> Result { + if text.is_empty() { + Err("Messages must not be empty".to_string()) + } else { + Ok(text) + } +} +``` + +You could extend the validation in `validate_message` in similar ways to `validate_name`, or add additional checks to `send_message`, like: + +- Rejecting messages from senders who haven't set their names. +- Rate-limiting users so they can't send new messages too quickly. + +## 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. + +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)]` attribute, 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`. + +To `server/src/lib.rs`, add the definition of the connect reducer: + +```rust +#[spacetimedb(connect)] +// Called when a client connects to the SpacetimeDB +pub fn identity_connected(ctx: ReducerContext) { + if let Some(user) = User::filter_by_identity(&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 }); + } 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 { + 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. + +```rust +#[spacetimedb(disconnect)] +// 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 }); + } else { + // This branch should be unreachable, + // as it doesn't make sense for a client to disconnect without connecting first. + log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender); + } +} +``` + +## 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 ``. + +From the `quickstart-chat` directory, run: + +```bash +spacetime publish --project-path server +``` + +## Call Reducers + +You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. + +```bash +spacetime call send_message 'Hello, World!' +``` + +Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. + +```bash +spacetime logs +``` + +You should now see the output that your module printed in the database. + +```bash +info: Hello, World! +``` + +## SQL Queries + +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" +``` + +```bash + text +--------- + "Hello, World!" +``` + +## What's next? + +You can find the full code for this module [in the SpacetimeDB module examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/modules/quickstart-chat). + +You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quickstart guide for your client language of choice: [Rust](quickstart2.), [C#](quickstart3.), [TypeScript](quickstart4.) or [Python](quickstart5.). + +If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](part-1.) or check out our example game, [BitcraftMini](part-3.). diff --git a/Writerside2/topics/satn.md b/Writerside2/topics/satn.md new file mode 100644 index 00000000..774ff1b3 --- /dev/null +++ b/Writerside2/topics/satn.md @@ -0,0 +1,163 @@ +# SATN JSON Format + +The Spacetime Algebraic Type Notation JSON format defines how Spacetime `AlgebraicType`s and `AlgebraicValue`s are encoded as JSON. Algebraic types and values are JSON-encoded for transport via the [HTTP Databases API](database.) and the [WebSocket text protocol](ws#text-protocol.). + +## Values + +### At a glance + +| Type | Description | +| ---------------- | ---------------------------------------------------------------- | +| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype.). | +| `SumValue` | A value whose type is a [`SumType`](#sumtype.). | +| `ProductValue` | A value whose type is a [`ProductType`](#producttype.). | +| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype.). | +| | | + +### `AlgebraicValue` + +```json +SumValue | ProductValue | BuiltinValue +``` + +### `SumValue` + +An instance of a [`SumType`](#sumtype.). `SumValue`s are encoded as a JSON object with a single key, a non-negative integer tag which identifies the variant. The value associated with this key is the variant data. Variants which hold no data will have an empty array as their value. + +The tag is an index into the [`SumType.variants`](#sumtype.) array of the value's [`SumType`](#sumtype.). + +```json +{ + "": AlgebraicValue +} +``` + +### `ProductValue` + +An instance of a [`ProductType`](#producttype.). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#productype.) array of the value's [`ProductType`](#producttype.). + +```json +array +``` + +### `BuiltinValue` + +An instance of a [`BuiltinType`](#builtintype.). `BuiltinValue`s are encoded as JSON values of corresponding types. + +```json +boolean | number | string | array | map +``` + +| [`BuiltinType`](#builtintype.) | JSON type | +| ----------------------------- | ------------------------------------- | +| `Bool` | `boolean` | +| Integer types | `number` | +| Float types | `number` | +| `String` | `string` | +| Array types | `array` | +| Map types | `map` | + +All SATS integer types are encoded as JSON `number`s, so values of 64-bit and 128-bit integer types may lose precision when encoding values larger than 2⁵². + +## Types + +All SATS types are JSON-encoded by converting them to an `AlgebraicValue`, then JSON-encoding that meta-value. + +### At a glance + +| Type | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------ | +| [`AlgebraicType`](#algebraictype.) | Any SATS type. | +| [`SumType`](#sumtype.) | Sum types, i.e. tagged unions. | +| [`ProductType`](#productype.) | Product types, i.e. structures. | +| [`BuiltinType`](#builtintype.) | Built-in and primitive types, including booleans, numbers, strings, arrays and maps. | +| [`AlgebraicTypeRef`](#algebraictyperef.) | An indirect reference to a type, used to implement recursive types. | + +#### `AlgebraicType` + +`AlgebraicType` is the most general meta-type in the Spacetime Algebraic Type System. Any SATS type can be represented as an `AlgebraicType`. `AlgebraicType` is encoded as a tagged union, with variants for [`SumType`](#sumtype.), [`ProductType`](#producttype.), [`BuiltinType`](#builtintype.) and [`AlgebraicTypeRef`](#algebraictyperef.). + +```json +{ "Sum": SumType } +| { "Product": ProductType } +| { "Builtin": BuiltinType } +| { "Ref": AlgebraicTypeRef } +``` + +#### `SumType` + +The meta-type `SumType` represents sum types, also called tagged unions or Rust `enum`s. A sum type has some number of variants, each of which has an `AlgebraicType` of variant data, and an optional string discriminant. For each instance, exactly one variant will be active. The instance will contain only that variant's data. + +A `SumType` with zero variants is called an empty type or never type because it is impossible to construct an instance. + +Instances of `SumType`s are [`SumValue`s](#sumvalue.), and store a tag which identifies the active variant. + +```json +// SumType: +{ + "variants": array, +} + +// SumTypeVariant: +{ + "algebraic_type": AlgebraicType, + "name": { "some": string } | { "none": [] } +} +``` + +### `ProductType` + +The meta-type `ProductType` represents product types, also called structs or tuples. A product type has some number of fields, each of which has an `AlgebraicType` of field data, and an optional string field name. Each instance will contain data for all of the product type's fields. + +A `ProductType` with zero fields is called a unit type because it has a single instance, the unit, which is empty. + +Instances of `ProductType`s are [`ProductValue`s](#productvalue.), and store an array of field data. + +```json +// ProductType: +{ + "elements": array, +} + +// ProductTypeElement: +{ + "algebraic_type": AlgebraicType, + "name": { "some": string } | { "none": [] } +} +``` + +### `BuiltinType` + +The meta-type `BuiltinType` represents SATS primitive types: booleans, integers, floating-point numbers, strings, arrays and maps. `BuiltinType` is encoded as a tagged union, with a variant for each SATS primitive type. + +SATS integer types are identified by their signedness and width in bits. SATS supports the same set of integer types as Rust, i.e. 8, 16, 32, 64 and 128-bit signed and unsigned integers. + +SATS floating-point number types are identified by their width in bits. SATS supports 32 and 64-bit floats, which correspond to [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) single- and double-precision binary floats, respectively. + +SATS array and map types are homogeneous, meaning that each array has a single element type to which all its elements must conform, and each map has a key type and a value type to which all of its keys and values must conform. + +```json +{ "Bool": [] } +| { "I8": [] } +| { "U8": [] } +| { "I16": [] } +| { "U16": [] } +| { "I32": [] } +| { "U32": [] } +| { "I64": [] } +| { "U64": [] } +| { "I128": [] } +| { "U128": [] } +| { "F32": [] } +| { "F64": [] } +| { "String": [] } +| { "Array": AlgebraicType } +| { "Map": { + "key_ty": AlgebraicType, + "ty": AlgebraicType, + } } +``` + +### `AlgebraicTypeRef` + +`AlgebraicTypeRef`s are JSON-encoded as non-negative integers. These are indices into a typespace, like the one returned by the [`/database/schema/:name_or_address GET` HTTP endpoint](database#databaseschemaname_or_address-get.). diff --git a/Writerside2/topics/sdks/c-sharp/c-sharp_quickstart.md b/Writerside2/topics/sdks/c-sharp/c-sharp_quickstart.md new file mode 100644 index 00000000..4d5b1e92 --- /dev/null +++ b/Writerside2/topics/sdks/c-sharp/c-sharp_quickstart.md @@ -0,0 +1,438 @@ +# C# Client SDK Quick Start + +In this guide we'll show you how to get up and running with a simple SpacetimeDB app with a client written in C#. + +We'll implement a command-line client for the module created in our [Rust](rust_quickstart.md) or [C# Module](quickstart.md) Quickstart guides. Ensure you followed one of these guides before continuing. + +## Project structure + +Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: + +```bash +cd quickstart-chat +``` + +Within it, create a new C# console application project called `client` using either Visual Studio, Rider or the .NET CLI: + +```bash +dotnet new console -o client +``` + +Open the project in your IDE of choice. + +## Add the NuGet package for the C# SpacetimeDB SDK + +Add the `SpacetimeDB.ClientSDK` [NuGet package](https://www.nuget.org/packages/spacetimedbsdk) using Visual Studio or Rider _NuGet Package Manager_ or via the .NET CLI: + +```bash +dotnet add package SpacetimeDB.ClientSDK +``` + +## Generate your module types + +The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. + +In your `quickstart-chat` directory, run: + +```bash +mkdir -p client/module_bindings +spacetime generate --lang csharp --out-dir client/module_bindings --project-path server +``` + +Take a look inside `client/module_bindings`. The CLI should have generated five files: + +``` +module_bindings +├── Message.cs +├── ReducerEvent.cs +├── SendMessageReducer.cs +├── SetNameReducer.cs +└── User.cs +``` + +## Add imports to Program.cs + +Open `client/Program.cs` and add the following imports: + +```csharp +using SpacetimeDB; +using SpacetimeDB.Types; +using System.Collections.Concurrent; +``` + +We will also need to create some global variables that will be explained when we use them later. Add the following to the top of `Program.cs`: + +```csharp +// our local client SpacetimeDB identity +Identity? local_identity = null; + +// declare a thread safe queue to store commands in format (command, args) +ConcurrentQueue<(string,string)> input_queue = new ConcurrentQueue<(string, string)>(); + +// declare a threadsafe cancel token to cancel the process loop +CancellationTokenSource cancel_token = new CancellationTokenSource(); +``` + +## Define 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 several things: + +1. Initialize the `AuthToken` module, which loads and stores our authentication token to/from local storage. +2. Create the `SpacetimeDBClient` instance. +3. Register callbacks on any events we want to handle. These will print to standard output messages received from the database and updates about users' names and online statuses. +4. Start our processing thread which connects to the SpacetimeDB module, updates the SpacetimeDB client and processes commands that come in from the input loop running in the main thread. +5. Start the input loop, which reads commands from standard input and sends them to the processing thread. +6. When the input loop exits, stop the processing thread and wait for it to exit. + +```csharp +void Main() +{ + AuthToken.Init(".spacetime_csharp_quickstart"); + + // create the client, pass in a logger to see debug messages + SpacetimeDBClient.CreateInstance(new ConsoleLogger()); + + RegisterCallbacks(); + + // spawn a thread to call process updates and process commands + var thread = new Thread(ProcessThread); + thread.Start(); + + InputLoop(); + + // this signals the ProcessThread to stop + cancel_token.Cancel(); + thread.Join(); +} +``` + +## Register callbacks + +We need to handle several sorts of events: + +1. `onConnect`: When we connect, we will call `Subscribe` to tell the module what tables we care about. +2. `onIdentityReceived`: When we receive our credentials, we'll use the `AuthToken` module to save our token so that the next time we connect, we can re-authenticate as the same user. +3. `onSubscriptionApplied`: When we get the onSubscriptionApplied callback, that means our local client cache has been fully populated. At this time we'll print the user menu. +4. `User.OnInsert`: When a new user joins, we'll print a message introducing them. +5. `User.OnUpdate`: When a user is updated, we'll print their new name, or declare their new online status. +6. `Message.OnInsert`: When we receive a new message, we'll print it. +7. `Reducer.OnSetNameEvent`: If the server rejects our attempt to set our name, we'll print an error. +8. `Reducer.OnSendMessageEvent`: If the server rejects a message we send, we'll print an error. + +```csharp +void RegisterCallbacks() +{ + SpacetimeDBClient.instance.onConnect += OnConnect; + SpacetimeDBClient.instance.onIdentityReceived += OnIdentityReceived; + SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + + User.OnInsert += User_OnInsert; + User.OnUpdate += User_OnUpdate; + + Message.OnInsert += Message_OnInsert; + + Reducer.OnSetNameEvent += Reducer_OnSetNameEvent; + Reducer.OnSendMessageEvent += Reducer_OnSendMessageEvent; +} +``` + +### 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 `OnInsert` and `OnDelete` methods, which are automatically generated 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.OnInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. + +`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `ReducerEvent`. This will be `null` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument. + +Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `UserNameOrIdentity` to handle this. + +```csharp +string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()!.Substring(0, 8); + +void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) +{ + if (insertedValue.Online) + { + Console.WriteLine($"{UserNameOrIdentity(insertedValue)} is online"); + } +} +``` + +### Notify about updated users + +Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User::update_by_identity` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column. + +`OnUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. + +In our module, users can be updated for three reasons: + +1. They've set their name using the `SetName` reducer. +2. They're an existing user re-connecting, so their `Online` has been set to `true`. +3. They've disconnected, so their `Online` has been set to `false`. + +We'll print an appropriate message in each of these cases. + +```csharp +void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) +{ + if (oldValue.Name != newValue.Name) + { + Console.WriteLine($"{UserNameOrIdentity(oldValue)} renamed to {newValue.Name}"); + } + + if (oldValue.Online == newValue.Online) + return; + + if (newValue.Online) + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); + } + else + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); + } +} +``` + +### 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 `SendMessage` reducer invocation. We have to handle the backlog we receive when our subscription is initialized separately, to ensure they're printed in the correct order. To that effect, our `OnInsert` callback will check if its `ReducerEvent` argument is not `null`, and only print in that case. + +To find the `User` based on the message's `Sender` identity, we'll use `User::FilterByIdentity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `FilterByIdentity` accepts a `byte[]`, rather than an `Identity`. The `Sender` identity stored in the message is also a `byte[]`, not an `Identity`, so we can just pass it to the filter method. + +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`. + +```csharp +void PrintMessage(Message message) +{ + var sender = User.FilterByIdentity(message.Sender); + var senderName = "unknown"; + if (sender != null) + { + senderName = UserNameOrIdentity(sender); + } + + Console.WriteLine($"{senderName}: {message.Text}"); +} + +void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent) +{ + if (dbEvent != null) + { + PrintMessage(insertedValue); + } +} +``` + +### Warn if our name was rejected + +We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducerEvent` method of the `Reducer` namespace, which is automatically implemented for each reducer by `spacetime generate`. + +Each reducer callback takes one fixed argument: + +The ReducerEvent that triggered the callback. It contains several fields. The ones we care about are: + +1. The `Identity` of the client that called the reducer. +2. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`. +3. The error message, if any, that the reducer returned. + +It also takes a variable amount of additional arguments that match the reducer's arguments. + +These callbacks will be invoked in one of two cases: + +1. If the reducer was successful and altered any of our subscribed rows. +2. If we requested an invocation which failed. + +Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. + +We already handle successful `SetName` invocations using our `User.OnUpdate` 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 `Reducer_OnSetNameEvent` as a `Reducer.OnSetNameEvent` callback which checks if the reducer failed, and if it did, prints an error message including the rejected name. + +We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. + +```csharp +void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) +{ + bool localIdentityFailedToChangeName = + reducerEvent.Identity == local_identity && + reducerEvent.Status == ClientApi.Event.Types.Status.Failed; + + if (localIdentityFailedToChangeName) + { + Console.Write($"Failed to change name to {name}"); + } +} +``` + +### 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. + +```csharp +void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) +{ + bool localIdentityFailedToSendMessage = + reducerEvent.Identity == local_identity && + reducerEvent.Status == ClientApi.Event.Types.Status.Failed; + + if (localIdentityFailedToSendMessage) + { + Console.Write($"Failed to send message {text}"); + } +} +``` + +## Connect callback + +Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. + +```csharp +void OnConnect() +{ + SpacetimeDBClient.instance.Subscribe(new List + { + "SELECT * FROM User", "SELECT * FROM Message" + }); +} +``` + +## OnIdentityReceived callback + +This callback is executed when we receive our credentials from the SpacetimeDB module. We'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app. + +```csharp +void OnIdentityReceived(string authToken, Identity identity, Address _address) +{ + local_identity = identity; + AuthToken.SaveToken(authToken); +} +``` + +## OnSubscriptionApplied callback + +Once our subscription is applied, we'll print all the previously sent messages. We'll define a function `PrintMessagesInOrder` to do this. `PrintMessagesInOrder` calls the automatically generated `Iter` function on our `Message` table, which returns an iterator over all rows in the table. We'll use the `OrderBy` method on the iterator to sort the messages by their `Sent` timestamp. + +```csharp +void PrintMessagesInOrder() +{ + foreach (Message message in Message.Iter().OrderBy(item => item.Sent)) + { + PrintMessage(message); + } +} + +void OnSubscriptionApplied() +{ + Console.WriteLine("Connected"); + PrintMessagesInOrder(); +} +``` + + + +## Process thread + +Since the input loop will be blocking, we'll run our processing code in a separate thread. This thread will: + +1. Connect to the module. We'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`. We will also store if SSL is enabled in a constant called `SSL_ENABLED`. This only needs to be `true` if we are using `SpacetimeDB Cloud`. Replace `` with the name you chose when publishing your module during the module quickstart. + +`Connect` takes an auth token, which is `null` for a new connection, or a stored string for a returning user. We are going to use the optional AuthToken module which uses local storage to store the auth token. If you want to use your own way to associate an auth token with a user, you can pass in your own auth token here. + +2. Loop until the thread is signaled to exit, calling `Update` on the SpacetimeDBClient to process any updates received from the module, and `ProcessCommand` to process any commands received from the input loop. + +3. Finally, Close the connection to the module. + +```csharp +const string HOST = "http://localhost:3000"; +const string DBNAME = "module"; + +void ProcessThread() +{ + SpacetimeDBClient.instance.Connect(AuthToken.Token, HOST, DBNAME); + + // loop until cancellation token + while (!cancel_token.IsCancellationRequested) + { + SpacetimeDBClient.instance.Update(); + + ProcessCommands(); + + Thread.Sleep(100); + } + + SpacetimeDBClient.instance.Close(); +} +``` + +## Input loop and ProcessCommands + +The input loop will read commands from standard input and send them to the processing thread using the input queue. The `ProcessCommands` function is called every 100ms by the processing thread to process any pending commands. + +Supported Commands: + +1. Send a message: `message`, send the message to the module by calling `Reducer.SendMessage` which is automatically generated by `spacetime generate`. + +2. Set name: `name`, will send the new name to the module by calling `Reducer.SetName` which is automatically generated by `spacetime generate`. + +```csharp +void InputLoop() +{ + while (true) + { + var input = Console.ReadLine(); + if (input == null) + { + break; + } + + if (input.StartsWith("/name ")) + { + input_queue.Enqueue(("name", input.Substring(6))); + continue; + } + else + { + input_queue.Enqueue(("message", input)); + } + } +} + +void ProcessCommands() +{ + // process input queue commands + while (input_queue.TryDequeue(out var command)) + { + switch (command.Item1) + { + case "message": + Reducer.SendMessage(command.Item2); + break; + case "name": + Reducer.SetName(command.Item2); + break; + } + } +} +``` + +## Run the client + +Finally we just need to add a call to `Main` in `Program.cs`: + +```csharp +Main(); +``` + +Now, we can run the client by hitting start in Visual Studio or Rider; or by running the following command in the `client` directory: + +```bash +dotnet run --project client +``` + +## What's next? + +Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example. diff --git a/Writerside2/topics/sdks/c-sharp/sdks_c-sharp_index.md b/Writerside2/topics/sdks/c-sharp/sdks_c-sharp_index.md new file mode 100644 index 00000000..a0f1c7f3 --- /dev/null +++ b/Writerside2/topics/sdks/c-sharp/sdks_c-sharp_index.md @@ -0,0 +1,959 @@ +# The SpacetimeDB C# client SDK + +The SpacetimeDB client C# for Rust contains all the tools you need to build native clients for SpacetimeDB modules using C#. + +## Table of Contents + +- [The SpacetimeDB C# client SDK](#the-spacetimedb-c-client-sdk.) + - [Table of Contents](#table-of-contents.) + - [Install the SDK](#install-the-sdk.) + - [Using the `dotnet` CLI tool](#using-the-dotnet-cli-tool.) + - [Using Unity](#using-unity.) + - [Generate module bindings](#generate-module-bindings.) + - [Initialization](#initialization.) + - [Static Method `SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) + - [Property `SpacetimeDBClient.instance`](#property-spacetimedbclientinstance.) + - [Class `NetworkManager`](#class-networkmanager.) + - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect.) + - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived.) + - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect.) + - [Query subscriptions & one-time actions](#subscribe-to-queries.) + - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.) + - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied.) + - [Method `SpacetimeDBClient.OneOffQuery`](#event-spacetimedbclientoneoffquery.) + - [View rows of subscribed tables](#view-rows-of-subscribed-tables.) + - [Class `{TABLE}`](#class-table.) + - [Static Method `{TABLE}.Iter`](#static-method-tableiter.) + - [Static Method `{TABLE}.FilterBy{COLUMN}`](#static-method-tablefilterbycolumn.) + - [Static Method `{TABLE}.Count`](#static-method-tablecount.) + - [Static Event `{TABLE}.OnInsert`](#static-event-tableoninsert.) + - [Static Event `{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) + - [Static Event `{TABLE}.OnDelete`](#static-event-tableondelete.) + - [Static Event `{TABLE}.OnUpdate`](#static-event-tableonupdate.) + - [Observe and invoke reducers](#observe-and-invoke-reducers.) + - [Class `Reducer`](#class-reducer.) + - [Static Method `Reducer.{REDUCER}`](#static-method-reducerreducer.) + - [Static Event `Reducer.On{REDUCER}`](#static-event-reduceronreducer.) + - [Class `ReducerEvent`](#class-reducerevent.) + - [Enum `Status`](#enum-status.) + - [Variant `Status.Committed`](#variant-statuscommitted.) + - [Variant `Status.Failed`](#variant-statusfailed.) + - [Variant `Status.OutOfEnergy`](#variant-statusoutofenergy.) + - [Identity management](#identity-management.) + - [Class `AuthToken`](#class-authtoken.) + - [Static Method `AuthToken.Init`](#static-method-authtokeninit.) + - [Static Property `AuthToken.Token`](#static-property-authtokentoken.) + - [Static Method `AuthToken.SaveToken`](#static-method-authtokensavetoken.) + - [Class `Identity`](#class-identity.) + - [Class `Identity`](#class-identity-1.) + - [Customizing logging](#customizing-logging.) + - [Interface `ISpacetimeDBLogger`](#interface-ispacetimedblogger.) + - [Class `ConsoleLogger`](#class-consolelogger.) + - [Class `UnityDebugLogger`](#class-unitydebuglogger.) + +## Install the SDK + +### Using the `dotnet` CLI tool + +If you would like to create a console application using .NET, you can create a new project using `dotnet new console` and add the SpacetimeDB SDK to your dependencies: + +```bash +dotnet add package spacetimedbsdk +``` + +(See also the [CSharp Quickstart](quickstart1.) for an in-depth example of such a console application.) + +### Using Unity + +To install the SpacetimeDB SDK into a Unity project, [download the SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest), packaged as a `.unitypackage`. + +In Unity navigate to the `Assets > Import Package > Custom Package` menu in the menu bar. Select your `SpacetimeDB.Unity.Comprehensive.Tutorial.unitypackage` file and leave all folders checked. + +(See also the [Unity Tutorial](part-1.)) + +## Generate module bindings + +Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the C# interface files using the Spacetime CLI. From your project directory, run: + +```bash +mkdir -p module_bindings +spacetime generate --lang cs --out-dir module_bindings --project-path PATH-TO-MODULE-DIRECTORY +``` + +Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. + +## Initialization + +### Static Method `SpacetimeDBClient.CreateInstance` + +```cs +namespace SpacetimeDB { + +public class SpacetimeDBClient { + public static void CreateInstance(ISpacetimeDBLogger loggerToUse); +} + +} +``` + +Create a global SpacetimeDBClient instance, accessible via [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance.) + +| Argument | Type | Meaning | +| ------------- | ----------------------------------------------------- | --------------------------------- | +| `loggerToUse` | [`ISpacetimeDBLogger`](#interface-ispacetimedblogger.) | The logger to use to log messages | + +There is a provided logger called [`ConsoleLogger`](#class-consolelogger.) which logs to `System.Console`, and can be used as follows: + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; +SpacetimeDBClient.CreateInstance(new ConsoleLogger()); +``` + +### Property `SpacetimeDBClient.instance` + +```cs +namespace SpacetimeDB { + +public class SpacetimeDBClient { + public static SpacetimeDBClient instance; +} + +} +``` + +This is the global instance of a SpacetimeDB client in a particular .NET/Unity process. Much of the SDK is accessible through this instance. + +### Class `NetworkManager` + +The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. + +![Unity-AddNetworkManager](Unity-AddNetworkManager.JPG) + +This component will handle calling [`SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect.), you still need to handle that yourself. See the [Unity Quickstart](UnityQuickStart.) and [Unity Tutorial](UnityTutorialPart1.) for more information. + +### Method `SpacetimeDBClient.Connect` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public void Connect( + string? token, + string host, + string addressOrName, + bool sslEnabled = true + ); +} + +} +``` + + + +Connect to a database named `addressOrName` accessible over the internet at the URI `host`. + +| Argument | Type | Meaning | +| --------------- | --------- | -------------------------------------------------------------------------- | +| `token` | `string?` | Identity token to use, if one is available. | +| `host` | `string` | URI of the SpacetimeDB instance running the module. | +| `addressOrName` | `string` | Address or name of the module. | +| `sslEnabled` | `bool` | Whether or not to use SSL when connecting to SpacetimeDB. Default: `true`. | + +If a `token` is supplied, it will be passed to the new connection to identify and authenticate the user. Otherwise, a new token and [`Identity`](#class-identity.) will be generated by the server and returned in [`onConnect`](#event-spacetimedbclientonconnect.). + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +const string DBNAME = "chat"; + +// Connect to a local DB with a fresh identity +SpacetimeDBClient.instance.Connect(null, "localhost:3000", DBNAME, false); + +// Connect to cloud with a fresh identity +SpacetimeDBClient.instance.Connect(null, "dev.spacetimedb.net", DBNAME, true); + +// Connect to cloud using a saved identity from the filesystem, or get a new one and save it +AuthToken.Init(); +Identity localIdentity; +SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); +SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { + AuthToken.SaveToken(authToken); + localIdentity = identity; +} +``` + +(You should probably also store the returned `Identity` somewhere; see the [`onIdentityReceived`](#event-spacetimedbclientonidentityreceived.) event.) + +### Event `SpacetimeDBClient.onIdentityReceived` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public event Action onIdentityReceived; +} + +} +``` + ++Called when we receive an auth token, [`Identity`](#class-identity.) and [`Address`](#class-address.) from the server. The [`Identity`](#class-identity.) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The [`Address`](#class-address.) is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity.). + +To store the auth token to the filesystem, use the static method [`AuthToken.SaveToken`](#static-method-authtokensavetoken.). You may also want to store the returned [`Identity`](#class-identity.) in a local variable. + +If an existing auth token is used to connect to the database, the same auth token and the identity it came with will be returned verbatim in `onIdentityReceived`. + +```cs +// Connect to cloud using a saved identity from the filesystem, or get a new one and save it +AuthToken.Init(); +Identity localIdentity; +SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); +SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { + AuthToken.SaveToken(authToken); + localIdentity = identity; +} +``` + +### Event `SpacetimeDBClient.onConnect` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public event Action onConnect; +} + +} +``` + +Allows registering delegates to be invoked upon authentication with the database. + +Once this occurs, the SDK is prepared for calls to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.). + +## Subscribe to queries + +### Method `SpacetimeDBClient.Subscribe` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public void Subscribe(List queries); +} + +} +``` + +| Argument | Type | Meaning | +| --------- | -------------- | ---------------------------- | +| `queries` | `List` | SQL queries to subscribe to. | + +Subscribe to a set of queries, to be notified when rows which match those queries are altered. + +`Subscribe` will return an error if called before establishing a connection with the [`SpacetimeDBClient.Connect`](#method-connect.) function. In that case, the queries are not registered. + +The `Subscribe` method does not return data directly. `spacetime generate` will generate classes [`SpacetimeDB.Types.{TABLE}`](#class-table.) for each table in your module. These classes are used to reecive information from the database. See the section [View Rows of Subscribed Tables](#view-rows-of-subscribed-tables.) for more information. + +A new call to `Subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache, and [`{TABLE}.OnDelete`](#event-tableondelete.) callbacks will be invoked for them. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +void Main() +{ + AuthToken.Init(); + SpacetimeDBClient.CreateInstance(new ConsoleLogger()); + + SpacetimeDBClient.instance.onConnect += OnConnect; + + // Our module contains a table named "Loot" + Loot.OnInsert += Loot_OnInsert; + + SpacetimeDBClient.instance.Connect(/* ... */); +} + +void OnConnect() +{ + SpacetimeDBClient.instance.Subscribe(new List { + "SELECT * FROM Loot" + }); +} + +void Loot_OnInsert( + Loot loot, + ReducerEvent? event +) { + Console.Log($"Loaded loot {loot.itemType} at coordinates {loot.position}"); +} +``` + +### Event `SpacetimeDBClient.onSubscriptionApplied` + +```cs +namespace SpacetimeDB { + +class SpacetimeDBClient { + public event Action onSubscriptionApplied; +} + +} +``` + +Register a delegate to be invoked when a subscription is registered with the database. + +```cs +using SpacetimeDB; + +void OnSubscriptionApplied() +{ + Console.WriteLine("Now listening on queries."); +} + +void Main() +{ + // ...initialize... + SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; +} +``` + +### Method [`OneTimeQuery`](#method-spacetimedbclientsubscribe.) + +You may not want to subscribe to a query, but instead want to run a query once and receive the results immediately via a `Task` result: + +```csharp +// Query all Messages from the sender "bob" +SpacetimeDBClient.instance.OneOffQuery("WHERE sender = \"bob\""); +``` + +## View rows of subscribed tables + +The SDK maintains a local view of the database called the "client cache". This cache contains whatever rows are selected via a call to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.). These rows are represented in the SpacetimeDB .Net SDK as instances of [`SpacetimeDB.Types.{TABLE}`](#class-table.). + +ONLY the rows selected in a [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.) call will be available in the client cache. All operations in the client sdk operate on these rows exclusively, and have no information about the state of the rest of the database. + +In particular, SpacetimeDB does not support foreign key constraints. This means that if you are using a column as a foreign key, SpacetimeDB will not automatically bring in all of the rows that key might reference. You will need to manually subscribe to all tables you need information from. + +To optimize network performance, prefer selecting as few rows as possible in your [`Subscribe`](#method-spacetimedbclientsubscribe.) query. Processes that need to view the entire state of the database are better run inside the database -- that is, inside modules. + +### Class `{TABLE}` + +For each table defined by a module, `spacetime generate` will generate a class [`SpacetimeDB.Types.{TABLE}`](#class-table.) whose name is that table's name converted to `PascalCase`. The generated class contains a property for each of the table's columns, whose names are the column names converted to `camelCase`. It also contains various static events and methods. + +Static Methods: + +- [`{TABLE}.Iter()`](#static-method-tableiter.) iterates all subscribed rows in the client cache. +- [`{TABLE}.FilterBy{COLUMN}(value)`](#static-method-tablefilterbycolumn.) filters subscribed rows in the client cache by a column value. +- [`{TABLE}.Count()`](#static-method-tablecount.) counts the number of subscribed rows in the client cache. + +Static Events: + +- [`{TABLE}.OnInsert`](#static-event-tableoninsert.) is called when a row is inserted into the client cache. +- [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) is called when a row is about to be removed from the client cache. +- If the table has a primary key attribute, [`{TABLE}.OnUpdate`](#static-event-tableonupdate.) is called when a row is updated. +- [`{TABLE}.OnDelete`](#static-event-tableondelete.) is called while a row is being removed from the client cache. You should almost always use [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) instead. + +Note that it is not possible to directly insert into the database from the client SDK! All insertion validation should be performed inside serverside modules for security reasons. You can instead [invoke reducers](#observe-and-invoke-reducers.), which run code inside the database that can insert rows for you. + +#### Static Method `{TABLE}.Iter` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public static System.Collections.Generic.IEnumerable
Iter(); +} + +} +``` + +Iterate over all the subscribed rows in the table. This method is only available after [`SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied.) has occurred. + +When iterating over rows and filtering for those containing a particular column, [`TableType::filter`](#method-filter.) will be more efficient, so prefer it when possible. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { + SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); +}; +SpacetimeDBClient.instance.onSubscriptionApplied += () => { + // Will print a line for each `User` row in the database. + foreach (var user in User.Iter()) { + Console.WriteLine($"User: {user.Name}"); + } +}; +SpacetimeDBClient.instance.connect(/* ... */); +``` + +#### Static Method `{TABLE}.FilterBy{COLUMN}` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + // If the column has no #[unique] or #[primarykey] constraint + public static System.Collections.Generic.IEnumerable
FilterBySender(COLUMNTYPE value); + + // If the column has a #[unique] or #[primarykey] constraint + public static TABLE? FilterBySender(COLUMNTYPE value); +} + +} +``` + +For each column of a table, `spacetime generate` generates a static method on the [table class](#class-table.) to filter or seek subscribed rows where that column matches a requested value. These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `PascalCase`. + +The method's return type depends on the column's attributes: + +- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filterBy{COLUMN}` method returns a `{TABLE}?`, where `{TABLE}` is the [table class](#class-table.). +- For non-unique columns, the `filter_by` method returns an `IEnumerator<{TABLE}>`. + +#### Static Method `{TABLE}.Count` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public static int Count(); +} + +} +``` + +Return the number of subscribed rows in the table, or 0 if there is no active connection. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { + SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); +}; +SpacetimeDBClient.instance.onSubscriptionApplied += () => { + Console.WriteLine($"There are {User.Count()} users in the database."); +}; +SpacetimeDBClient.instance.connect(/* ... */); +``` + +#### Static Event `{TABLE}.OnInsert` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public delegate void InsertEventHandler( + TABLE insertedValue, + ReducerEvent? dbEvent + ); + public static event InsertEventHandler OnInsert; +} + +} +``` + +Register a delegate for when a subscribed row is newly inserted into the database. + +The delegate takes two arguments: + +- A [`{TABLE}`](#class-table.) instance with the data of the inserted row +- A [`ReducerEvent?`], which contains the data of the reducer that inserted the row, or `null` if the row is being inserted while initializing a subscription. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +/* initialize, subscribe to table User... */ + +User.OnInsert += (User user, ReducerEvent? reducerEvent) => { + if (reducerEvent == null) { + Console.WriteLine($"New user '{user.Name}' received during subscription update."); + } else { + Console.WriteLine($"New user '{user.Name}' inserted by reducer {reducerEvent.Reducer}."); + } +}; +``` + +#### Static Event `{TABLE}.OnBeforeDelete` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public delegate void DeleteEventHandler( + TABLE deletedValue, + ReducerEvent dbEvent + ); + public static event DeleteEventHandler OnBeforeDelete; +} + +} +``` + +Register a delegate for when a subscribed row is about to be deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked for each of those rows before any of them is deleted. + +The delegate takes two arguments: + +- A [`{TABLE}`](#class-table.) instance with the data of the deleted row +- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that deleted the row. + +This event should almost always be used instead of [`OnDelete`](#static-event-tableondelete.). This is because often, many rows will be deleted at once, and `OnDelete` can be invoked in an arbitrary order on these rows. This means that data related to a row may already be missing when `OnDelete` is called. `OnBeforeDelete` does not have this problem. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +/* initialize, subscribe to table User... */ + +User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { + Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); +}; +``` + +#### Static Event `{TABLE}.OnDelete` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public delegate void DeleteEventHandler( + TABLE deletedValue, + SpacetimeDB.ReducerEvent dbEvent + ); + public static event DeleteEventHandler OnDelete; +} + +} +``` + +Register a delegate for when a subscribed row is being deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked on those rows in arbitrary order, and data for some rows may already be missing when it is invoked. For this reason, prefer the event [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.). + +The delegate takes two arguments: + +- A [`{TABLE}`](#class-table.) instance with the data of the deleted row +- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that deleted the row. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +/* initialize, subscribe to table User... */ + +User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { + Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); +}; +``` + +#### Static Event `{TABLE}.OnUpdate` + +```cs +namespace SpacetimeDB.Types { + +class TABLE { + public delegate void UpdateEventHandler( + TABLE oldValue, + TABLE newValue, + ReducerEvent dbEvent + ); + public static event UpdateEventHandler OnUpdate; +} + +} +``` + +Register a delegate for when a subscribed row is being updated. This event is only available if the row has a column with the `#[primary_key]` attribute. + +The delegate takes three arguments: + +- A [`{TABLE}`](#class-table.) instance with the old data of the updated row +- A [`{TABLE}`](#class-table.) instance with the new data of the updated row +- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that updated the row. + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; + +/* initialize, subscribe to table User... */ + +User.OnUpdate += (User oldUser, User newUser, ReducerEvent reducerEvent) => { + Debug.Assert(oldUser.UserId == newUser.UserId, "Primary key never changes in an update"); + + Console.WriteLine($"User with ID {oldUser.UserId} had name changed "+ + $"from '{oldUser.Name}' to '{newUser.Name}' by reducer {reducerEvent.Reducer}."); +}; +``` + +## Observe and invoke reducers + +"Reducer" is SpacetimeDB's name for the stored procedures that run in modules inside the database. You can invoke reducers from a connected client SDK, and also receive information about which reducers are running. + +`spacetime generate` generates a class [`SpacetimeDB.Types.Reducer`](#class-reducer.) that contains methods and events for each reducer defined in a module. To invoke a reducer, use the method [`Reducer.{REDUCER}`](#static-method-reducerreducer.) generated for it. To receive a callback each time a reducer is invoked, use the static event [`Reducer.On{REDUCER}`](#static-event-reduceronreducer.). + +### Class `Reducer` + +```cs +namespace SpacetimeDB.Types { + +class Reducer {} + +} +``` + +This class contains a static method and event for each reducer defined in a module. + +#### Static Method `Reducer.{REDUCER}` + +```cs +namespace SpacetimeDB.Types { +class Reducer { + +/* void {REDUCER_NAME}(...ARGS...) */ + +} +} +``` + +For each reducer defined by a module, `spacetime generate` generates a static method which sends a request to the database to invoke that reducer. The generated function's name is the reducer's name converted to `PascalCase`. + +Reducers don't run immediately! They run as soon as the request reaches the database. Don't assume data inserted by a reducer will be available immediately after you call this method. + +For reducers which accept a `ReducerContext` as their first argument, the `ReducerContext` is not included in the generated function's argument list. + +For example, if we define a reducer in Rust as follows: + +```rust +#[spacetimedb(reducer)] +pub fn set_name( + ctx: ReducerContext, + user_id: u64, + name: String +) -> Result<(), Error>; +``` + +The following C# static method will be generated: + +```cs +namespace SpacetimeDB.Types { +class Reducer { + +public static void SendMessage(UInt64 userId, string name); + +} +} +``` + +#### Static Event `Reducer.On{REDUCER}` + +```cs +namespace SpacetimeDB.Types { +class Reducer { + +public delegate void /*{REDUCER}*/Handler(ReducerEvent reducerEvent, /* {ARGS...} */); + +public static event /*{REDUCER}*/Handler On/*{REDUCER}*/Event; + +} +} +``` + +For each reducer defined by a module, `spacetime generate` generates an event to run each time the reducer is invoked. The generated functions are named `on{REDUCER}Event`, where `{REDUCER}` is the reducer's name converted to `PascalCase`. + +The first argument to the event handler is an instance of [`SpacetimeDB.Types.ReducerEvent`](#class-reducerevent.) describing the invocation -- its timestamp, arguments, and whether it succeeded or failed. The remaining arguments are the arguments passed to the reducer. Reducers cannot have return values, so no return value information is included. + +For example, if we define a reducer in Rust as follows: + +```rust +#[spacetimedb(reducer)] +pub fn set_name( + ctx: ReducerContext, + user_id: u64, + name: String +) -> Result<(), Error>; +``` + +The following C# static method will be generated: + +```cs +namespace SpacetimeDB.Types { +class Reducer { + +public delegate void SetNameHandler( + ReducerEvent reducerEvent, + UInt64 userId, + string name +); +public static event SetNameHandler OnSetNameEvent; + +} +} +``` + +Which can be used as follows: + +```cs +/* initialize, wait for onSubscriptionApplied... */ + +Reducer.SetNameHandler += ( + ReducerEvent reducerEvent, + UInt64 userId, + string name +) => { + if (reducerEvent.Status == ClientApi.Event.Types.Status.Committed) { + Console.WriteLine($"User with id {userId} set name to {name}"); + } else if (reducerEvent.Status == ClientApi.Event.Types.Status.Failed) { + Console.WriteLine( + $"User with id {userId} failed to set name to {name}:" + + reducerEvent.ErrMessage + ); + } else if (reducerEvent.Status == ClientApi.Event.Types.Status.OutOfEnergy) { + Console.WriteLine( + $"User with id {userId} failed to set name to {name}:" + + "Invoker ran out of energy" + ); + } +}; +Reducer.SetName(USER_ID, NAME); +``` + +### Class `ReducerEvent` + +`spacetime generate` defines an class `ReducerEvent` containing an enum `ReducerType` with a variant for each reducer defined by a module. The variant's name will be the reducer's name converted to `PascalCase`. + +For example, the example project shown in the Rust Module quickstart will generate the following (abridged) code. + +```cs +namespace SpacetimeDB.Types { + +public enum ReducerType +{ + /* A member for each reducer in the module, with names converted to PascalCase */ + None, + SendMessage, + SetName, +} +public partial class SendMessageArgsStruct +{ + /* A member for each argument of the reducer SendMessage, with names converted to PascalCase. */ + public string Text; +} +public partial class SetNameArgsStruct +{ + /* A member for each argument of the reducer SetName, with names converted to PascalCase. */ + public string Name; +} +public partial class ReducerEvent : ReducerEventBase { + // Which reducer was invoked + public ReducerType Reducer { get; } + // If event.Reducer == ReducerType.SendMessage, the arguments + // sent to the SendMessage reducer. Otherwise, accesses will + // throw a runtime error. + public SendMessageArgsStruct SendMessageArgs { get; } + // If event.Reducer == ReducerType.SetName, the arguments + // passed to the SetName reducer. Otherwise, accesses will + // throw a runtime error. + public SetNameArgsStruct SetNameArgs { get; } + + /* Additional information, present on any ReducerEvent */ + // The name of the reducer. + public string ReducerName { get; } + // The timestamp of the reducer invocation inside the database. + public ulong Timestamp { get; } + // The identity of the client that invoked the reducer. + public SpacetimeDB.Identity Identity { get; } + // Whether the reducer succeeded, failed, or ran out of energy. + public ClientApi.Event.Types.Status Status { get; } + // If event.Status == Status.Failed, the error message returned from inside the module. + public string ErrMessage { get; } +} + +} +``` + +#### Enum `Status` + +```cs +namespace ClientApi { +public sealed partial class Event { +public static partial class Types { + +public enum Status { + Committed = 0, + Failed = 1, + OutOfEnergy = 2, +} + +} +} +} +``` + +An enum whose variants represent possible reducer completion statuses of a reducer invocation. + +##### Variant `Status.Committed` + +The reducer finished successfully, and its row changes were committed to the database. + +##### Variant `Status.Failed` + +The reducer failed, either by panicking or returning a `Err`. + +##### Variant `Status.OutOfEnergy` + +The reducer was canceled because the module owner had insufficient energy to allow it to run to completion. + +## Identity management + +### Class `AuthToken` + +The AuthToken helper class handles creating and saving SpacetimeDB identity tokens in the filesystem. + +#### Static Method `AuthToken.Init` + +```cs +namespace SpacetimeDB { + +class AuthToken { + public static void Init( + string configFolder = ".spacetime_csharp_sdk", + string configFile = "settings.ini", + string? configRoot = null + ); +} + +} +``` + +Creates a file `$"{configRoot}/{configFolder}/{configFile}"` to store tokens. +If no arguments are passed, the default is `"%HOME%/.spacetime_csharp_sdk/settings.ini"`. + +| Argument | Type | Meaning | +| -------------- | -------- | ---------------------------------------------------------------------------------- | +| `configFolder` | `string` | The folder to store the config file in. Default is `"spacetime_csharp_sdk"`. | +| `configFile` | `string` | The name of the config file. Default is `"settings.ini"`. | +| `configRoot` | `string` | The root folder to store the config file in. Default is the user's home directory. | + +#### Static Property `AuthToken.Token` + +```cs +namespace SpacetimeDB { + +class AuthToken { + public static string? Token { get; } +} + +} +``` + +The auth token stored on the filesystem, if one exists. + +#### Static Method `AuthToken.SaveToken` + +```cs +namespace SpacetimeDB { + +class AuthToken { + public static void SaveToken(string token); +} + +} +``` + +Save a token to the filesystem. + +### Class `Identity` + +```cs +namespace SpacetimeDB +{ + public struct Identity : IEquatable + { + public byte[] Bytes { get; } + public static Identity From(byte[] bytes); + public bool Equals(Identity other); + public static bool operator ==(Identity a, Identity b); + public static bool operator !=(Identity a, Identity b); + } +} +``` + +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. + +### Class `Identity` +```cs +namespace SpacetimeDB +{ + public struct Address : IEquatable
+ { + public byte[] Bytes { get; } + public static Address? From(byte[] bytes); + public bool Equals(Address other); + public static bool operator ==(Address a, Address b); + public static bool operator !=(Address a, Address b); + } +} +``` + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity.). + +## Customizing logging + +The SpacetimeDB C# SDK performs internal logging. Instances of [`ISpacetimeDBLogger`](#interface-ispacetimedblogger.) can be passed to [`SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) to customize how SDK logs are delivered to your application. + +This is set up automatically for you if you use Unity-- adding a [`NetworkManager`](#class-networkmanager.) component to your unity scene will automatically initialize the `SpacetimeDBClient` with a [`UnityDebugLogger`](#class-unitydebuglogger.). + +Outside of unity, all you need to do is the following: + +```cs +using SpacetimeDB; +using SpacetimeDB.Types; +SpacetimeDBClient.CreateInstance(new ConsoleLogger()); +``` + +### Interface `ISpacetimeDBLogger` + +```cs +namespace SpacetimeDB +{ + +public interface ISpacetimeDBLogger +{ + void Log(string message); + void LogError(string message); + void LogWarning(string message); + void LogException(Exception e); +} + +} +``` + +This interface provides methods that are invoked when the SpacetimeDB C# SDK needs to log at various log levels. You can create custom implementations if needed to integrate with existing logging solutions. + +### Class `ConsoleLogger` + +```cs +namespace SpacetimeDB { + +public class ConsoleLogger : ISpacetimeDBLogger {} + +} +``` + +An `ISpacetimeDBLogger` implementation for regular .NET applications, using `Console.Write` when logs are received. + +### Class `UnityDebugLogger` + +```cs +namespace SpacetimeDB { + +public class UnityDebugLogger : ISpacetimeDBLogger {} + +} +``` + +An `ISpacetimeDBLogger` implementation for Unity, using the Unity `Debug.Log` api. diff --git a/Writerside2/topics/sdks/python/python_index.md b/Writerside2/topics/sdks/python/python_index.md new file mode 100644 index 00000000..a87d8ac5 --- /dev/null +++ b/Writerside2/topics/sdks/python/python_index.md @@ -0,0 +1,552 @@ +# The SpacetimeDB Python client SDK + +The SpacetimeDB client SDK for Python contains all the tools you need to build native clients for SpacetimeDB modules using Python. + +## Install the SDK + +Use pip to install the SDK: + +```bash +pip install spacetimedb-sdk +``` + +## Generate module bindings + +Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the Python interface files using the Spacetime CLI. From your project directory, run: + +```bash +mkdir -p module_bindings +spacetime generate --lang python \ + --out-dir module_bindings \ + --project-path PATH-TO-MODULE-DIRECTORY +``` + +Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. + +Import your bindings in your client's code: + +```python +import module_bindings +``` + +## Basic vs Async SpacetimeDB Client + +This SDK provides two different client modules for interacting with your SpacetimeDB module. + +The Basic client allows you to have control of the main loop of your application and you are responsible for regularly calling the client's `update` function. This is useful in settings like PyGame where you want to have full control of the main loop. + +The Async client has a run function that you call after you set up all your callbacks and it will take over the main loop and handle updating the client for you. With the async client, you can have a regular "tick" function by using the `schedule_event` function. + +## Common Client Reference + +The following functions and types are used in both the Basic and Async clients. + +### API at a glance + +| Definition | Description | +|---------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------| +| Type [`Identity`](#type-identity.) | A unique public identifier for a client. | +| Type [`Address`](#type-address.) | An opaque identifier for differentiating connections by the same `Identity`. | +| Type [`ReducerEvent`](#type-reducerevent.) | `class` containing information about the reducer that triggered a row update event. | +| Type [`module_bindings::{TABLE}`](#type-table.) | Autogenerated `class` type for a table, holding one row. | +| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](#method-filter_by_column.) | Autogenerated method to iterate over or seek subscribed rows where a column matches a value. | +| Method [`module_bindings::{TABLE}::iter`](#method-iter.) | Autogenerated method to iterate over all subscribed rows. | +| Method [`module_bindings::{TABLE}::register_row_update`](#method-register_row_update.) | Autogenerated method to register a callback that fires when a row changes. | +| Function [`module_bindings::{REDUCER_NAME}::{REDUCER_NAME}`](#function-reducer.) | Autogenerated function to invoke a reducer. | +| Function [`module_bindings::{REDUCER_NAME}::register_on_{REDUCER_NAME}`](#function-register_on_reducer.) | Autogenerated function to register a callback to run whenever the reducer is invoked. | + +### Type `Identity` + +```python +class Identity: + @staticmethod + def from_string(string) + + @staticmethod + def from_bytes(data) + + def __str__(self) + + def __eq__(self, other) +``` + +| Member | Args | Meaning | +| ------------- | ---------- | ------------------------------------ | +| `from_string` | `str` | Create an Identity from a hex string | +| `from_bytes` | `bytes` | Create an Identity from raw bytes | +| `__str__` | `None` | Convert the Identity to a hex string | +| `__eq__` | `Identity` | Compare two Identities for equality | + +A unique public identifier for a user of a database. + +### Type `Address` + +```python +class Address: + @staticmethod + def from_string(string) + + @staticmethod + def from_bytes(data) + + def __str__(self) + + def __eq__(self, other) +``` + +| Member | Type | Meaning | +|---------------|-----------|-------------------------------------| +| `from_string` | `str` | Create an Address from a hex string | +| `from_bytes` | `bytes` | Create an Address from raw bytes | +| `__str__` | `None` | Convert the Address to a hex string | +| `__eq__` | `Address` | Compare two Identities for equality | + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity.). + +### Type `ReducerEvent` + +```python +class ReducerEvent: + def __init__(self, caller_identity, reducer_name, status, message, args): + self.caller_identity = caller_identity + self.reducer_name = reducer_name + self.status = status + self.message = message + self.args = args +``` + +| Member | Type | Meaning | +|-------------------|---------------------|------------------------------------------------------------------------------------| +| `caller_identity` | `Identity` | The identity of the user who invoked the reducer | +| `caller_address` | `Optional[Address]` | The address of the user who invoked the reducer, or `None` for scheduled reducers. | +| `reducer_name` | `str` | The name of the reducer that was invoked | +| `status` | `str` | The status of the reducer invocation ("committed", "failed", "outofenergy") | +| `message` | `str` | The message returned by the reducer if it fails | +| `args` | `List[str]` | The arguments passed to the reducer | + +This class contains the information about a reducer event to be passed to row update callbacks. + +### Type `{TABLE}` + +```python +class TABLE: + is_table_class = True + + primary_key = "identity" + + @classmethod + def register_row_update(cls, callback: Callable[[str,TABLE,TABLE,ReducerEvent], None]) + + @classmethod + def iter(cls) -> Iterator[User] + + @classmethod + def filter_by_COLUMN_NAME(cls, COLUMN_VALUE) -> TABLE +``` + +This class is autogenerated for each table in your module. It contains methods for filtering and iterating over subscribed rows. + +### Method `filter_by_{COLUMN}` + +```python +def filter_by_COLUMN(self, COLUMN_VALUE) -> TABLE +``` + +| Argument | Type | Meaning | +| -------------- | ------------- | ---------------------- | +| `column_value` | `COLUMN_TYPE` | The value to filter by | + +For each column of a table, `spacetime generate` generates a `classmethod` on the [table class](#type-table.) to filter or seek 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 depends on the column's attributes: + +- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filter_by` method returns a `{TABLE}` or None, where `{TABLE}` is the [table struct](#type-table.). +- For non-unique columns, the `filter_by` method returns an `Iterator` that can be used in a `for` loop. + +### Method `iter` + +```python +def iter(self) -> Iterator[TABLE] +``` + +Iterate over all the subscribed rows in the table. + +### Method `register_row_update` + +```python +def register_row_update(self, callback: Callable[[str,TABLE,TABLE,ReducerEvent], None]) +``` + +| Argument | Type | Meaning | +| ---------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `callback` | `Callable[[str,TABLE,TABLE,ReducerEvent]` | Callback to be invoked when a row is updated (Args: row_op, old_value, new_value, reducer_event) | + +Register a callback function to be executed when a row is updated. Callback arguments are: + +- `row_op`: The type of row update event. One of `"insert"`, `"delete"`, or `"update"`. +- `old_value`: The previous value of the row, `None` if the row was inserted. +- `new_value`: The new value of the row, `None` if the row was deleted. +- `reducer_event`: The [`ReducerEvent`](#type-reducerevent.) that caused the row update, or `None` if the row was updated as a result of a subscription change. + +### Function `{REDUCER_NAME}` + +```python +def {REDUCER_NAME}(arg1, arg2) +``` + +This function is autogenerated for each reducer in your module. It is used to invoke the reducer. The arguments match the arguments defined in the reducer's `#[reducer]` attribute. + +### Function `register_on_{REDUCER_NAME}` + +```python +def register_on_{REDUCER_NAME}(callback: Callable[[Identity, Optional[Address], str, str, ARG1_TYPE, ARG1_TYPE], None]) +``` + +| Argument | Type | Meaning | +| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| `callback` | `Callable[[Identity, str, str, ARG1_TYPE, ARG1_TYPE], None]` | Callback to be invoked when the reducer is invoked (Args: caller_identity, status, message, args) | + +Register a callback function to be executed when the reducer is invoked. Callback arguments are: + +- `caller_identity`: The identity of the user who invoked the reducer. +- `caller_address`: The address of the user who invoked the reducer, or `None` for scheduled reducers. +- `status`: The status of the reducer invocation ("committed", "failed", "outofenergy"). +- `message`: The message returned by the reducer if it fails. +- `args`: Variable number of arguments passed to the reducer. + +## Async Client Reference + +### API at a glance + +| Definition | Description | +| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| Function [`SpacetimeDBAsyncClient::run`](#function-run.) | Run the client. This function will not return until the client is closed. | +| Function [`SpacetimeDBAsyncClient::subscribe`](#function-subscribe.) | Subscribe to receive data and transaction updates for the provided queries. | +| Function [`SpacetimeDBAsyncClient::register_on_subscription_applied`](#function-register_on_subscription_applied.) | Register a callback when the local cache is updated as a result of a change to the subscription queries. | +| Function [`SpacetimeDBAsyncClient::force_close`](#function-force_close.) | Signal the client to stop processing events and close the connection to the server. | +| Function [`SpacetimeDBAsyncClient::schedule_event`](#function-schedule_event.) | Schedule an event to be fired after a delay | + +### Function `run` + +```python +async def run( + self, + auth_token, + host, + address_or_name, + ssl_enabled, + on_connect, + subscription_queries=[], + ) +``` + +Run the client. This function will not return until the client is closed. + +| Argument | Type | Meaning | +| ---------------------- | --------------------------------- | -------------------------------------------------------------- | +| `auth_token` | `str` | Auth token to authenticate the user. (None if new user) | +| `host` | `str` | Hostname of SpacetimeDB server | +| `address_or_name` | `&str` | Name or address of the module. | +| `ssl_enabled` | `bool` | Whether to use SSL when connecting to the server. | +| `on_connect` | `Callable[[str, Identity], None]` | Callback to be invoked when the client connects to the server. | +| `subscription_queries` | `List[str]` | List of queries to subscribe to. | + +If `auth_token` is not None, they will be passed to the new connection to identify and authenticate the user. Otherwise, a new Identity and auth token will be generated by the server. An optional [local_config](#local_config.) module can be used to store the user's auth token to local storage. + +If you are connecting to SpacetimeDB Cloud `testnet` the host should be `testnet.spacetimedb.com` and `ssl_enabled` should be `True`. If you are connecting to SpacetimeDB Standalone locally, the host should be `localhost:3000` and `ssl_enabled` should be `False`. For instructions on how to deploy to these environments, see the [Deployment Section](testnet.) + +```python +asyncio.run( + spacetime_client.run( + AUTH_TOKEN, + "localhost:3000", + "my-module-name", + False, + on_connect, + ["SELECT * FROM User", "SELECT * FROM Message"], + ) +) +``` + +### Function `subscribe` + +```rust +def subscribe(self, queries: List[str]) +``` + +Subscribe to a set of queries, to be notified when rows which match those queries are altered. + +| Argument | Type | Meaning | +| --------- | ----------- | ---------------------------- | +| `queries` | `List[str]` | SQL queries to subscribe to. | + +The `queries` should be a slice of strings representing SQL queries. + +A new call to `subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache. Row update events will be dispatched for any inserts and deletes that occur as a result of the new queries. For these events, the [`ReducerEvent`](#type-reducerevent.) argument will be `None`. + +This should be called before the async client is started with [`run`](#function-run.). + +```python +spacetime_client.subscribe(["SELECT * FROM User;", "SELECT * FROM Message;"]) +``` + +Subscribe to a set of queries, to be notified when rows which match those queries are altered. + +### Function `register_on_subscription_applied` + +```python +def register_on_subscription_applied(self, callback) +``` + +Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. + +| Argument | Type | Meaning | +| ---------- | -------------------- | ------------------------------------------------------ | +| `callback` | `Callable[[], None]` | Callback to be invoked when subscriptions are applied. | + +The callback will be invoked after a successful [`subscribe`](#function-subscribe.) call when the initial set of matching rows becomes available. + +```python +spacetime_client.register_on_subscription_applied(on_subscription_applied) +``` + +### Function `force_close` + +```python +def force_close(self) +) +``` + +Signal the client to stop processing events and close the connection to the server. + +```python +spacetime_client.force_close() +``` + +### Function `schedule_event` + +```python +def schedule_event(self, delay_secs, callback, *args) +``` + +Schedule an event to be fired after a delay + +To create a repeating event, call schedule_event() again from within the callback function. + +| Argument | Type | Meaning | +| ------------ | -------------------- | -------------------------------------------------------------- | +| `delay_secs` | `float` | number of seconds to wait before firing the event | +| `callback` | `Callable[[], None]` | Callback to be invoked when the event fires. | +| `args` | `*args` | Variable number of arguments to pass to the callback function. | + +```python +def application_tick(): + # ... do some work + + spacetime_client.schedule_event(0.1, application_tick) + +spacetime_client.schedule_event(0.1, application_tick) +``` + +## Basic Client Reference + +### API at a glance + +| Definition | Description | +|------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| Function [`SpacetimeDBClient::init`](#function-init.) | Create a network manager instance. | +| Function [`SpacetimeDBClient::subscribe`](#function-subscribe.) | Subscribe to receive data and transaction updates for the provided queries. | +| Function [`SpacetimeDBClient::register_on_event`](#function-register_on_event.) | Register a callback function to handle transaction update events. | +| Function [`SpacetimeDBClient::unregister_on_event`](#function-unregister_on_event.) | Unregister a callback function that was previously registered using `register_on_event`. | +| Function [`SpacetimeDBClient::register_on_subscription_applied`](#function-register_on_subscription_applied.) | Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. | +| Function [`SpacetimeDBClient::unregister_on_subscription_applied`](#function-unregister_on_subscription_applied.) | Unregister a callback function from the subscription update event. | +| Function [`SpacetimeDBClient::update`](#function-update.) | Process all pending incoming messages from the SpacetimeDB module. | +| Function [`SpacetimeDBClient::close`](#function-close.) | Close the WebSocket connection. | +| Type [`TransactionUpdateMessage`](#type-transactionupdatemessage.) | Represents a transaction update message. | + +### Function `init` + +```python +@classmethod +def init( + auth_token: str, + host: str, + address_or_name: str, + ssl_enabled: bool, + autogen_package: module, + on_connect: Callable[[], NoneType] = None, + on_disconnect: Callable[[str], NoneType] = None, + on_identity: Callable[[str, Identity, Address], NoneType] = None, + on_error: Callable[[str], NoneType] = None +) +``` + +Create a network manager instance. + +| Argument | Type | Meaning | +|-------------------|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `auth_token` | `str` | This is the token generated by SpacetimeDB that matches the user's identity. If None, token will be generated | +| `host` | `str` | Hostname:port for SpacetimeDB connection | +| `address_or_name` | `str` | The name or address of the database to connect to | +| `ssl_enabled` | `bool` | Whether to use SSL when connecting to the server. | +| `autogen_package` | `ModuleType` | Python package where SpacetimeDB module generated files are located. | +| `on_connect` | `Callable[[], None]` | Optional callback called when a connection is made to the SpacetimeDB module. | +| `on_disconnect` | `Callable[[str], None]` | Optional callback called when the Python client is disconnected from the SpacetimeDB module. The argument is the close message. | +| `on_identity` | `Callable[[str, Identity, Address], None]` | Called when the user identity is recieved from SpacetimeDB. First argument is the auth token used to login in future sessions. Third argument is the client connection's [`Address`](#type-address.). | +| `on_error` | `Callable[[str], None]` | Optional callback called when the Python client connection encounters an error. The argument is the error message. | + +This function creates a new SpacetimeDBClient instance. It should be called before any other functions in the SpacetimeDBClient class. This init will call connect for you. + +```python +SpacetimeDBClient.init(autogen, on_connect=self.on_connect) +``` + +### Function `subscribe` + +```python +def subscribe(queries: List[str]) +``` + +Subscribe to receive data and transaction updates for the provided queries. + +| Argument | Type | Meaning | +| --------- | ----------- | -------------------------------------------------------------------------------------------------------- | +| `queries` | `List[str]` | A list of queries to subscribe to. Each query is a string representing an sql formatted query statement. | + +This function sends a subscription request to the SpacetimeDB module, indicating that the client wants to receive data and transaction updates related to the specified queries. + +```python +queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] +SpacetimeDBClient.instance.subscribe(queries) +``` + +### Function `register_on_event` + +```python +def register_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) +``` + +Register a callback function to handle transaction update events. + +| Argument | Type | Meaning | +| ---------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `callback` | `Callable[[TransactionUpdateMessage], None]` | A callback function that takes a single argument of type `TransactionUpdateMessage`. This function will be invoked with a `TransactionUpdateMessage` instance containing information about the transaction update event. | + +This function registers a callback function that will be called when a reducer modifies a table matching any of the subscribed queries or if a reducer called by this Python client encounters a failure. + +```python +def handle_event(transaction_update): + # Code to handle the transaction update event + +SpacetimeDBClient.instance.register_on_event(handle_event) +``` + +### Function `unregister_on_event` + +```python +def unregister_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) +``` + +Unregister a callback function that was previously registered using `register_on_event`. + +| Argument | Type | Meaning | +| ---------- | -------------------------------------------- | ------------------------------------ | +| `callback` | `Callable[[TransactionUpdateMessage], None]` | The callback function to unregister. | + +```python +SpacetimeDBClient.instance.unregister_on_event(handle_event) +``` + +### Function `register_on_subscription_applied` + +```python +def register_on_subscription_applied(callback: Callable[[], NoneType]) +``` + +Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. + +| Argument | Type | Meaning | +| ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `callback` | `Callable[[], None]` | A callback function that will be invoked on each subscription update. The callback function should not accept any arguments and should not return any value. | + +```python +def subscription_callback(): + # Code to be executed on each subscription update + +SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) +``` + +### Function `unregister_on_subscription_applied` + +```python +def unregister_on_subscription_applied(callback: Callable[[], NoneType]) +``` + +Unregister a callback function from the subscription update event. + +| Argument | Type | Meaning | +| ---------- | -------------------- | -------------------------------------------------------------------------------------------------------- | +| `callback` | `Callable[[], None]` | A callback function that was previously registered with the `register_on_subscription_applied` function. | + +```python +def subscription_callback(): + # Code to be executed on each subscription update + +SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) +``` + +### Function `update` + +```python +def update() +``` + +Process all pending incoming messages from the SpacetimeDB module. + +This function must be called on a regular interval in the main loop to process incoming messages. + +```python +while True: + SpacetimeDBClient.instance.update() # Call the update function in a loop to process incoming messages + # Additional logic or code can be added here +``` + +### Function `close` + +```python +def close() +``` + +Close the WebSocket connection. + +This function closes the WebSocket connection to the SpacetimeDB module. + +```python +SpacetimeDBClient.instance.close() +``` + +### Type `TransactionUpdateMessage` + +```python +class TransactionUpdateMessage: + def __init__( + self, + caller_identity: Identity, + status: str, + message: str, + reducer_name: str, + args: Dict + ) +``` + +| Member | Args | Meaning | +| ----------------- | ---------- | ------------------------------------------------- | +| `caller_identity` | `Identity` | The identity of the caller. | +| `status` | `str` | The status of the transaction. | +| `message` | `str` | A message associated with the transaction update. | +| `reducer_name` | `str` | The reducer used for the transaction. | +| `args` | `Dict` | Additional arguments for the transaction. | + +Represents a transaction update message. Used in on_event callbacks. + +For more details, see [`register_on_event`](#function-register_on_event.). diff --git a/Writerside2/topics/sdks/python/python_quickstart.md b/Writerside2/topics/sdks/python/python_quickstart.md new file mode 100644 index 00000000..fe6dbc22 --- /dev/null +++ b/Writerside2/topics/sdks/python/python_quickstart.md @@ -0,0 +1,379 @@ +# Python Client SDK Quick Start + +In this guide, we'll show you how to get up and running with a simple SpacetimDB app with a client written in Python. + +We'll implement a command-line client for the module created in our [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart6.) guides. Make sure you follow one of these guides before you start on this one. + +## Install the SpacetimeDB SDK Python Package + +1. Run pip install + +```bash +pip install spacetimedb_sdk +``` + +## Project structure + +Enter the directory `quickstart-chat` you created in the Rust or C# Module Quickstart guides and create a `client` folder: + +```bash +cd quickstart-chat +mkdir client +``` + +## Create the Python main file + +Create a file called `main.py` in the `client` and open it in your favorite editor. We prefer [VS Code](https://code.visualstudio.com/). + +## Add imports + +We need to add several imports for this quickstart: + +- [`asyncio`](https://docs.python.org/3/library/asyncio.html) is required to run the async code in the SDK. +- [`multiprocessing.Queue`](https://docs.python.org/3/library/multiprocessing.html) allows us to pass our input to the async code, which we will run in a separate thread. +- [`threading`](https://docs.python.org/3/library/threading.html) allows us to spawn our async code in a separate thread so the main thread can run the input loop. + +- `spacetimedb_sdk.spacetimedb_async_client.SpacetimeDBAsyncClient` is the async wrapper around the SpacetimeDB client which we use to interact with our SpacetimeDB module. +- `spacetimedb_sdk.local_config` is an optional helper module to load the auth token from local storage. + +```python +import asyncio +from multiprocessing import Queue +import threading + +from spacetimedb_sdk.spacetimedb_async_client import SpacetimeDBAsyncClient +import spacetimedb_sdk.local_config as local_config +``` + +## Generate your module types + +The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. + +In your `client` directory, run: + +```bash +mkdir -p module_bindings +spacetime generate --lang python --out-dir module_bindings --project-path ../server +``` + +Take a look inside `client/module_bindings`. The CLI should have generated five files: + +``` +module_bindings ++-- message.py ++-- send_message_reducer.py ++-- set_name_reducer.py ++-- user.py +``` + +Now we import these types by adding the following lines to `main.py`: + +```python +import module_bindings +from module_bindings.user import User +from module_bindings.message import Message +import module_bindings.send_message_reducer as send_message_reducer +import module_bindings.set_name_reducer as set_name_reducer +``` + +## Global variables + +Next we will add our global `input_queue` and `local_identity` variables which we will explain later when they are used. + +```python +input_queue = Queue() +local_identity = None +``` + +## Define 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 four things: + +1. Init the optional local config module. The first parameter is the directory name to be created in the user home directory. +1. Create our async SpacetimeDB client. +1. Register our callbacks. +1. Start the async client in a thread. +1. Run a loop to read user input and send it to a repeating event in the async client. +1. When the user exits, stop the async client and exit the program. + +```python +if __name__ == "__main__": + local_config.init(".spacetimedb-python-quickstart") + + spacetime_client = SpacetimeDBAsyncClient(module_bindings) + + register_callbacks(spacetime_client) + + thread = threading.Thread(target=run_client, args=(spacetime_client,)) + thread.start() + + input_loop() + + spacetime_client.force_close() + thread.join() +``` + +## Register callbacks + +We need to handle several sorts of events: + +1. OnSubscriptionApplied is a special callback that is executed when the local client cache is populated. We will talk more about this later. +2. When a new user joins or a user is updated, we'll print an appropriate message. +3. When we receive a new message, we'll print it. +4. If the server rejects our attempt to set our name, we'll print an error. +5. If the server rejects a message we send, we'll print an error. +6. We use the `schedule_event` function to register a callback to be executed after 100ms. This callback will check the input queue for any user input and execute the appropriate command. + +Because python requires functions to be defined before they're used, the following code must be added to `main.py` before main block: + +```python +def register_callbacks(spacetime_client): + spacetime_client.client.register_on_subscription_applied(on_subscription_applied) + + User.register_row_update(on_user_row_update) + Message.register_row_update(on_message_row_update) + + set_name_reducer.register_on_set_name(on_set_name_reducer) + send_message_reducer.register_on_send_message(on_send_message_reducer) + + spacetime_client.schedule_event(0.1, check_commands) +``` + +### Handling User row updates + +For each table, we can register a row update callback to be run whenever a subscribed row is inserted, updated or deleted. We register these callbacks using the `register_row_update` methods that are generated automatically 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::row_update` callbacks may be invoked with users who are offline. We'll only notify about online users. + +We are also going to check for updates to the user row. This can happen for three reasons: + +1. They've set their name using the `set_name` reducer. +2. They're an existing user re-connecting, so their `online` has been set to `true`. +3. They've disconnected, so their `online` has been set to `false`. + +We'll print an appropriate message in each of these cases. + +`row_update` callbacks take four arguments: the row operation ("insert", "update", or "delete"), the old row if it existed, the new or updated row, and a `ReducerEvent`. This will `None` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an class that contains information about the reducer that triggered this row update event. + +Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `user_name_or_identity` handle this. + +Add these functions before the `register_callbacks` function: + +```python +def user_name_or_identity(user): + if user.name: + return user.name + else: + return (str(user.identity))[:8] + +def on_user_row_update(row_op, user_old, user, reducer_event): + if row_op == "insert": + if user.online: + print(f"User {user_name_or_identity(user)} connected.") + elif row_op == "update": + if user_old.online and not user.online: + print(f"User {user_name_or_identity(user)} disconnected.") + elif not user_old.online and user.online: + print(f"User {user_name_or_identity(user)} connected.") + + if user_old.name != user.name: + print( + f"User {user_name_or_identity(user_old)} renamed to {user_name_or_identity(user)}." + ) +``` + +### 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 `on_message_row_update` callback will check if its `reducer_event` argument is not `None`, and only print in that case. + +To find the `User` based on the message's `sender` identity, we'll use `User::filter_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filter_by_identity` accepts a `bytes`, rather than an `&Identity`. The `sender` identity stored in the message is also a `bytes`, not an `Identity`, so we can just pass it to the filter method. + +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`. + +Add these functions before the `register_callbacks` function: + +```python +def on_message_row_update(row_op, message_old, message, reducer_event): + if reducer_event is not None and row_op == "insert": + print_message(message) + +def print_message(message): + user = User.filter_by_identity(message.sender) + user_name = "unknown" + if user is not None: + user_name = user_name_or_identity(user) + + print(f"{user_name}: {message.text}") +``` + +### Warn if our name was rejected + +We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `register_on_` method, which is automatically implemented for each reducer by `spacetime generate`. + +Each reducer callback takes four fixed arguments: + +1. The `Identity` of the client who requested the reducer invocation. +2. The `Address` of the client who requested the reducer invocation, or `None` for scheduled reducers. +3. The `Status` of the reducer run, one of `committed`, `failed` or `outofenergy`. +4. The `Message` returned by the reducer in error cases, or `None` if the reducer succeeded. + +It also takes a variable number of arguments which match the calling arguments of the reducer. + +These callbacks will be invoked in one of two cases: + +1. If the reducer was successful and altered any of our subscribed rows. +2. If we requested an invocation which failed. + +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 `on_set_name_reducer` as a callback which checks if the reducer failed, and if it did, prints an error message including the rejected name. + +We'll test both that our identity matches the sender and that the status is `failed`, even though the latter implies the former, for demonstration purposes. + +Add this function before the `register_callbacks` function: + +```python +def on_set_name_reducer(sender_id, sender_address, status, message, name): + if sender_id == local_identity: + if status == "failed": + print(f"Failed to set name: {message}") +``` + +### 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. + +Add this function before the `register_callbacks` function: + +```python +def on_send_message_reducer(sender_id, sender_address, status, message, msg): + if sender_id == local_identity: + if status == "failed": + print(f"Failed to send message: {message}") +``` + +### OnSubscriptionApplied callback + +This callback fires after the client cache is updated as a result in a change to the client subscription. This happens after connect and if after calling `subscribe` to modify the subscription. + +In this case, we want to print all the existing messages when the subscription is applied. `print_messages_in_order` iterates over all the `Message`s we've received, sorts them, and then prints them. `Message.iter()` is generated for all table types, and returns an iterator over all the messages in the client's cache. + +Add these functions before the `register_callbacks` function: + +```python +def print_messages_in_order(): + all_messages = sorted(Message.iter(), key=lambda x: x.sent) + for entry in all_messages: + print(f"{user_name_or_identity(User.filter_by_identity(entry.sender))}: {entry.text}") + +def on_subscription_applied(): + print(f"\nSYSTEM: Connected.") + print_messages_in_order() +``` + +### Check commands repeating event + +We'll use a repeating event to check the user input queue every 100ms. If there's a command in the queue, we'll execute it. If not, we'll just keep waiting. Notice that at the end of the function we call `schedule_event` again to so the event will repeat. + +If the command is to send a message, we'll call the `send_message` reducer. If the command is to set our name, we'll call the `set_name` reducer. + +Add these functions before the `register_callbacks` function: + +```python +def check_commands(): + global input_queue + + if not input_queue.empty(): + choice = input_queue.get() + if choice[0] == "name": + set_name_reducer.set_name(choice[1]) + else: + send_message_reducer.send_message(choice[1]) + + spacetime_client.schedule_event(0.1, check_commands) +``` + +### OnConnect callback + +This callback fires after the client connects to the server. We'll use it to save our credentials to a file so that we can re-authenticate as the same user next time we connect. + +The `on_connect` callback takes three arguments: + +1. The `Auth Token` is the equivalent of your private key. This is the only way to authenticate with the SpacetimeDB module as this user. +2. The `Identity` is the equivalent of your public key. This is used to uniquely identify this user and will be sent to other clients. We store this in a global variable so we can use it to identify that a given message or transaction was sent by us. +3. The `Address` is an opaque identifier 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. + +To store our auth token, we use the optional component `local_config`, which provides a simple interface for storing and retrieving a single `Identity` from a file. We'll use the `local_config::set_string` method to store the auth token. Other projects might want to associate this token with some other identifier such as an email address or Steam ID. + +The `on_connect` callback is passed to the client connect function so it just needs to be defined before the `run_client` described next. + +```python +def on_connect(auth_token, identity): + global local_identity + local_identity = identity + + local_config.set_string("auth_token", auth_token) +``` + +## Async client thread + +We are going to write a function that starts the async client, which will be executed on a separate thread. + +```python +def run_client(spacetime_client): + asyncio.run( + spacetime_client.run( + local_config.get_string("auth_token"), + "localhost:3000", + "chat", + False, + on_connect, + ["SELECT * FROM User", "SELECT * FROM Message"], + ) + ) +``` + +## Input loop + +Finally, we need a function to be executed on the main loop which listens for user input and adds it to the queue. + +```python +def input_loop(): + global input_queue + + while True: + user_input = input() + if len(user_input) == 0: + return + elif user_input.startswith("/name "): + input_queue.put(("name", user_input[6:])) + else: + input_queue.put(("message", user_input)) +``` + +## Run the client + +Make sure your module from the Rust or C# module quickstart is published. If you used a different module name than `chat`, you will need to update the `connect` call in the `run_client` function. + +Run the client: + +```bash +python main.py +``` + +If you want to connect another client, you can use the --client command line option, which is built into the local_config module. This will create different settings file for the new client's auth token. + +```bash +python main.py --client 2 +``` + +## Next steps + +Congratulations! You've built a simple chat app with a Python client. You can now use this as a starting point for your own SpacetimeDB apps. + +For a more complex example of the Spacetime Python SDK, check out our [AI Agent](https://github.com/clockworklabs/spacetime-mud/tree/main/ai-agent-python-client) for the [Spacetime Multi-User Dungeon](https://github.com/clockworklabs/spacetime-mud). The AI Agent uses the OpenAI API to create dynamic content on command. diff --git a/Writerside2/topics/sdks/rust/sdks_rust_index.md b/Writerside2/topics/sdks/rust/sdks_rust_index.md new file mode 100644 index 00000000..239cabff --- /dev/null +++ b/Writerside2/topics/sdks/rust/sdks_rust_index.md @@ -0,0 +1,1183 @@ +# The SpacetimeDB Rust client SDK + +The SpacetimeDB client SDK for Rust contains all the tools you need to build native clients for SpacetimeDB modules using Rust. + +## Install the SDK + +First, create a new project using `cargo new` and add the SpacetimeDB SDK to your dependencies: + +```bash +cargo add spacetimedb +``` + +## Generate module bindings + +Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's `src` directory and generate the Rust interface files using the Spacetime CLI. From your project directory, run: + +```bash +mkdir -p src/module_bindings +spacetime generate --lang rust \ + --out-dir src/module_bindings \ + --project-path PATH-TO-MODULE-DIRECTORY +``` + +Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. + +Declare a `mod` for the bindings in your client's `src/main.rs`: + +```rust +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`](rust_#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`](rust_#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`](rust_#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`](rust_#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}`](rust_#type-table.) | Autogenerated `struct` type for a table, holding one row. | +| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](rust_#method-filter_by_column.) | Autogenerated method to iterate over or seek subscribed rows where a 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`](rust_#method-iter.) | Iterate over all subscribed rows. | +| Method [`spacetimedb_sdk::table::TableType::filter`](rust_#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`](rust_#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}`](rust_#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` + +```rust +module_bindings::connect( + spacetimedb_uri: impl TryInto, + db_name: &str, + credentials: Option, +) -> anyhow::Result<()> +``` + +Connect to a database named `db_name` accessible over the internet at the URI `spacetimedb_uri`. + +| 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` + +```rust +spacetimedb_sdk::disconnect() +``` + +Gracefully close the current WebSocket connection. + +If there is no active connection, this operation does nothing. + +```rust +connect(SPACETIMEDB_URI, MODULE_NAME, credentials) + .expect("Connection failed"); + +run_app(); + +disconnect(); +``` + +### 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!" +``` + +### Function `once_on_disconnect` + +```rust +spacetimedb_sdk::once_on_disconnect( + callback: impl FnOnce() + Send + 'static, +) -> DisconnectCallbackId +``` + +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. + +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. + +```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. +``` + +### Function `remove_on_disconnect` + +```rust +spacetimedb_sdk::remove_on_disconnect( + id: DisconnectCallbackId, +) +``` + +Unregister a previously-registered [`on_disconnect`](#function-on_disconnect.) callback. + +| Argument | Type | Meaning | +| -------- | ---------------------- | ------------------------------------------ | +| `id` | `DisconnectCallbackId` | 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_disconnect(|| unreachable!()); + +remove_on_disconnect(id); + +disconnect(); + +// No `unreachable` panic. +``` + +## Subscribe to queries + +### Function `subscribe` + +```rust +spacetimedb_sdk::subscribe(queries: &[&str]) -> anyhow::Result<()> +``` + +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}`](rust_#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. + +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. + +```rust +subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) + .expect("Called `subscribe` before `connect`"); +``` + +### Function `subscribe_owned` + +```rust +spacetimedb_sdk::subscribe_owned(queries: Vec) -> 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. + +A new call to `subscribe_owned` (or [`subscribe`](rust_#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. + +`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. + +```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` + +```rust +spacetimedb_sdk::on_subscription_applied( + callback: impl FnMut() + Send + 'static, +) -> SubscriptionCallbackId +``` + +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`](rust_#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. +``` + +### Function `once_on_subscription_applied` + +```rust +spacetimedb_sdk::once_on_subscription_applied( + callback: impl FnOnce() + Send + 'static, +) -> SubscriptionCallbackId +``` + +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`](rust_#function-subscribe.) or [`subscribe_owned`](#function-subscribe_owned.) call when the initial set of matching rows becomes available. + +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. + +```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. +``` + +### 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)); + +// Will print "Subscription applied!" + +remove_on_subscription_applied(id); + +subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) + .expect("Called `subscribe` before `connect`"); + +// Nothing printed this time. +``` + +## Identify a client + +### Type `Identity` + +```rust +spacetimedb_sdk::identity::Identity +``` + +A unique public identifier for a client connected to a database. + +### Type `Token` + +```rust +spacetimedb_sdk::identity::Token +``` + +A private access token for a client connected to a database. + +### Type `Credentials` + +```rust +spacetimedb_sdk::identity::Credentials +``` + +Credentials, including a private access token, sufficient to authenticate a client connected to a database. + +| Field | Type | +| ---------- | ---------------------------- | +| `identity` | [`Identity`](rust_#type-identity.) | +| `token` | [`Token`](#type-token.) | + +### Type `Address` + +```rust +spacetimedb_sdk::Address +``` + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](rust_#type-identity.). + +### Function `identity` + +```rust +spacetimedb_sdk::identity::identity() -> Result +``` + +Read the current connection's public [`Identity`](rust_#type-identity.). + +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 identity is {:?}", identity()); + +// Prints "My identity is Ok(Identity { bytes: [...several u8s...] })" +``` + +### Function `token` + +```rust +spacetimedb_sdk::identity::token() -> Result +``` + +Read the current connection's private [`Token`](#type-token.). + +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` + +```rust +spacetimedb_sdk::identity::credentials() -> Result +``` + +Read the current connection's [`Credentials`](#type-credentials.), including a public [`Identity`](rust_#type-identity.) and a private [`Token`](#type-token.). + +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 credentials are {:?}", credentials()); + +// Prints "My credentials are Ok(Credentials { +// identity: Identity { bytes: [...several u8s...] }, +// token: Token { string: "...several Base64 digits..."}, +// })" +``` + +### Function `address` + +```rust +spacetimedb_sdk::identity::address() -> Result
+``` + +Read the current connection's [`Address`](rust_#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()); +``` + +### Function `on_connect` + +```rust +spacetimedb_sdk::identity::on_connect( + callback: impl FnMut(&Credentials, Address) + Send + 'static, +) -> ConnectCallbackId +``` + +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`](rust_#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. + +The returned `ConnectCallbackId` can be passed to [`remove_on_connect`](#function-remove_on_connect.) to unregister the callback. + +```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`. +``` + +### Function `once_on_connect` + +```rust +spacetimedb_sdk::identity::once_on_connect( + callback: impl FnOnce(&Credentials, Address) + Send + 'static, +) -> ConnectCallbackId +``` + +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`](rust_#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. + +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` + +```rust +spacetimedb_sdk::identity::remove_on_connect(id: ConnectCallbackId) +``` + +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. | + +If `id` does not refer to a currently-registered callback, this operation does nothing. + +```rust +let id = on_connect(|_creds, _addr| unreachable!()); + +remove_on_connect(id); + +connect(SPACETIMEDB_URI, DB_NAME, None) + .expect("Failed to connect"); + +sleep(Duration::from_secs(1)); + +// No `unreachable` panic. +``` + +### Function `load_credentials` + +```rust +spacetimedb_sdk::identity::load_credentials( + dirname: &str, +) -> Result> +``` + +Load a saved [`Credentials`](#type-credentials.) from a file within `~/dirname`, if one exists. + +| Argument | Type | Meaning | +| --------- | ------ | ----------------------------------------------------- | +| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | + +`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. + +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.). + +```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"); +``` + +### Function `save_credentials` + +```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.). + +| Argument | Type | Meaning | +| ------------- | -------------- | ----------------------------------------------------- | +| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | +| `credentials` | `&Credentials` | [`Credentials`](#type-credentials.) to store. | + +`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. + +Returns `Err` when IO or serialization fails. + +```rust +const CREDENTIALS_DIR = ".my-module"; + +let creds = load_credentials(CREDENTIALS_DIRectory) + .expect("Error while loading credentials"); + +on_connect(|creds, _addr| { + if let Err(e) = save_credentials(CREDENTIALS_DIR, creds) { + eprintln!("Error while saving credentials: {:?}", e); + } +}); + +connect(SPACETIMEDB_URI, DB_NAME, creds) + .expect("Failed to connect"); +``` + +## View subscribed rows of tables + +### Type `{TABLE}` + +```rust +module_bindings::{TABLE} +``` + +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}` + +```rust +module_bindings::{TABLE}::filter_by_{COLUMN}( + value: {COLUMN_TYPE}, +) -> {FILTER_RESULT}<{TABLE}> +``` + +For each column of a table, `spacetime generate` generates a static method on the [table struct](rust_#type-table.) to filter or seek 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 depends on the column's attributes: + +- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filter_by` method returns an `Option<{TABLE}>`, where `{TABLE}` is the [table struct](rust_#type-table.). +- For non-unique columns, the `filter_by` method returns an `impl Iterator`. + +### Trait `TableType` + +```rust +spacetimedb_sdk::table::TableType +``` + +Every [generated table struct](rust_#type-table.) implements the trait `TableType`. + +#### Method `count` + +```rust +TableType::count() -> usize +``` + +Return the number of subscribed rows in the table, or 0 if there is no active connection. + +This method acquires a global lock. + +```rust +connect(SPACETIMEDB_URI, DB_NAME, None) + .expect("Failed to connect"); + +on_subscription_applied(|| println!("There are {} users", User::count())); + +subscribe(&["SELECT * FROM User;"]) + .unwrap(); + +sleep(Duration::from_secs(1)); + +// Will the number of `User` rows in the database. +``` + +#### Method `iter` + +```rust +TableType::iter() -> impl Iterator +``` + +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`](rust_#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. + +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](rust_#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` + +```rust +TableType::find( + predicate: impl FnMut(&Self) -> bool, +) -> Option +``` + +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 `filter_by_{COLUMN}` methods](rust_#method-filter_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. +``` + +#### Method `on_insert` + +```rust +TableType::on_insert( + callback: impl FnMut(&Self, Option<&ReducerEvent>) + Send + 'static, +) -> InsertCallbackId +``` + +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`](rust_#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(); + +sleep(Duration::from_secs(1)); + +// Will print a note whenever a new `User` row is inserted. +``` + +#### Method `remove_on_insert` + +```rust +TableType::remove_on_insert(id: InsertCallbackId) +``` + +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. | + +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_insert(|_, _| unreachable!()); + +User::remove_on_insert(id); + +subscribe(&["SELECT * FROM User;"]) + .unwrap(); + +sleep(Duration::from_secs(1)); + +// No `unreachable` panic. +``` + +#### Method `on_delete` + +```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: + +- `row: &Self`, the previously-present row which is no longer resident in the database. +- `reducer_event: Option<&ReducerEvent>`, the [`ReducerEvent`](rust_#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. + +```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)); + +// 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" }" +``` + +#### 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. +``` + +### Trait `TableWithPrimaryKey` + +```rust +spacetimedb_sdk::table::TableWithPrimaryKey +``` + +[Generated table structs](rust_#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`](rust_#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) +``` + +| 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. + +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_update(|_, _, _| unreachable!); + +User::remove_on_update(id); + +subscribe(&["SELECT * FROM User;"]) + .unwrap(); + +// No `unreachable` panic. +``` + +## Observe and request reducer invocations + +### Type `ReducerEvent` + +```rust +module_bindings::ReducerEvent +``` + +`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.). + +[`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. + +### Type `{REDUCER}Args` + +```rust +module_bindings::{REDUCER}Args +``` + +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`. + +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...}) +``` + +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`](rust_#type-identity.) of the client which invoked the reducer. +- `caller_address: Option
`, the [`Address`](rust_#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. + +### Function `remove_on_{REDUCER}` + +```rust +module_bindings::remove_on_{REDUCER}(id: ReducerCallbackId<{REDUCER}Args>) +``` + +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. + +### Type `Status` + +```rust +spacetimedb_sdk::reducer::Status +``` + +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. diff --git a/Writerside2/topics/sdks/rust/sdks_rust_quickstart.md b/Writerside2/topics/sdks/rust/sdks_rust_quickstart.md new file mode 100644 index 00000000..f6049bf5 --- /dev/null +++ b/Writerside2/topics/sdks/rust/sdks_rust_quickstart.md @@ -0,0 +1,487 @@ +# Rust Client SDK Quick Start + +In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in Rust. + +We'll implement a command-line client for the module created in our Rust or C# Module Quickstart guides. Make sure you follow one of these guides before you start on this one. + +## Project structure + +Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: + +```bash +cd quickstart-chat +``` + +Within it, create a `client` crate, our client application, which users run locally: + +```bash +cargo new client +``` + +## Depend on `spacetimedb-sdk` and `hex` + +`client/Cargo.toml` should be initialized without any dependencies. We'll need two: + +- [`spacetimedb-sdk`](https://crates.io/crates/spacetimedb-sdk), which defines client-side interfaces for interacting with a remote SpacetimeDB module. +- [`hex`](https://crates.io/crates/hex), which we'll use to print unnamed users' identities as hexadecimal strings. + +Below the `[dependencies]` line in `client/Cargo.toml`, add: + +```toml +spacetimedb-sdk = "0.7" +hex = "0.4" +``` + +Make sure you depend on the same version of `spacetimedb-sdk` as is reported by the SpacetimeDB CLI tool's `spacetime version`! + +## Clear `client/src/main.rs` + +`client/src/main.rs` should be initialized with a trivial "Hello world" program. Clear it out so we can write our chat client. + +In your `quickstart-chat` directory, run: + +```bash +rm client/src/main.rs +touch client/src/main.rs +``` + +## Generate your module types + +The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. + +In your `quickstart-chat` directory, run: + +```bash +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: + +``` +module_bindings +├── message.rs +├── mod.rs +├── send_message_reducer.rs +├── set_name_reducer.rs +└── user.rs +``` + +We need to declare the module in our client crate, and we'll want to import its definitions. + +To `client/src/main.rs`, add: + +```rust +mod module_bindings; +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. + +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}, +}; +``` + +## Define 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: + +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: + +```rust +fn main() { + register_callbacks(); + connect_to_db(); + subscribe_to_tables(); + user_input_loop(); +} +``` + +## Register callbacks + +We need to handle several sorts of events: + +1. When we connect and receive our credentials, we'll save them to a file so that the next time we connect, we can re-authenticate as the same user. +2. When a new user joins, we'll print a message introducing them. +3. When a user is updated, we'll print their new name, or declare their new online status. +4. When we receive a new message, we'll print it. +5. When we're informed of the backlog of past messages, we'll sort them and print them in order. +6. If the server rejects our attempt to set our name, we'll print an error. +7. If the server rejects a message we send, we'll print an error. +8. When our connection ends, we'll print a note, then exit the process. + +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); + + // When a new user joins, print a notification. + User::on_insert(on_user_inserted); + + // When a user's status changes, print a notification. + User::on_update(on_user_updated); + + // When a new message is received, print it. + Message::on_insert(on_message_inserted); + + // When we receive the message backlog, print it in timestamp order. + on_subscription_applied(on_sub_applied); + + // When we fail to set our name, print a warning. + 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); +} +``` + +### 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. + +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) { + eprintln!("Failed to save credentials: {:?}", e); + } +} + +const CREDS_DIR: &str = ".spacetime_chat"; +``` + +### 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`. + +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. + +`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. + +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. + +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>) { + if user.online { + println!("User {} connected.", user_name_or_identity(user)); + } +} + +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]) +} +``` + +### 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. + +`on_update` callbacks take three arguments: the old row, the new row, and an `Option<&ReducerEvent>`. + +In our module, users can be updated for three reasons: + +1. They've set their name using the `set_name` reducer. +2. They're an existing user re-connecting, so their `online` has been set to `true`. +3. They've disconnected, so their `online` has been set to `false`. + +We'll print an appropriate message in each of these cases. + +To `client/src/main.rs`, add: + +```rust +/// Our `User::on_update` callback: +/// print a notification about name and status changes. +fn on_user_updated(old: &User, new: &User, _: Option<&ReducerEvent>) { + if old.name != new.name { + println!( + "User {} renamed to {}.", + user_name_or_identity(old), + user_name_or_identity(new) + ); + } + if old.online && !new.online { + println!("User {} disconnected.", user_name_or_identity(new)); + } + if !old.online && new.online { + println!("User {} connected.", user_name_or_identity(new)); + } +} +``` + +### 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. + +To find the `User` based on the message's `sender` identity, we'll use `User::filter_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filter_by_identity` accepts an owned `Identity`, rather than a reference. We can `clone` the identity held in `message.sender`. + +We'll print the user's name or identity in the same way as we did when notifying about `User` table events, but here we have to handle the case where we don't find a matching `User` row. This can happen when the module owner sends a message using the CLI's `spacetime call`. In this case, we'll print `unknown`. + +To `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 print_message(message: &Message) { + let sender = User::filter_by_identity(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: + +```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::>(); + messages.sort_by_key(|m| m.sent); + for message in messages { + print_message(&message); + } +} +``` + +### Warn if our name was rejected + +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. + +These callbacks will be invoked in one of two cases: + +1. If the reducer was successful and altered any of our subscribed rows. +2. If we requested an invocation which failed. + +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. + +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); + } +} +``` + +### 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); + } +} +``` + +### 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 +/// The URL of the SpacetimeDB instance hosting our chat module. +const SPACETIMEDB_URI: &str = "http://localhost:3000"; + +/// The module name we chose when we published our module. +const DB_NAME: &str = ""; + +/// 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"); +} +``` + +## Subscribe to queries + +SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. + +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(); +} +``` + +## 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. + +`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`. + +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() { + 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()); + } else { + send_message(line); + } + } +} +``` + +## Run it + +Change your directory to the client app, then compile and run it. From the `quickstart-chat` directory, run: + +```bash +cd client +cargo run +``` + +You should see something like: + +``` +User d9e25c51996dea2f connected. +``` + +Now try sending a message. Type `Hello, world!` and press enter. You should see something like: + +``` +d9e25c51996dea2f: Hello, world! +``` + +Next, set your name. Type `/name `, replacing `` with your name. You should see something like: + +``` +User d9e25c51996dea2f renamed to . +``` + +Then send another message. Type `Hello after naming myself.` and press enter. You should see: + +``` +: 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: + +``` +User connected. +: Hello, world! +: Hello after naming myself. +``` + +## What's next? + +You can find the full code for this client [in the Rust SDK's examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/sdk/examples/quickstart-chat). + +Check out the [Rust SDK Reference](rust1.) 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. + +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). + +Or, you could extend the module and the client together, perhaps: + +- 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. diff --git a/Writerside2/topics/sdks/sdks_index.md b/Writerside2/topics/sdks/sdks_index.md new file mode 100644 index 00000000..bcc59bfd --- /dev/null +++ b/Writerside2/topics/sdks/sdks_index.md @@ -0,0 +1,74 @@ + 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 + +- [Rust](rust1.) - [(Quickstart)](quickstart2.) +- [C#](c-sharp1.) - [(Quickstart)](quickstart3.) +- [TypeScript](typescript.) - [(Quickstart)](quickstart4.) +- [Python](python.) - [(Quickstart)](quickstart5.) + +## Key Features + +The SpacetimeDB Client SDKs offer the following key functionalities: + +### Connection Management + +The SDKs handle the process of connecting and disconnecting from the SpacetimeDB server, simplifying this process for the client applications. + +### Authentication + +The SDKs support authentication using an auth token, allowing clients to securely establish a session with the SpacetimeDB server. + +### Local Database View + +Each client can define a local view of the database via a subscription consisting of a set of queries. This local view is maintained by the server and populated into a local cache on the client side. + +### Reducer Calls + +The SDKs allow clients to call transactional functions (reducers) on the server. + +### Callback Registrations + +The SpacetimeDB Client SDKs offer powerful callback functionality that allow clients to monitor changes in their local database view. These callbacks come in two forms: + +#### Connection and Subscription Callbacks + +Clients can also register callbacks that trigger when the connection to the server is established or lost, or when a subscription is updated. This allows clients to react to changes in the connection status. + +#### Row Update Callbacks + +Clients can register callbacks that trigger when any row in their local cache is updated by the server. These callbacks contain information about the reducer that triggered the change. This feature enables clients to react to changes in data that they're interested in. + +#### Reducer Call Callbacks + +Clients can also register callbacks that fire when a reducer call modifies something in the client's local view. This allows the client to know when a transactional function it has executed has had an effect on the data it cares about. + +Additionally, when a client makes a reducer call that fails, the SDK triggers the registered reducer callback on the client that initiated the failed call with the error message that was returned from the server. This allows for appropriate error handling or user notifications. + +## Choosing a Language + +When selecting a language for your client application with SpacetimeDB, a variety of factors come into play. While the functionality of the SDKs remains consistent across different languages, the choice of language will often depend on the specific needs and context of your application. Here are a few considerations: + +### Team Expertise + +The familiarity of your development team with a particular language can greatly influence your choice. You might want to choose a language that your team is most comfortable with to increase productivity and reduce development time. + +### Application Type + +Different languages are often better suited to different types of applications. For instance, if you are developing a web-based application, you might opt for TypeScript due to its seamless integration with web technologies. On the other hand, if you're developing a desktop application, you might choose C# or Python, depending on your requirements and platform. Python is also very useful for utility scripts and tools. + +### Performance + +The performance characteristics of the different languages can also be a factor. If your application is performance-critical, you might opt for Rust, known for its speed and memory efficiency. + +### Platform Support + +The platform you're targeting can also influence your choice. For instance, if you're developing a game or a 3D application using the Unity engine, you'll want to choose the C# SDK, as Unity uses C# as its primary scripting language. + +### Ecosystem and Libraries + +Each language has its own ecosystem of libraries and tools that can help in developing your application. If there's a library in a particular language that you want to use, it may influence your choice. + +Remember, the best language to use is the one that best fits your use case and the one you and your team are most comfortable with. It's worth noting that due to the consistent functionality across different SDKs, transitioning from one language to another should you need to in the future will primarily involve syntax changes rather than changes in the application's logic. + +You may want to use multiple languages in your application. For instance, you might want to use C# in Unity for your game logic, TypeScript for a web-based administration panel, and Python for utility scripts. This is perfectly fine, as the SpacetimeDB server is completely client-agnostic. diff --git a/Writerside2/topics/sdks/typescript/typescript_index.md b/Writerside2/topics/sdks/typescript/typescript_index.md new file mode 100644 index 00000000..2316ecbb --- /dev/null +++ b/Writerside2/topics/sdks/typescript/typescript_index.md @@ -0,0 +1,942 @@ +# The SpacetimeDB Typescript client SDK + +The SpacetimeDB client SDK for TypeScript contains all the tools you need to build clients for SpacetimeDB modules using Typescript, either in the browser or with NodeJS. + +> You need a database created before use the client, so make sure to follow the Rust or C# Module Quickstart guides if need one. + +## Install the SDK + +First, create a new client project, and add the following to your `tsconfig.json` file: + +```json +{ + "compilerOptions": { + //You can use any target higher than this one + //https://www.typescriptlang.org/tsconfig#target + "target": "es2015" + } +} +``` + +Then add the SpacetimeDB SDK to your dependencies: + +```bash +cd client +npm install @clockworklabs/spacetimedb-sdk +``` + +You should have this folder layout starting from the root of your project: + +```bash +quickstart-chat +├── client +│ ├── node_modules +│ ├── public +│ └── src +└── server + └── src +``` + +### Tip for utilities/scripts + +If want to create a quick script to test your module bindings from the command line, you can use https://www.npmjs.com/package/tsx to execute TypeScript files. + +Then you create a `script.ts` file and add the imports, code and execute with: + +```bash +npx tsx src/script.ts +``` + +## Generate module bindings + +Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's `src` directory and generate the Typescript interface files using the Spacetime CLI. From your project directory, run: + +```bash +mkdir -p client/src/module_bindings +spacetime generate --lang typescript \ + --out-dir client/src/module_bindings \ + --project-path server +``` + +And now you will get the files for the `reducers` & `tables`: + +```bash +quickstart-chat +├── client +│ ├── node_modules +│ ├── public +│ └── src +| └── module_bindings +| ├── add_reducer.ts +| ├── person.ts +| └── say_hello_reducer.ts +└── server + └── src +``` + +Import the `module_bindings` in your client's _main_ file: + +```typescript +import { SpacetimeDBClient, Identity } from "@clockworklabs/spacetimedb-sdk"; + +import Person from "./module_bindings/person"; +import AddReducer from "./module_bindings/add_reducer"; +import SayHelloReducer from "./module_bindings/say_hello_reducer"; +console.log(Person, AddReducer, SayHelloReducer); +``` + +> There is a known issue where if you do not use every type in your file, it will not pull them into the published build. To fix this, we are using `console.log` to force them to get pulled in. + +## API at a glance + +### Classes + +| Class | Description | +|-------------------------------------------------|------------------------------------------------------------------------------| +| [`SpacetimeDBClient`](#class-spacetimedbclient.) | The database client connection to a SpacetimeDB server. | +| [`Identity`](typescript_#class-identity.) | The user's public identity. | +| [`Address`](typescript_#class-address.) | An opaque identifier for differentiating connections by the same `Identity`. | +| [`{Table}`](typescript_#class-table.) | `{Table}` is a placeholder for each of the generated tables. | +| [`{Reducer}`](typescript_#class-reducer.) | `{Reducer}` is a placeholder for each of the generated reducers. | + +### Class `SpacetimeDBClient` + +The database client connection to a SpacetimeDB server. + +Defined in [spacetimedb-sdk.spacetimedb](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/spacetimedb.ts): + +| Constructors | Description | +| ----------------------------------------------------------------- | ------------------------------------------------------------------------ | +| [`SpacetimeDBClient.constructor`](#spacetimedbclient-constructor.) | Creates a new `SpacetimeDBClient` database client. | +| Properties | +| [`SpacetimeDBClient.identity`](#spacetimedbclient-identity.) | The user's public identity. | +| [`SpacetimeDBClient.live`](#spacetimedbclient-live.) | Whether the client is connected. | +| [`SpacetimeDBClient.token`](#spacetimedbclient-token.) | The user's private authentication token. | +| Methods | | +| [`SpacetimeDBClient.connect`](#spacetimedbclient-connect.) | Connect to a SpacetimeDB module. | +| [`SpacetimeDBClient.disconnect`](#spacetimedbclient-disconnect.) | Close the current connection. | +| [`SpacetimeDBClient.subscribe`](#spacetimedbclient-subscribe.) | Subscribe to a set of queries. | +| Events | | +| [`SpacetimeDBClient.onConnect`](#spacetimedbclient-onconnect.) | Register a callback to be invoked upon authentication with the database. | +| [`SpacetimeDBClient.onError`](#spacetimedbclient-onerror.) | Register a callback to be invoked upon a error. | + +## Constructors + +### `SpacetimeDBClient` constructor + +Creates a new `SpacetimeDBClient` database client and set the initial parameters. + +```ts +new SpacetimeDBClient(host: string, name_or_address: string, auth_token?: string, protocol?: "binary" | "json") +``` + +#### Parameters + +| Name | Type | Description | +| :---------------- | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| `host` | `string` | The host of the SpacetimeDB server. | +| `name_or_address` | `string` | The name or address of the SpacetimeDB module. | +| `auth_token?` | `string` | The credentials to use to connect to authenticate with SpacetimeDB. | +| `protocol?` | `"binary"` \| `"json"` | Define how encode the messages: `"binary"` \| `"json"`. Binary is more efficient and compact, but JSON provides human-readable debug information. | + +#### Example + +```ts +const host = "ws://localhost:3000"; +const name_or_address = "database_name"; +const auth_token = undefined; +const protocol = "binary"; + +var spacetimeDBClient = new SpacetimeDBClient( + host, + name_or_address, + auth_token, + protocol +); +``` + +## Class methods + +### `SpacetimeDBClient.registerReducers` + +Registers reducer classes for use with a SpacetimeDBClient + +```ts +registerReducers(...reducerClasses: ReducerClass[]) +``` + +#### Parameters + +| Name | Type | Description | +| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | +| `reducerClasses` | `ReducerClass` | A list of classes to register | + +#### Example + +```ts +import SayHelloReducer from './types/say_hello_reducer'; +import AddReducer from './types/add_reducer'; + +SpacetimeDBClient.registerReducers(SayHelloReducer, AddReducer); +``` + +--- + +### `SpacetimeDBClient.registerTables` + +Registers table classes for use with a SpacetimeDBClient + +```ts +registerTables(...reducerClasses: TableClass[]) +``` + +#### Parameters + +| Name | Type | Description | +| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | +| `tableClasses` | `TableClass` | A list of classes to register | + +#### Example + +```ts +import User from './types/user'; +import Player from './types/player'; + +SpacetimeDBClient.registerTables(User, Player); +``` + +--- + +## Properties + +### `SpacetimeDBClient` identity + +The user's public [Identity](typescript_#class-identity.). + +``` +identity: Identity | undefined +``` + +--- + +### `SpacetimeDBClient` live + +Whether the client is connected. + +```ts +live: boolean; +``` + +--- + +### `SpacetimeDBClient` token + +The user's private authentication token. + +``` +token: string | undefined +``` + +#### Parameters + +| Name | Type | Description | +| :------------ | :----------------------------------------------------- | :------------------------------ | +| `reducerName` | `string` | The name of the reducer to call | +| `serializer` | [`Serializer`](serializer.Serializer.md) | - | + +--- + +### `SpacetimeDBClient` connect + +Connect to The SpacetimeDB Websocket For Your Module. By default, this will use a secure websocket connection. The parameters are optional, and if not provided, will use the values provided on construction of the client. + +```ts +connect(host: string?, name_or_address: string?, auth_token: string?): Promise +``` + +#### Parameters + +| Name | Type | Description | +| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | +| `host?` | `string` | The hostname of the SpacetimeDB server. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | +| `name_or_address?` | `string` | The name or address of the SpacetimeDB module. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | +| `auth_token?` | `string` | The credentials to use to authenticate with SpacetimeDB. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | + +#### Returns + +`Promise`<`void`\> + +#### Example + +```ts +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 +); +// Connect with the initial parameters +spacetimeDBClient.connect(); +//Set the `auth_token` +spacetimeDBClient.connect(undefined, undefined, NEW_TOKEN); +``` + +--- + +### `SpacetimeDBClient` disconnect + +Close the current connection. + +```ts +disconnect(): void +``` + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); + +spacetimeDBClient.disconnect(); +``` + +--- + +### `SpacetimeDBClient` subscribe + +Subscribe to a set of queries, to be notified when rows which match those queries are altered. + +> A new call to `subscribe` will remove all previous subscriptions and replace them with the new `queries`. +> If any rows matched the previous subscribed queries but do not match the new queries, +> those rows will be removed from the client cache, and [`{Table}.on_delete`](#table-ondelete.) callbacks will be invoked for them. + +```ts +subscribe(queryOrQueries: string | string[]): void +``` + +#### Parameters + +| Name | Type | Description | +| :--------------- | :--------------------- | :------------------------------- | +| `queryOrQueries` | `string` \| `string`[] | A `SQL` query or list of queries | + +#### Example + +```ts +spacetimeDBClient.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); +``` + +## Events + +### `SpacetimeDBClient` onConnect + +Register a callback to be invoked upon authentication with the database. + +```ts +onConnect(callback: (token: string, identity: Identity) => void): void +``` + +The callback will be invoked with the public user [Identity](typescript_#class-identity.), private authentication token and connection [`Address`](typescript_#class-address.) provided by the database. If credentials were supplied to [connect](#spacetimedbclient-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 will be generated by the database to identify this user. + +The credentials passed to the callback can be saved and used to authenticate the same user in future connections. + +#### Parameters + +| Name | Type | +|:-----------|:-----------------------------------------------------------------------------------------------------------------| +| `callback` | (`token`: `string`, `identity`: [`Identity`](typescript_#class-identity.), `address`: [`Address`](typescript_#class-address.)) => `void` | + +#### Example + +```ts +spacetimeDBClient.onConnect((token, identity, address) => { + console.log("Connected to SpacetimeDB"); + console.log("Token", token); + console.log("Identity", identity); + console.log("Address", address); +}); +``` + +--- + +### `SpacetimeDBClient` onError + +Register a callback to be invoked upon an error. + +```ts +onError(callback: (...args: any[]) => void): void +``` + +#### Parameters + +| Name | Type | +| :--------- | :----------------------------- | +| `callback` | (...`args`: `any`[]) => `void` | + +#### Example + +```ts +spacetimeDBClient.onError((...args: any[]) => { + console.error("ERROR", args); +}); +``` + +### Class `Identity` + +A unique public identifier for a user of a database. + +Defined in [spacetimedb-sdk.identity](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/identity.ts): + +| Constructors | Description | +| ----------------------------------------------- | -------------------------------------------- | +| [`Identity.constructor`](#identity-constructor.) | Creates a new `Identity`. | +| Methods | | +| [`Identity.isEqual`](#identity-isequal.) | Compare two identities for equality. | +| [`Identity.toHexString`](#identity-tohexstring.) | Print the identity as a hexadecimal string. | +| Static methods | | +| [`Identity.fromString`](#identity-fromstring.) | Parse an Identity from a hexadecimal string. | + +## Constructors + +### `Identity` constructor + +```ts +new Identity(data: Uint8Array) +``` + +#### Parameters + +| Name | Type | +| :----- | :----------- | +| `data` | `Uint8Array` | + +## Methods + +### `Identity` isEqual + +Compare two identities for equality. + +```ts +isEqual(other: Identity): boolean +``` + +#### Parameters + +| Name | Type | +| :------ | :---------------------------- | +| `other` | [`Identity`](typescript_#class-identity.) | + +#### Returns + +`boolean` + +--- + +### `Identity` toHexString + +Print an `Identity` as a hexadecimal string. + +```ts +toHexString(): string +``` + +#### Returns + +`string` + +--- + +### `Identity` fromString + +Static method; parse an Identity from a hexadecimal string. + +```ts +Identity.fromString(str: string): Identity +``` + +#### Parameters + +| Name | Type | +| :---- | :------- | +| `str` | `string` | + +#### Returns + +[`Identity`](typescript_#class-identity.) + +### Class `Address` + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](typescript_#type-identity.). + +Defined in [spacetimedb-sdk.address](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/address.ts): + +| Constructors | Description | +| ----------------------------------------------- | -------------------------------------------- | +| [`Address.constructor`](#address-constructor.) | Creates a new `Address`. | +| Methods | | +| [`Address.isEqual`](#address-isequal.) | Compare two identities for equality. | +| [`Address.toHexString`](#address-tohexstring.) | Print the address as a hexadecimal string. | +| Static methods | | +| [`Address.fromString`](#address-fromstring.) | Parse an Address from a hexadecimal string. | + +## Constructors + +### `Address` constructor + +```ts +new Address(data: Uint8Array) +``` + +#### Parameters + +| Name | Type | +| :----- | :----------- | +| `data` | `Uint8Array` | + +## Methods + +### `Address` isEqual + +Compare two addresses for equality. + +```ts +isEqual(other: Address): boolean +``` + +#### Parameters + +| Name | Type | +| :------ | :---------------------------- | +| `other` | [`Address`](typescript_#class-address.) | + +#### Returns + +`boolean` + +___ + +### `Address` toHexString + +Print an `Address` as a hexadecimal string. + +```ts +toHexString(): string +``` + +#### Returns + +`string` + +___ + +### `Address` fromString + +Static method; parse an Address from a hexadecimal string. + +```ts +Address.fromString(str: string): Address +``` + +#### Parameters + +| Name | Type | +| :---- | :------- | +| `str` | `string` | + +#### Returns + +[`Address`](typescript_#class-address.) + +### Class `{Table}` + +For each table defined by a module, `spacetime generate` generates a `class` in the `module_bindings` folder whose name is that table's name converted to `PascalCase`. + +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; returned subscribed rows with a given value in a particular 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 + +### {Table} name + +• **name**: `string` + +The name of the `Class`. + +--- + +### {Table} tableName + +The name of the table in the database. + +▪ `Static` **tableName**: `string` = `"Person"` + +## Methods + +### {Table} all + +Return all the subscribed rows in the table. + +```ts +{Table}.all(): {Table}[] +``` + +#### Returns + +`{Table}[]` + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); + +spacetimeDBClient.onConnect((token, identity, address) => { + spacetimeDBClient.subscribe(["SELECT * FROM Person"]); + + setTimeout(() => { + console.log(Person.all()); // Prints all the `Person` rows in the database. + }, 5000); +}); +``` + +--- + +### {Table} count + +Return the number of subscribed rows in the table, or 0 if there is no active connection. + +```ts +{Table}.count(): number +``` + +#### Returns + +`number` + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); + +spacetimeDBClient.onConnect((token, identity, address) => { + spacetimeDBClient.subscribe(["SELECT * FROM Person"]); + + setTimeout(() => { + console.log(Person.count()); + }, 5000); +}); +``` + +--- + +### {Table} filterBy{COLUMN} + +For each column of a table, `spacetime generate` generates a static method on the `Class` to filter or seek subscribed rows where that column matches a requested value. + +These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `camelCase`. + +```ts +{Table}.filterBy{COLUMN}(value): {Table}[] +``` + +#### Parameters + +| Name | Type | +| :------ | :-------------------------- | +| `value` | The type of the `{COLUMN}`. | + +#### Returns + +`{Table}[]` + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); + +spacetimeDBClient.onConnect((token, identity, address) => { + spacetimeDBClient.subscribe(["SELECT * FROM Person"]); + + setTimeout(() => { + console.log(Person.filterByName("John")); // prints all the `Person` rows named John. + }, 5000); +}); +``` + +--- + +### {Table} fromValue + +Deserialize an `AlgebraicType` into this `{Table}`. + +```ts + {Table}.fromValue(value: AlgebraicValue): {Table} +``` + +#### Parameters + +| Name | Type | +| :------ | :--------------- | +| `value` | `AlgebraicValue` | + +#### Returns + +`{Table}` + +--- + +### {Table} getAlgebraicType + +Serialize `this` into an `AlgebraicType`. + +#### Example + +```ts +{Table}.getAlgebraicType(): AlgebraicType +``` + +#### Returns + +`AlgebraicType` + +--- + +### {Table} onInsert + +Register an `onInsert` callback for when a subscribed row is newly inserted into the database. + +```ts +{Table}.onInsert(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | Description | +| :--------- | :---------------------------------------------------------------------------- | :----------------------------------------------------- | +| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is inserted. | + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); +spacetimeDBClient.onConnect((token, identity, address) => { + 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); + } +}); +``` + +--- + +### {Table} removeOnInsert + +Unregister a previously-registered [`onInsert`](#table-oninsert.) callback. + +```ts +{Table}.removeOnInsert(callback: (value: Person, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | +| :--------- | :---------------------------------------------------------------------------- | +| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | + +--- + +### {Table} onUpdate + +Register an `onUpdate` callback to run when an existing row is modified by primary key. + +```ts +{Table}.onUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +`onUpdate` callbacks are only meaningful for tables with a column declared as a primary key. Tables without primary keys will never fire `onUpdate` callbacks. + +#### Parameters + +| Name | Type | Description | +| :--------- | :------------------------------------------------------------------------------------------------------ | :---------------------------------------------------- | +| `callback` | (`oldValue`: `{Table}`, `newValue`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is updated. | + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); +spacetimeDBClient.onConnect((token, identity, address) => { + spacetimeDBClient.subscribe(["SELECT * FROM Person"]); +}); + +Person.onUpdate((oldPerson, newPerson, reducerEvent) => { + console.log("Person updated by reducer", reducerEvent, oldPerson, newPerson); +}); +``` + +--- + +### {Table} removeOnUpdate + +Unregister a previously-registered [`onUpdate`](#table-onupdate.) callback. + +```ts +{Table}.removeOnUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | +| :--------- | :------------------------------------------------------------------------------------------------------ | +| `callback` | (`oldValue`: `{Table}`, `newValue`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | + +--- + +### {Table} onDelete + +Register an `onDelete` callback for when a subscribed row is removed from the database. + +```ts +{Table}.onDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | Description | +| :--------- | :---------------------------------------------------------------------------- | :---------------------------------------------------- | +| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is removed. | + +#### Example + +```ts +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "database_name" +); +spacetimeDBClient.onConnect((token, identity, address) => { + 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 + ); + } +}); +``` + +--- + +### {Table} removeOnDelete + +Unregister a previously-registered [`onDelete`](#table-ondelete.) callback. + +```ts +{Table}.removeOnDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void +``` + +#### Parameters + +| Name | Type | +| :--------- | :---------------------------------------------------------------------------- | +| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | + +### Class `{Reducer}` + +`spacetime generate` defines an `{Reducer}` class in the `module_bindings` folder for each reducer defined by a module. + +The class's name will be the reducer's name converted to `PascalCase`. + +| Static methods | Description | +| ------------------------------- | ------------------------------------------------------------ | +| [`Reducer.call`](#reducer-call.) | Executes the reducer. | +| Events | | +| [`Reducer.on`](#reducer-on.) | Register a callback to run each time the reducer is invoked. | + +## Static methods + +### {Reducer} call + +Executes the reducer. + +```ts +{Reducer}.call(): void +``` + +#### Example + +```ts +SayHelloReducer.call(); +``` + +## Events + +### {Reducer} on + +Register a callback to run each time the reducer is invoked. + +```ts +{Reducer}.on(callback: (reducerEvent: ReducerEvent, ...reducerArgs: any[]) => void): void +``` + +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. + +#### Parameters + +| Name | Type | +| :--------- | :---------------------------------------------------------- | +| `callback` | `(reducerEvent: ReducerEvent, ...reducerArgs: any[]) => void)` | + +#### Example + +```ts +SayHelloReducer.on((reducerEvent, ...reducerArgs) => { + console.log("SayHelloReducer called", reducerEvent, reducerArgs); +}); +``` diff --git a/Writerside2/topics/sdks/typescript/typescript_quickstart.md b/Writerside2/topics/sdks/typescript/typescript_quickstart.md new file mode 100644 index 00000000..13ccd4d6 --- /dev/null +++ b/Writerside2/topics/sdks/typescript/typescript_quickstart.md @@ -0,0 +1,502 @@ +# Typescript Client SDK Quick Start + +In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in Typescript. + +We'll implement a basic single page web app for the module created in our Rust or C# Module Quickstart guides. **Make sure you follow one of these guides before you start on this one.** + +## Project structure + +Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: + +```bash +cd quickstart-chat +``` + +Within it, create a `client` react app: + +```bash +npx create-react-app client --template typescript +``` + +We also need to install the `spacetime-client-sdk` package: + +```bash +cd client +npm install @clockworklabs/spacetimedb-sdk +``` + +## Basic layout + +We are going to start by creating a basic layout for our app. The page contains four sections: + +1. A profile section, where we can set our name. +2. A message section, where we can see all the messages. +3. A system section, where we can see system messages. +4. A new message section, where we can send a new message. + +The `onSubmitNewName` and `onMessageSubmit` callbacks will be called when the user clicks the submit button in the profile and new message sections, respectively. We'll hook these up later. + +Replace the entire contents of `client/src/App.tsx` with the following: + +```typescript +import React, { useEffect, useState } from "react"; +import logo from "./logo.svg"; +import "./App.css"; + +export type MessageType = { + name: string; + message: string; +}; + +function App() { + const [newName, setNewName] = useState(""); + const [settingName, setSettingName] = useState(false); + const [name, setName] = useState(""); + const [systemMessage, setSystemMessage] = useState(""); + const [messages, setMessages] = useState([]); + + const [newMessage, setNewMessage] = useState(""); + + const onSubmitNewName = (e: React.FormEvent) => { + e.preventDefault(); + setSettingName(false); + // Fill in app logic here + }; + + const onMessageSubmit = (e: React.FormEvent) => { + e.preventDefault(); + // Fill in app logic here + setNewMessage(""); + }; + + return ( +
+
+

Profile

+ {!settingName ? ( + <> +

{name}

+ + + ) : ( +
+ setNewName(e.target.value)} + /> + + + )} +
+
+

Messages

+ {messages.length < 1 &&

No messages

} +
+ {messages.map((message, key) => ( +
+

+ {message.name} +

+

{message.message}

+
+ ))} +
+
+
+

System

+
+

{systemMessage}

+
+
+
+
+

New Message

+ + + +
+
+ ); +} + +export default App; +``` + +Now when you run `npm start`, you should see a basic chat app that does not yet send or receive messages. + +## Generate your module types + +The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. + +In your `quickstart-chat` directory, run: + +```bash +mkdir -p client/src/module_bindings +spacetime generate --lang typescript --out-dir client/src/module_bindings --project-path server +``` + +Take a look inside `client/src/module_bindings`. The CLI should have generated four files: + +``` +module_bindings +├── message.ts +├── send_message_reducer.ts +├── set_name_reducer.ts +└── user.ts +``` + +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 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); +``` + +## Create your SpacetimeDB client + +First, we need to create a SpacetimeDB client and connect to the module. Create your client at the top of the `App` function. + +We are going to create a stateful variable to store our client's SpacetimeDB identity when we receive it. Also, we are using `localStorage` to retrieve your auth token if this client has connected before. We will explain these later. + +Replace `` with the name you chose when publishing your module during the module quickstart. If you are using SpacetimeDB Cloud, the host will be `wss://spacetimedb.com/spacetimedb`. + +Add this before the `App` function declaration: + +```typescript +let token = localStorage.getItem("auth_token") || undefined; +var spacetimeDBClient = new SpacetimeDBClient( + "ws://localhost:3000", + "chat", + token +); +``` + +Inside the `App` function, add a few refs: + +```typescript +let local_identity = useRef(undefined); +let initialized = useRef(false); +const client = useRef(spacetimeDBClient); +``` + +## Register callbacks and connect + +We need to handle several sorts of events: + +1. `onConnect`: When we connect and receive our credentials, we'll save them to browser local storage, so that the next time we connect, we can re-authenticate as the same user. +2. `initialStateSync`: When we're informed of the backlog of past messages, we'll sort them and update the `message` section of the page. +3. `Message.onInsert`: When we receive a new message, we'll update the `message` section of the page. +4. `User.onInsert`: When a new user joins, we'll update the `system` section of the page with an appropiate message. +5. `User.onUpdate`: When a user is updated, we'll add a message with their new name, or declare their new online status to the `system` section of the page. +6. `SetNameReducer.on`: If the server rejects our attempt to set our name, we'll update the `system` section of the page with an appropriate error message. +7. `SendMessageReducer.on`: If the server rejects a message we send, we'll update the `system` section of the page with an appropriate error message. + +We will add callbacks for each of these items in the following sections. All of these callbacks will be registered inside the `App` function after the `useRef` declarations. + +### onConnect Callback + +On connect SpacetimeDB will provide us with our client credentials. + +Each user has a set of 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. + +These 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. + +We want to store our local client identity in a stateful variable and also save our `token` to local storage for future connections. + +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. + +Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. + +To the body of `App`, add: + +```typescript +client.current.onConnect((token, identity, address) => { + console.log("Connected to SpacetimeDB"); + + local_identity.current = identity; + + localStorage.setItem("auth_token", token); + + client.current.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); +}); +``` + +### initialStateSync callback + +This callback fires when our local client cache of the database is populated. This is a good time to set the initial messages list. + +We'll define a helper function, `setAllMessagesInOrder`, to supply the `MessageType` class for our React application. It will call the autogenerated `Message.all` function to get an array of `Message` rows, then sort them and convert them to `MessageType`. + +To find the `User` based on the message's `sender` identity, we'll use `User::filterByIdentity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filterByIdentity` accepts a `UInt8Array`, rather than an `Identity`. The `sender` identity stored in the message is also a `UInt8Array`, not an `Identity`, so we can just pass it to the filter method. + +Whenever we want to display a user name, if they have set a name, we'll use that. If they haven't set a name, we'll instead use the first 8 bytes of their identity, encoded as hexadecimal. We'll define the function `userNameOrIdentity` to handle this. + +We also 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 display `unknown`. + +To the body of `App`, add: + +```typescript +function userNameOrIdentity(user: User): string { + console.log(`Name: ${user.name} `); + if (user.name !== null) { + return user.name || ""; + } else { + var identityStr = new Identity(user.identity).toHexString(); + console.log(`Name: ${identityStr} `); + return new Identity(user.identity).toHexString().substring(0, 8); + } +} + +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 sender_identity = User.filterByIdentity(message.sender); + let display_name = sender_identity + ? userNameOrIdentity(sender_identity) + : "unknown"; + + return { + name: display_name, + message: message.text, + }; + }); + + setMessages(messagesType); +} + +client.current.on("initialStateSync", () => { + setAllMessagesInOrder(); + var user = User.filterByIdentity(local_identity?.current?.toUint8Array()!); + setName(userNameOrIdentity(user!)); +}); +``` + +### Message.onInsert callback - Update messages + +When we receive a new message, we'll update the messages section of the page. Keep in mind that we only want to do this for new messages, i.e. those inserted by a `send_message` reducer invocation. When the server is initializing our cache, we'll get a callback for each existing message, but we don't want to update the page for those. To that effect, our `onInsert` callback will check if its `ReducerEvent` argument is not `undefined`, and only update the `message` section in that case. + +To the body of `App`, add: + +```typescript +Message.onInsert((message, reducerEvent) => { + if (reducerEvent !== undefined) { + setAllMessagesInOrder(); + } +}); +``` + +### User.onInsert callback - 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 `onInsert` and `onDelete` methods of the trait `TableType`, 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.onInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. + +`onInsert` and `onDelete` callbacks take two arguments: the altered row, and a `ReducerEvent | undefined`. This will be `undefined` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is a class containing information about the reducer that triggered this event. For now, we can ignore this argument. + +We are going to add a helper function called `appendToSystemMessage` that will append a line to the `systemMessage` state. We will use this to update the `system` message when a new user joins. + +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); +} + +User.onInsert((user, reducerEvent) => { + if (user.online) { + appendToSystemMessage(`${userNameOrIdentity(user)} has connected.`); + } +}); +``` + +### User.onUpdate callback - 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 `onUpdate` method which is automatically implemented by `spacetime generate` for any table with a `#[primarykey]` column. + +`onUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. + +In our module, users can be updated for three reasons: + +1. They've set their name using the `set_name` reducer. +2. They're an existing user re-connecting, so their `online` has been set to `true`. +3. They've disconnected, so their `online` has been set to `false`. + +We'll update the `system` message in each of these cases. + +To the body of `App`, add: + +```typescript +User.onUpdate((oldUser, user, reducerEvent) => { + if (oldUser.online === false && user.online === true) { + appendToSystemMessage(`${userNameOrIdentity(user)} has connected.`); + } else if (oldUser.online === true && user.online === false) { + appendToSystemMessage(`${userNameOrIdentity(user)} has disconnected.`); + } + + if (user.name !== oldUser.name) { + appendToSystemMessage( + `User ${userNameOrIdentity(oldUser)} renamed to ${userNameOrIdentity( + user + )}.` + ); + } +}); +``` + +### SetNameReducer.on callback - Handle errors and update profile name + +We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducer` method which is automatically implemented for each reducer by `spacetime generate`. + +Each reducer callback takes a number of parameters: + +1. `ReducerEvent` that contains information about the reducer that triggered this event. It contains several fields. The ones we care about are: + + - `callerIdentity`: The `Identity` of the client that called the reducer. + - `status`: The `Status` of the reducer run, one of `"Committed"`, `"Failed"` or `"OutOfEnergy"`. + - `message`: The error message, if any, that the reducer returned. + +2. The rest of the parameters are arguments passed to the reducer. + +These callbacks will be invoked in one of two cases: + +1. If the reducer was successful and altered any of our subscribed rows. +2. If we requested an invocation which failed. + +Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. + +We already handle other users' `set_name` calls using our `User.onUpdate` callback, but we need some additional behavior for setting our own name. If our name was rejected, we'll update the `system` message. If our name was accepted, we'll update our name in the app. + +We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. + +If the reducer status comes back as `committed`, we'll update the name in our app. + +To the body of `App`, add: + +```typescript +SetNameReducer.on((reducerEvent, newName) => { + if ( + local_identity.current && + reducerEvent.callerIdentity.isEqual(local_identity.current) + ) { + if (reducerEvent.status === "failed") { + appendToSystemMessage(`Error setting name: ${reducerEvent.message} `); + } else if (reducerEvent.status === "committed") { + setName(newName); + } + } +}); +``` + +### SendMessageReducer.on callback - Handle errors + +We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different. We don't need to do anything for successful SendMessage reducer runs; our Message.onInsert callback already displays them. + +To the body of `App`, add: + +```typescript +SendMessageReducer.on((reducerEvent, newMessage) => { + if ( + local_identity.current && + reducerEvent.callerIdentity.isEqual(local_identity.current) + ) { + if (reducerEvent.status === "failed") { + appendToSystemMessage(`Error sending message: ${reducerEvent.message} `); + } + } +}); +``` + +## Update the UI button callbacks + +We need to update the `onSubmitNewName` and `onMessageSubmit` callbacks to send the appropriate reducer to the module. + +`spacetime generate` defined two functions for us, `SetNameReducer.call` and `SendMessageReducer.call`, 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 `SetNameReducer.call` and `SendMessageReducer.call` take one argument, a `String`. + +Add the following to the `onSubmitNewName` callback: + +```typescript +SetNameReducer.call(newName); +``` + +Add the following to the `onMessageSubmit` callback: + +```typescript +SendMessageReducer.call(newMessage); +``` + +## Connecting to the module + +We need to connect to the module when the app loads. We'll do this by adding a `useEffect` hook to the `App` function. This hook should only run once, when the component is mounted, but we are going to use an `initialized` boolean to ensure that it only runs once. + +```typescript +useEffect(() => { + if (!initialized.current) { + client.current.connect(); + initialized.current = true; + } +}, []); +``` + +## What's next? + +When you run `npm start` you should see a chat app that can send and receive messages. If you open it in multiple private browser windows, you should see that messages are synchronized between them. + +Congratulations! You've built a simple chat app with SpacetimeDB. You can find the full source code for this app [here](https://github.com/clockworklabs/spacetimedb-typescript-sdk/tree/main/examples/quickstart) + +For a more advanced example of the SpacetimeDB TypeScript SDK, take a look at the [Spacetime MUD (multi-user dungeon)](https://github.com/clockworklabs/spacetime-mud/tree/main/react-client). + +## Troubleshooting + +If you encounter the following error: + +``` +TS2802: Type 'IterableIterator' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher. +``` + +You can fix it by changing your compiler target. Add the following to your `tsconfig.json` file: + +```json +{ + "compilerOptions": { + "target": "es2015" + } +} +``` diff --git a/Writerside2/topics/sql/sql_index.md b/Writerside2/topics/sql/sql_index.md new file mode 100644 index 00000000..96f0c223 --- /dev/null +++ b/Writerside2/topics/sql/sql_index.md @@ -0,0 +1,407 @@ +# SQL Support + +SpacetimeDB supports a subset of SQL as a query language. Developers can evaluate SQL queries against a Spacetime database via the `spacetime sql` command-line tool and the [`/database/sql/:name_or_address POST` HTTP endpoint](database#databasesqlname_or_address-post.). Client developers also write SQL queries when subscribing to events in the [WebSocket API](ws#subscribe.) or via an SDK `subscribe` function. + +SpacetimeDB aims to support much of the [SQL 2016 standard](https://www.iso.org/standard/63555.html), and in particular aims to be compatible with [PostgreSQL](https://www.postgresql.org/). + +SpacetimeDB 0.6 implements a relatively small subset of SQL. Future SpacetimeDB versions will implement additional SQL features. + +## Types + +| Type | Description | +| --------------------------------------------- | -------------------------------------- | +| [Nullable types](#nullable-types.) | Types which may not hold a value. | +| [Logic types](#logic-types.) | Booleans, i.e. `true` and `false`. | +| [Integer types](#integer-types.) | Numbers without fractional components. | +| [Floating-point types](#floating-point-types.) | Numbers with fractional components. | +| [Text types](#text-types.) | UTF-8 encoded text. | + +### Definition statements + +| Statement | Description | +| ----------------------------- | ------------------------------------ | +| [CREATE TABLE](#create-table.) | Create a new table. | +| [DROP TABLE](#drop-table.) | Remove a table, discarding all rows. | + +### Query statements + +| Statement | Description | +| ----------------- | -------------------------------------------------------------------------------------------- | +| [FROM](#from.) | A source of data, like a table or a value. | +| [JOIN](#join.) | Combine several data sources. | +| [SELECT](#select.) | Select specific rows and columns from a data source, and optionally compute a derived value. | +| [DELETE](#delete.) | Delete specific rows from a table. | +| [INSERT](#insert.) | Insert rows into a table. | +| [UPDATE](#update.) | Update specific rows in a table. | + +## Data types + +SpacetimeDB is built on the Spacetime Algebraic Type System, or SATS. SATS is a richer, more expressive type system than the one included in the SQL language. + +Because SATS is a richer type system than SQL, some SATS types cannot cleanly correspond to SQL types. In particular, the SpacetimeDB SQL interface is unable to construct or compare instances of product and sum types. As such, SpacetimeDB SQL must largely restrict themselves to interacting with columns of builtin types. + +Most SATS builtin types map cleanly to SQL types. + +### Nullable types + +SpacetimeDB types, by default, do not permit `NULL` as a value. Nullable types are encoded in SATS using a sum type which corresponds to [Rust's `Option`](https://doc.rust-lang.org/stable/std/option/enum.Option.html). In SQL, such types can be written by adding the constraint `NULL`, like `INT NULL`. + +### Logic types + +| SQL | SATS | Example | +| --------- | ------ | --------------- | +| `BOOLEAN` | `Bool` | `true`, `false` | + +### Numeric types + +#### Integer types + +An integer is a number without a fractional component. + +Adding the `UNSIGNED` constraint to an integer type allows only positive values. This allows representing a larger positive range without increasing the width of the integer. + +| SQL | SATS | Example | Min | Max | +| ------------------- | ----- | ------- | ------ | ----- | +| `TINYINT` | `I8` | 1 | -(2⁷) | 2⁷-1 | +| `TINYINT UNSIGNED` | `U8` | 1 | 0 | 2⁸-1 | +| `SMALLINT` | `I16` | 1 | -(2¹⁵) | 2¹⁵-1 | +| `SMALLINT UNSIGNED` | `U16` | 1 | 0 | 2¹⁶-1 | +| `INT`, `INTEGER` | `I32` | 1 | -(2³¹) | 2³¹-1 | +| `INT UNSIGNED` | `U32` | 1 | 0 | 2³²-1 | +| `BIGINT` | `I64` | 1 | -(2⁶³) | 2⁶³-1 | +| `BIGINT UNSIGNED` | `U64` | 1 | 0 | 2⁶⁴-1 | + +#### Floating-point types + +SpacetimeDB supports single- and double-precision [binary IEEE-754 floats](https://en.wikipedia.org/wiki/IEEE_754). + +| SQL | SATS | Example | Min | Max | +| ----------------- | ----- | ------- | ------------------------ | ----------------------- | +| `REAL` | `F32` | 1.0 | -3.40282347E+38 | 3.40282347E+38 | +| `DOUBLE`, `FLOAT` | `F64` | 1.0 | -1.7976931348623157E+308 | 1.7976931348623157E+308 | + +### Text types + +SpacetimeDB supports a single string type, `String`. SpacetimeDB strings are UTF-8 encoded. + +| SQL | SATS | Example | Notes | +| ----------------------------------------------- | -------- | ------- | -------------------- | +| `CHAR`, `VARCHAR`, `NVARCHAR`, `TEXT`, `STRING` | `String` | 'hello' | Always UTF-8 encoded | + +> SpacetimeDB SQL currently does not support length contraints like `CHAR(10)`. + +## Syntax + +### Comments + +SQL line comments begin with `--`. + +```sql +-- This is a comment +``` + +### Expressions + +We can express different, composable, values that are universally called `expressions`. + +An expression is one of the following: + +#### Literals + +| Example | Description | +| --------- | ----------- | +| `1` | An integer. | +| `1.0` | A float. | +| `'hello'` | A string. | +| `true` | A boolean. | + +#### Binary operators + +| Example | Description | +| ------- | ------------------- | +| `1 > 2` | Integer comparison. | +| `1 + 2` | Integer addition. | + +#### Logical expressions + +Any expression which returns a boolean, i.e. `true` or `false`, is a logical expression. + +| Example | Description | +| ---------------- | ------------------------------------------------------------ | +| `1 > 2` | Integer comparison. | +| `1 + 2 == 3` | Equality comparison between a constant and a computed value. | +| `true AND false` | Boolean and. | +| `true OR false` | Boolean or. | +| `NOT true` | Boolean inverse. | + +#### Function calls + +| Example | Description | +| --------------- | -------------------------------------------------- | +| `lower('JOHN')` | Apply the function `lower` to the string `'JOHN'`. | + +#### Table identifiers + +| Example | Description | +| ------------- | ------------------------- | +| `inventory` | Refers to a table. | +| `"inventory"` | Refers to the same table. | + +#### Column references + +| Example | Description | +| -------------------------- | ------------------------------------------------------- | +| `inventory_id` | Refers to a column. | +| `"inventory_id"` | Refers to the same column. | +| `"inventory.inventory_id"` | Refers to the same column, explicitly naming its table. | + +#### Wildcards + +Special "star" expressions which select all the columns of a table. + +| Example | Description | +| ------------- | ------------------------------------------------------- | +| `*` | Refers to all columns of a table identified by context. | +| `inventory.*` | Refers to all columns of the `inventory` table. | + +#### Parenthesized expressions + +Sub-expressions can be enclosed in parentheses for grouping and to override operator precedence. + +| Example | Description | +| ------------- | ----------------------- | +| `1 + (2 / 3)` | One plus a fraction. | +| `(1 + 2) / 3` | A sum divided by three. | + +### `CREATE TABLE` + +A `CREATE TABLE` statement creates a new, initially empty table in the database. + +The syntax of the `CREATE TABLE` statement is: + +> **CREATE TABLE** _table_name_ (_column_name_ _data_type_, ...); + +![create-table](create_table.svg) + +#### Examples + +Create a table `inventory` with two columns, an integer `inventory_id` and a string `name`: + +```sql +CREATE TABLE inventory (inventory_id INTEGER, name TEXT); +``` + +Create a table `player` with two integer columns, an `entity_id` and an `inventory_id`: + +```sql +CREATE TABLE player (entity_id INTEGER, inventory_id INTEGER); +``` + +Create a table `location` with three columns, an integer `entity_id` and floats `x` and `z`: + +```sql +CREATE TABLE location (entity_id INTEGER, x REAL, z REAL); +``` + +### `DROP TABLE` + +A `DROP TABLE` statement removes a table from the database, deleting all its associated rows, indexes, constraints and sequences. + +To empty a table of rows without destroying the table, use [`DELETE`](#delete.). + +The syntax of the `DROP TABLE` statement is: + +> **DROP TABLE** _table_name_; + +![drop-table](drop_table.svg) + +Examples: + +```sql +DROP TABLE inventory; +``` + +## Queries + +### `FROM` + +A `FROM` clause derives a data source from a table name. + +The syntax of the `FROM` clause is: + +> **FROM** _table_name_ _join_clause_?; + +![from](from.svg) + +#### Examples + +Select all rows from the `inventory` table: + +```sql +SELECT * FROM inventory; +``` + +### `JOIN` + +A `JOIN` clause combines two data sources into a new data source. + +Currently, SpacetimeDB SQL supports only inner joins, which return rows from two data sources where the values of two columns match. + +The syntax of the `JOIN` clause is: + +> **JOIN** _table_name_ **ON** _expr_ = _expr_; + +![join](join.svg) + +### Examples + +Select all players rows who have a corresponding location: + +```sql +SELECT player.* FROM player + JOIN location + ON location.entity_id = player.entity_id; +``` + +Select all inventories which have a corresponding player, and where that player has a corresponding location: + +```sql +SELECT inventory.* FROM inventory + JOIN player + ON inventory.inventory_id = player.inventory_id + JOIN location + ON player.entity_id = location.entity_id; +``` + +### `SELECT` + +A `SELECT` statement returns values of particular columns from a data source, optionally filtering the data source to include only rows which satisfy a `WHERE` predicate. + +The syntax of the `SELECT` command is: + +> **SELECT** _column_expr_ > **FROM** _from_expr_ +> {**WHERE** _expr_}? + +![sql-select](select.svg) + +#### Examples + +Select all columns of all rows from the `inventory` table: + +```sql +SELECT * FROM inventory; +SELECT inventory.* FROM inventory; +``` + +Select only the `inventory_id` column of all rows from the `inventory` table: + +```sql +SELECT inventory_id FROM inventory; +SELECT inventory.inventory_id FROM inventory; +``` + +An optional `WHERE` clause can be added to filter the data source using a [logical expression](#logical-expressions.). The `SELECT` will return only the rows from the data source for which the expression returns `true`. + +#### Examples + +Select all columns of all rows from the `inventory` table, with a filter that is always true: + +```sql +SELECT * FROM inventory WHERE 1 = 1; +``` + +Select all columns of all rows from the `inventory` table with the `inventory_id` 1: + +```sql +SELECT * FROM inventory WHERE inventory_id = 1; +``` + +Select only the `name` column of all rows from the `inventory` table with the `inventory_id` 1: + +```sql +SELECT name FROM inventory WHERE inventory_id = 1; +``` + +Select all columns of all rows from the `inventory` table where the `inventory_id` is 2 or greater: + +```sql +SELECT * FROM inventory WHERE inventory_id > 1; +``` + +### `INSERT` + +An `INSERT INTO` statement inserts new rows into a table. + +One can insert one or more rows specified by value expressions. + +The syntax of the `INSERT INTO` statement is: + +> **INSERT INTO** _table_name_ (_column_name_, ...) **VALUES** (_expr_, ...), ...; + +![sql-insert](insert.svg) + +#### Examples + +Insert a single row: + +```sql +INSERT INTO inventory (inventory_id, name) VALUES (1, 'health1'); +``` + +Insert two rows: + +```sql +INSERT INTO inventory (inventory_id, name) VALUES (1, 'health1'), (2, 'health2'); +``` + +### UPDATE + +An `UPDATE` statement changes the values of a set of specified columns in all rows of a table, optionally filtering the table to update only rows which satisfy a `WHERE` predicate. + +Columns not explicitly modified with the `SET` clause retain their previous values. + +If the `WHERE` clause is absent, the effect is to update all rows in the table. + +The syntax of the `UPDATE` statement is + +> **UPDATE** _table_name_ **SET** > _column_name_ = _expr_, ... +> {_WHERE expr_}?; + +![sql-update](update.svg) + +#### Examples + +Set the `name` column of all rows from the `inventory` table with the `inventory_id` 1 to `'new name'`: + +```sql +UPDATE inventory + SET name = 'new name' + WHERE inventory_id = 1; +``` + +### DELETE + +A `DELETE` statement deletes rows that satisfy the `WHERE` clause from the specified table. + +If the `WHERE` clause is absent, the effect is to delete all rows in the table. In that case, the result is a valid empty table. + +The syntax of the `DELETE` statement is + +> **DELETE** _table_name_ +> {**WHERE** _expr_}?; + +![sql-delete](delete.svg) + +#### Examples + +Delete all the rows from the `inventory` table with the `inventory_id` 1: + +```sql +DELETE FROM inventory WHERE inventory_id = 1; +``` + +Delete all rows from the `inventory` table, leaving it empty: + +```sql +DELETE FROM inventory; +``` diff --git a/Writerside2/topics/unity/homeless.md b/Writerside2/topics/unity/homeless.md new file mode 100644 index 00000000..121fe538 --- /dev/null +++ b/Writerside2/topics/unity/homeless.md @@ -0,0 +1,355 @@ +### Create the Module + +1. It is important that you already have the SpacetimeDB CLI tool [installed](install.). + +2. Run SpacetimeDB locally using the installed CLI. In a **new** terminal or command window, run the following command: + +```bash +spacetime start +``` + +💡 Standalone mode will run in the foreground. +💡 Below examples Rust language, [but you may also use C#](c-sharp_index.md). + +## Create a Server Module + +Run the following command to initialize the SpacetimeDB server module project with Rust as the language: + +```bash +spacetime init --lang=rust server +``` + +This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. + +### SpacetimeDB Tables + +In this section we'll be making some edits to the file `server/src/lib.cs`. We recommend you open up this file in an IDE like VSCode or RustRover. + +**Important: Open the `server/src/lib.cs` file and delete its contents. We will be writing it from scratch here.** + +First we need to add some imports at the top of the file. + +**Copy and paste into lib.cs:** + +```csharp +// using SpacetimeDB; // Uncomment to omit `SpacetimeDB` attribute prefixes +using SpacetimeDB.Module; +using static SpacetimeDB.Runtime; +``` + +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](c-sharp.). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. + +**Append to the bottom of lib.cs:** + +```csharp +/// We're using this table as a singleton, +/// so there should typically only be one element where the version is 0. +[SpacetimeDB.Table] +public partial class Config +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public Identity Version; + public string? MessageOfTheDay; +} +``` + +Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. + +**Append to the bottom of lib.cs:** + +```csharp +/// This allows us to store 3D points in tables. +[SpacetimeDB.Type] +public partial class StdbVector3 +{ + public float X; + public float Y; + public float Z; +} +``` + +Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. + +```csharp +/// This stores information related to all entities in our game. In this tutorial +/// all entities must at least have an entity_id, a position, a direction and they +/// must specify whether or not they are moving. +[SpacetimeDB.Table] +public partial class EntityComponent +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong EntityId; + public StdbVector3 Position; + public float Direction; + public bool Moving; +} +``` + +Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. + +**Append to the bottom of lib.cs:** + +```csharp +/// All players have this component and it associates an entity with the user's +/// Identity. It also stores their username and whether or not they're logged in. +[SpacetimeDB.Table] +public partial class PlayerComponent +{ + // An EntityId that matches an EntityId in the `EntityComponent` table. + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + // The user's identity, which is unique to each player + [SpacetimeDB.Column(ColumnAttrs.Unique)] + public Identity Identity; + public string? Username; + public bool LoggedIn; +} +``` + +Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: + +**Append to the bottom of lib.cs:** + +```csharp +/// This reducer is called when the user logs in for the first time and +/// enters a username. +[SpacetimeDB.Reducer] +public static void CreatePlayer(DbEventArgs dbEvent, string username) +{ + // Get the Identity of the client who called this reducer + Identity sender = dbEvent.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"); + } + + // Create a new entity for this player + try + { + new EntityComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, + Direction = 0, + Moving = false, + }.Insert(); + } + catch + { + Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); + Throw; + } + + // The PlayerComponent uses the same entity_id and stores the identity of + // the owner, username, and whether or not they are logged in. + try + { + new PlayerComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Identity = dbEvent.Sender, + Username = username, + LoggedIn = true, + }.Insert(); + } + catch + { + Log("Error: Failed to insert PlayerComponent", LogLevel.Error); + throw; + } + Log($"Player created: {username}"); +} +``` + +--- + +**SpacetimeDB Reducers** + +"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. + +--- + +SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. + +- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. +- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. +- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. + +Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the module is initially published +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void OnInit() +{ + try + { + new Config + { + Version = 0, + MessageOfTheDay = "Hello, World!", + }.Insert(); + } + catch + { + Log("Error: Failed to insert Config", LogLevel.Error); + throw; + } +} +``` + +We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the client connects, we update the LoggedIn state to true +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void ClientConnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:true); +``` +```csharp +/// Called when the client disconnects, we update the logged_in state to false +[SpacetimeDB.Reducer(ReducerKind.Disconnect)] +public static void ClientDisonnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:false); +``` +```csharp +/// This helper function gets the PlayerComponent, sets the LoggedIn +/// variable and updates the PlayerComponent table row. +private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) +{ + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + player.LoggedIn = loggedIn; + PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); +} +``` + +Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. + +Using the `EntityId` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. + +**Append to the bottom of lib.cs:** + +```csharp +/// Updates the position of a player. This is also called when the player stops moving. +[SpacetimeDB.Reducer] +private static void UpdatePlayerPosition( + DbEventArgs dbEvent, + StdbVector3 position, + float direction, + bool moving) +{ + // First, look up the player using the sender identity + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + // Use the Player's EntityId to retrieve and update the EntityComponent + ulong playerEntityId = player.EntityId; + EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); + if (entity is null) + { + throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); + } + + entity.Position = position; + entity.Direction = direction; + entity.Moving = moving; + EntityComponent.UpdateByEntityId(playerEntityId, entity); +} +``` + +--- + +**Server Validation** + +In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. + +--- + +### Publishing a Module to SpacetimeDB + +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. + +```bash +cd server +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +### Finally, Add Chat Support + +The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. + +First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +[SpacetimeDB.Table] +public partial class ChatMessage +{ + // The primary key for this table will be auto-incremented + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + + // The entity id of the player that sent the message + public ulong SenderId; + + // Message contents + public string? Text; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +/// Adds a chat entry to the ChatMessage table +[SpacetimeDB.Reducer] +public static void SendChatMessage(DbEventArgs dbEvent, string text) +{ + // Get the player's entity id + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + + // Insert the chat message + new ChatMessage + { + SenderId = player.EntityId, + Text = text, + }.Insert(); +} +``` + +## Wrapping Up + +💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside2/topics/unity/part-1.md b/Writerside2/topics/unity/part-1.md new file mode 100644 index 00000000..bfad0644 --- /dev/null +++ b/Writerside2/topics/unity/part-1.md @@ -0,0 +1,57 @@ +# Unity Multiplayer Tutorial + +## Part 1 of 3: Setup + +This tutorial will guide you through setting up a multiplayer game project using Unity and SpacetimeDB. We will start by cloning the project, connecting it to SpacetimeDB and running the project. + +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! + +> [!IMPORTANT] +> TODO: This draft may link to WIP repos or docs - be sure to replace with final links after prerequisite PRs are approved (that are not yet approved upon writing this) + +## 1. Clone the Project + +Let's name it `SpacetimeDBUnityTutorial` for reference: +```bash +git clone https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade SpacetimeDBUnityTutorial +``` + +This project repo is separated into two sub-projects: + +1. [Server](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp) (SpacetimeDB Module) +1. [Client](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Client) (Unity project) + +> [!TIP] +> You may optionally _update_ the [SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk) via the Package Manager in Unity + +## 2. Publishing the Project + +From Unity, you don't need CLI commands for common functionality: + +1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) +1. Create an identity -> Select `testnet` for the server +1. Browse to your repo root `Server-Csharp` dir -> **Publish** -> **Generate** Unity files + +💡For the next section, we'll use the selected `Server` and publish result `Host` + +![Unity Publisher Tool](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-publisher.jpg) + +## 3. Connecting the Project + +1. Open `Scenes/Main` in Unity -> select the `GameManager` GameObject in the inspector. +1. Matching the earlier Publish setup: + 1. For the GameManager `Db Name or Address`, input `testnet` + 1. For the GameManager `Host`, input `https://testnet.spacetimedb.com +1. Save your scene + +## 4. Running the Project + +With the same `Main` scene open, press play! + +![Gameplay Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-action.jpg) + +![UI Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-ui.jpg) + +You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. + +Congratulations! You have successfully set up your multiplayer game project. In the next section, we will break down how Server Modules work and analyze the demo code. diff --git a/Writerside2/topics/unity/part-2.md b/Writerside2/topics/unity/part-2.md new file mode 100644 index 00000000..6856d42e --- /dev/null +++ b/Writerside2/topics/unity/part-2.md @@ -0,0 +1,489 @@ +# Unity Multiplayer Tutorial - Part 2 + +## Analyzing the C# Server Module + +This progressive tutorial is continued from [Part 1](part-11.md). + +In this part of the tutorial, we will create a SpacetimeDB (SpacetimeDB) server module using C# for the Unity multiplayer game. The server module will handle the game logic and data management for the game. + +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! + +## The Entity Component Systems (ECS) + +Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). + +We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. + +## C# Module Limitations & Nuances + +Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), you may run into unexpected issues until aware of the following: + +1. No DateTime-like types in Types or Tables: + - Use `string` for timestamps (exampled at [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs)), or `long` for Unix Epoch time. + +2. No Timers or async/await, such as those to create repeating loops: + - For repeating invokers, instead **re**schedule it from within a fired [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) function. + +3. Using `Debug` advanced option in the `Publisher` Unity editor tool will add callstack symbols for easier debugging: + - However, avoid using `Debug` mode when publishing outside a `localhost` server: + - Due to WASM buffer size limitations, this may cause publish failure. + +4. If you `throw` a new `Exception`, no error logs will appear. Instead, use either: + 1. Use `Log(message, LogLevel.Error);` before you throw. + 2. Use the demo's static [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs) class to `Utils.Throw()` to wrap the error log before throwing. + +5. `[AutoIncrement]` or `[PrimaryKeyAuto]` will never equal 0: + - Inserting a new row with an Auto key equaling 0 will always return a unique, non-0 value. + - +6. Enums cannot declare values out of the default order: + - For example, `{ Foo = 0, Bar = 3 }` will fail to compile. + +## Namespaces + +Common `using` statements include: + +```csharp +using SpacetimeDB; // Contains class|func|struct attributes like [Table], [Type], [Reducer] +using static SpacetimeDB.Runtime; // Contains Identity DbEventArgs, Log() +using SpacetimeDB.Module; // Contains prop attributes like [Column] +using Module.Utils; // Helper to workaround the `throw` and `DateTime` limitations noted above +``` + +- You will mostly see `SpacetimeDB.Module` in [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs) for schema definitions +- `SpacetimeDB` and `SpacetimeDB.Runtime` can be found in most all SpacetimeDB scripts +- `Module.Utils` parse DateTimeOffset into a timestamp string and wraps `throw` with error logs + +## Partial Classes & Structs + +- Throughout the demo, you will notice most classes or structs with a SpacetimeDB [Attribute] such as `[Table]` or `[Reducer]` will be defined with the `partial` keyword. + +- This allows the _Roslyn Compiler_ to [incrementally generate](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) additions to the SpacetimeDB SDK, such as adding helper functions and utilities. This means SpacetimeDB takes care of all the low-level tooling for you, such as inserting, updating or querying the DB. + - This further allows you to separate your models from logic within the same class. + +* Notice that the module class, itself, is also a `static partial class`. + +## Types & Tables + +`[Table]` attributes are database columns, while `[Type]` attributes are define a schema. + +### Types + +`[Type]` attributes attach to properties containing `[Table]` attributes when you want to use a custom Type that's not [SpacetimeDB natively-supported](c-sharp#supported-types.). These are generally defined as a `partial struct` or `partial class` + +Let's inspect a real example `Type`; open [Server-cs/Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): + +In Unity, you are likely familiar with the `Vector2` type. In SpacetimeDB, let's inspect the `StdbVector2` type to store 2D positions in the database: + +```csharp +/// A spacetime type which can be used in tables & reducers to represent a 2D position (such as movement) +[Type] +public partial class StdbVector2 +{ + public float X; + public float Z; + + // This allows us to use StdbVector2::ZERO in reducers + public static readonly StdbVector2 ZERO = new() + { + X = 0, + Z = 0, + }; +} +``` + +- Since `Types` are used in `Tables`, we can now use a custom SpacetimeDB `StdbVector3` `Type` in a `[Table]`. + +We may optionally include `static readonly` property "helper" functions such as the above-exampled `ZERO`. + +### Tables + +`[Table] attributes use `[Type]`s - either custom (like `StdbVector2` above) or [SpacetimeDB natively-supported types](../modules/c-sharp#supported-types). These are generally defined as a `struct` or `class` + +Let's inspect a real example `Table`, looking again at [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): + +```csharp +/// Represents chat messages within the game, including the sender and message content +[Table] +public partial class ChatMessage +{ + /// Primary key, automatically incremented + [Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong ChatEntityId; + + /// The entity id of the player (or NPC) that sent the message + public ulong SourceEntityId; + + /// Message contents + public string? ChatText; + + /// + /// Stringified ISO 8601 format (Unix Epoch Time) + /// + /// DateTime.ToUniversalTime().ToString("o"); + /// + public static string GetTimestamp(DateTimeOffset dateTimeOffset) => + dateTimeOffset.ToUniversalTime().ToString("o"); +} +``` + +- The `Id` vars are `ulong` types, commonly used for SpacetimeDB unique identifiers +- Notice how `Timestamp` is a `string` instead of DateTimeOffset (a limitation mentioned earlier). + +```csharp +/// This component will be created for all world objects that can move smoothly throughout the world, keeping track +/// of position, the last time the component was updated & the direction the mobile object is currently moving. +[Table] +public partial class MobileEntityComponent +{ + /// Primary key for the mobile entity + [Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + /// The last known location of this entity + public StdbVector2? Location; + + /// Movement direction, {0,0} if not moving at all. + public StdbVector2? Direction; + + /// Timestamp when movement started. Timestamp::UNIX_EPOCH if not moving. + public string? MoveStartTimestamp; +} +``` + +**Let's break this down:** + +- `EntityId` is the unique identifier for the table, declared as a `ulong` +- Location and Direction are both `StdbVector2` types discussed above +- `MoveStartTimestamp` is a string of epoch time, as you cannot use `DateTime`-like types within Tables. + - See the [Limitations](#limitations.) section below + +## Reducers + +Reducers are cloud functions that run on the server and can be called from the client, always returning `void`. + +Looking at the most straight-forward example, open [Chat.cs]( + + + + + + + +```csharp + +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](/docs/modules/c-sharp). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. + +**Append to the bottom of lib.cs:** + +```csharp +/// We're using this table as a singleton, +/// so there should typically only be one element where the version is 0. +[SpacetimeDB.Table] +public partial class Config +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public Identity Version; + public string? MessageOfTheDay; +} +``` + +Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. + +**Append to the bottom of lib.cs:** + +```csharp +/// This allows us to store 3D points in tables. +[SpacetimeDB.Type] +public partial class StdbVector3 +{ + public float X; + public float Y; + public float Z; +} +``` + +Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. + +```csharp +/// This stores information related to all entities in our game. In this tutorial +/// all entities must at least have an entity_id, a position, a direction and they +/// must specify whether or not they are moving. +[SpacetimeDB.Table] +public partial class EntityComponent +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong EntityId; + public StdbVector3 Position; + public float Direction; + public bool Moving; +} +``` + +Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. + +**Append to the bottom of lib.cs:** + +```csharp +/// All players have this component and it associates an entity with the user's +/// Identity. It also stores their username and whether or not they're logged in. +[SpacetimeDB.Table] +public partial class PlayerComponent +{ + // An EntityId that matches an EntityId in the `EntityComponent` table. + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + // The user's identity, which is unique to each player + [SpacetimeDB.Column(ColumnAttrs.Unique)] + public Identity Identity; + public string? Username; + public bool LoggedIn; +} +``` + +Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: + +**Append to the bottom of lib.cs:** + +```csharp +/// This reducer is called when the user logs in for the first time and +/// enters a username. +[SpacetimeDB.Reducer] +public static void CreatePlayer(DbEventArgs dbEvent, string username) +{ + // Get the Identity of the client who called this reducer + Identity sender = dbEvent.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"); + } + + // Create a new entity for this player + try + { + new EntityComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, + Direction = 0, + Moving = false, + }.Insert(); + } + catch + { + Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); + Throw; + } + + // The PlayerComponent uses the same entity_id and stores the identity of + // the owner, username, and whether or not they are logged in. + try + { + new PlayerComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Identity = dbEvent.Sender, + Username = username, + LoggedIn = true, + }.Insert(); + } + catch + { + Log("Error: Failed to insert PlayerComponent", LogLevel.Error); + throw; + } + Log($"Player created: {username}"); +} +``` + +--- + +**SpacetimeDB Reducers** + +"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. + +--- + +SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. + +- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. +- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. +- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. + +Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the module is initially published +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void OnInit() +{ + try + { + new Config + { + Version = 0, + MessageOfTheDay = "Hello, World!", + }.Insert(); + } + catch + { + Log("Error: Failed to insert Config", LogLevel.Error); + throw; + } +} +``` + +We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the client connects, we update the LoggedIn state to true +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void ClientConnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:true); +``` +```csharp +/// Called when the client disconnects, we update the logged_in state to false +[SpacetimeDB.Reducer(ReducerKind.Disconnect)] +public static void ClientDisonnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:false); +``` +```csharp +/// This helper function gets the PlayerComponent, sets the LoggedIn +/// variable and updates the PlayerComponent table row. +private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) +{ + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + player.LoggedIn = loggedIn; + PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); +} +``` + +Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. + +Using the `EntityId` in the `PlayerComponent` we retrieved, we can look up the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. + +**Append to the bottom of lib.cs:** + +```csharp +/// Updates the position of a player. This is also called when the player stops moving. +[SpacetimeDB.Reducer] +private static void UpdatePlayerPosition( + DbEventArgs dbEvent, + StdbVector3 position, + float direction, + bool moving) +{ + // First, look up the player using the sender identity + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + // Use the Player's EntityId to retrieve and update the EntityComponent + ulong playerEntityId = player.EntityId; + EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); + if (entity is null) + { + throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); + } + + entity.Position = position; + entity.Direction = direction; + entity.Moving = moving; + EntityComponent.UpdateByEntityId(playerEntityId, entity); +} +``` + +--- + +**Server Validation** + +In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. + +--- + +### Publishing a Module to SpacetimeDB + +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. + +```bash +cd server +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +### Finally, Add Chat Support + +The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. + +First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +[SpacetimeDB.Table] +public partial class ChatMessage +{ + // The primary key for this table will be auto-incremented + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + + // The entity id of the player that sent the message + public ulong SenderId; + + // Message contents + public string? Text; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +/// Adds a chat entry to the ChatMessage table +[SpacetimeDB.Reducer] +public static void SendChatMessage(DbEventArgs dbEvent, string text) +{ + // Get the player's entity id + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + + // Insert the chat message + new ChatMessage + { + SenderId = player.EntityId, + Text = text, + }.Insert(); +} +``` + +## Wrapping Up + +💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside2/topics/unity/part-2a-rust.md b/Writerside2/topics/unity/part-2a-rust.md new file mode 100644 index 00000000..1271b345 --- /dev/null +++ b/Writerside2/topics/unity/part-2a-rust.md @@ -0,0 +1,316 @@ +# Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (Rust) + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 1 Tutorial](part-11.md) + +## Create a Server Module + +Run the following command to initialize the SpacetimeDB server module project with Rust as the language: + +```bash +spacetime init --lang=rust server +``` + +This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. + +### SpacetimeDB Tables + +In this section we'll be making some edits to the file `server/src/lib.rs`. We recommend you open up this file in an IDE like VSCode or RustRover. + +**Important: Open the `server/src/lib.rs` file and delete its contents. We will be writing it from scratch here.** + +First we need to add some imports at the top of the file. + +**Copy and paste into lib.rs:** + +```rust +use spacetimedb::{spacetimedb, Identity, SpacetimeType, ReducerContext}; +use log; +``` + +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [Rust module reference](rust.). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. + +**Append to the bottom of lib.rs:** + +```rust +// We're using this table as a singleton, so there should typically only be one element where the version is 0. +#[spacetimedb(table)] +#[derive(Clone)] +pub struct Config { + #[primarykey] + pub version: u32, + pub message_of_the_day: String, +} +``` + +Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `#[derive(SpacetimeType)]` and a `#[spacetimedb(table)]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. + +**Append to the bottom of lib.rs:** + +```rust +// This allows us to store 3D points in tables. +#[derive(SpacetimeType, Clone)] +pub struct StdbVector3 { + pub x: f32, + pub y: f32, + pub z: f32, +} +``` + +Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. + +```rust +// This stores information related to all entities in our game. In this tutorial +// all entities must at least have an entity_id, a position, a direction and they +// must specify whether or not they are moving. +#[spacetimedb(table)] +#[derive(Clone)] +pub struct EntityComponent { + #[primarykey] + // The autoinc macro here just means every time we insert into this table + // we will receive a new row where this value will be increased by one. This + // allows us to easily get rows where `entity_id` is unique. + #[autoinc] + pub entity_id: u64, + pub position: StdbVector3, + pub direction: f32, + pub moving: bool, +} +``` + +Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `entity_id`. You'll see how this works later in the `create_player` reducer. + +**Append to the bottom of lib.rs:** + +```rust +// All players have this component and it associates an entity with the user's +// Identity. It also stores their username and whether or not they're logged in. +#[derive(Clone)] +#[spacetimedb(table)] +pub struct PlayerComponent { + // An entity_id that matches an entity_id in the `EntityComponent` table. + #[primarykey] + pub entity_id: u64, + + // The user's identity, which is unique to each player + #[unique] + pub owner_id: Identity, + pub username: String, + pub logged_in: bool, +} +``` + +Next, we write our very first reducer, `create_player`. From the client we will call this reducer when we create a new player: + +**Append to the bottom of lib.rs:** + +```rust +// This reducer is called when the user logs in for the first time and +// enters a username +#[spacetimedb(reducer)] +pub fn create_player(ctx: ReducerContext, username: String) -> Result<(), String> { + // Get the Identity of the client who called this reducer + let owner_id = ctx.sender; + + // Make sure we don't already have a player with this identity + if PlayerComponent::filter_by_owner_id(&owner_id).is_some() { + log::info!("Player already exists"); + return Err("Player already exists".to_string()); + } + + // Create a new entity for this player and get a unique `entity_id`. + let entity_id = EntityComponent::insert(EntityComponent + { + entity_id: 0, + position: StdbVector3 { x: 0.0, y: 0.0, z: 0.0 }, + direction: 0.0, + moving: false, + }).expect("Failed to create a unique PlayerComponent.").entity_id; + + // The PlayerComponent uses the same entity_id and stores the identity of + // the owner, username, and whether or not they are logged in. + PlayerComponent::insert(PlayerComponent { + entity_id, + owner_id, + username: username.clone(), + logged_in: true, + }).expect("Failed to insert player component."); + + log::info!("Player created: {}({})", username, entity_id); + + Ok(()) +} +``` + +--- + +**SpacetimeDB Reducers** + +"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. + +--- + +SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. + +- `init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. +- `connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `sender` value of the `ReducerContext`. +- `disconnect` - Called when a user disconnects from the SpacetimeDB module. + +Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. + +**Append to the bottom of lib.rs:** + +```rust +// Called when the module is initially published +#[spacetimedb(init)] +pub fn init() { + Config::insert(Config { + version: 0, + message_of_the_day: "Hello, World!".to_string(), + }).expect("Failed to insert config."); +} +``` + +We use the `connect` and `disconnect` reducers to update the logged in state of the player. The `update_player_login_state` helper function looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `logged_in` variable and calls the auto-generated `update` function on `PlayerComponent` to update the row. + +**Append to the bottom of lib.rs:** + +```rust +// Called when the client connects, we update the logged_in state to true +#[spacetimedb(connect)] +pub fn client_connected(ctx: ReducerContext) { + update_player_login_state(ctx, true); +} +``` +```rust +// Called when the client disconnects, we update the logged_in state to false +#[spacetimedb(disconnect)] +pub fn client_disconnected(ctx: ReducerContext) { + update_player_login_state(ctx, false); +} +``` +```rust +// This helper function gets the PlayerComponent, sets the logged +// in variable and updates the PlayerComponent table row. +pub fn update_player_login_state(ctx: ReducerContext, logged_in: bool) { + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + // We clone the PlayerComponent so we can edit it and pass it back. + let mut player = player.clone(); + player.logged_in = logged_in; + PlayerComponent::update_by_entity_id(&player.entity_id.clone(), player); + } +} +``` + +Our final reducer handles player movement. In `update_player_position` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `create_player` first. + +Using the `entity_id` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `update` function. + +**Append to the bottom of lib.rs:** + +```rust +// Updates the position of a player. This is also called when the player stops moving. +#[spacetimedb(reducer)] +pub fn update_player_position( + ctx: ReducerContext, + position: StdbVector3, + direction: f32, + moving: bool, +) -> Result<(), String> { + // First, look up the player using the sender identity, then use that + // entity_id to retrieve and update the EntityComponent + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + if let Some(mut entity) = EntityComponent::filter_by_entity_id(&player.entity_id) { + entity.position = position; + entity.direction = direction; + entity.moving = moving; + EntityComponent::update_by_entity_id(&player.entity_id, entity); + return Ok(()); + } + } + + // If we can not find the PlayerComponent or EntityComponent for + // this player then something went wrong. + return Err("Player not found".to_string()); +} +``` + +--- + +**Server Validation** + +In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. + +--- + +### Publishing a Module to SpacetimeDB + +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. + +```bash +cd server +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +### Finally, Add Chat Support + +The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. + +First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.rs``. + +**Append to the bottom of server/src/lib.rs:** + +```rust +#[spacetimedb(table)] +pub struct ChatMessage { + // The primary key for this table will be auto-incremented + #[primarykey] + #[autoinc] + pub message_id: u64, + + // The entity id of the player that sent the message + pub sender_id: u64, + // Message contents + pub text: String, +} +``` + +Now we need to add a reducer to handle inserting new chat messages. + +**Append to the bottom of server/src/lib.rs:** + +```rust +// Adds a chat entry to the ChatMessage table +#[spacetimedb(reducer)] +pub fn send_chat_message(ctx: ReducerContext, text: String) -> Result<(), String> { + if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { + // Now that we have the player we can insert the chat message using the player entity id. + ChatMessage::insert(ChatMessage { + // this column auto-increments so we can set it to 0 + message_id: 0, + sender_id: player.entity_id, + text, + }) + .unwrap(); + + return Ok(()); + } + + Err("Player not found".into()) +} +``` + +## Wrapping Up + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. + +From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside2/topics/unity/part-3.md b/Writerside2/topics/unity/part-3.md new file mode 100644 index 00000000..12e85ef3 --- /dev/null +++ b/Writerside2/topics/unity/part-3.md @@ -0,0 +1,479 @@ +# Unity Tutorial - Basic Multiplayer - Part 3 - Client + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from one of the Part 2 tutorials: +- [Rust Server Module](part-2a-rust1.md) +- [C# Server Module](part-2.) + +## Updating our Unity Project Client to use SpacetimeDB + +Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. + +### Import the SDK and Generate Module Files + +1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. + +```bash +https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git +``` + +![Unity-PackageManager](Unity-PackageManager.JPG) + +3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. + +```bash +mkdir -p ../client/Assets/module_bindings +spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp +``` + +### Connect to Your SpacetimeDB Module + +The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. + +![Unity-AddNetworkManager](Unity-AddNetworkManager.JPG) + +Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: + +**Append to the top of TutorialGameManager.cs** + +```csharp +using SpacetimeDB; +using SpacetimeDB.Types; +using System.Linq; +``` + +At the top of the class definition add the following members: + +**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** + +```csharp +// These are connection variables that are exposed on the GameManager +// inspector. +[SerializeField] private string moduleAddress = "unity-tutorial"; +[SerializeField] private string hostName = "localhost:3000"; + +// This is the identity for this player that is automatically generated +// the first time you log in. We set this variable when the +// onIdentityReceived callback is triggered by the SDK after connecting +private Identity local_identity; +``` + +The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. + +Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. + +**REPLACE the Start() function in TutorialGameManager.cs** + +```csharp +// Start is called before the first frame update +void Start() +{ + instance = this; + + SpacetimeDBClient.instance.onConnect += () => + { + Debug.Log("Connected."); + + // Request all tables + SpacetimeDBClient.instance.Subscribe(new List() + { + "SELECT * FROM *", + }); + }; + + // Called when we have an error connecting to SpacetimeDB + SpacetimeDBClient.instance.onConnectError += (error, message) => + { + Debug.LogError($"Connection error: " + message); + }; + + // Called when we are disconnected from SpacetimeDB + SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => + { + Debug.Log("Disconnected."); + }; + + // Called when we receive the client identity from SpacetimeDB + SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { + AuthToken.SaveToken(token); + local_identity = identity; + }; + + // Called after our local cache is populated from a Subscribe call + SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + + // Now that we’ve registered all our callbacks, lets connect to spacetimedb + SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); +} +``` + +In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. + +Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. + +--- + +**Local Client Cache** + +The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. + +--- + +Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. + +**Append after the Start() function in TutorialGameManager.cs** + +```csharp +void OnSubscriptionApplied() +{ + // If we don't have any data for our player, then we are creating a + // new one. Let's show the username dialog, which will then call the + // create player reducer + var player = PlayerComponent.FilterByOwnerId(local_identity); + if (player == null) + { + // Show username selection + UIUsernameChooser.instance.Show(); + } + + // Show the Message of the Day in our Config table of the Client Cache + UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); + + // Now that we've done this work we can unregister this callback + SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; +} +``` + +### Adding the Multiplayer Functionality + +Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. + +**Append to the top of UIUsernameChooser.cs** + +```csharp +using SpacetimeDB.Types; +``` + +Then we're doing a modification to the `ButtonPressed()` function: + +**Modify the ButtonPressed function in UIUsernameChooser.cs** + +```csharp +public void ButtonPressed() +{ + CameraController.RemoveDisabler(GetHashCode()); + _panel.SetActive(false); + + // Call the SpacetimeDB CreatePlayer reducer + Reducer.CreatePlayer(_usernameField.text); +} +``` + +We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. + +First append this using to the top of `RemotePlayer.cs` + +**Create file RemotePlayer.cs, then replace its contents:** + +```csharp +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using SpacetimeDB.Types; +using TMPro; + +public class RemotePlayer : MonoBehaviour +{ + public ulong EntityId; + + public TMP_Text UsernameElement; + + public string Username { set { UsernameElement.text = value; } } + + void Start() + { + // Initialize overhead name + UsernameElement = GetComponentInChildren(); + var canvas = GetComponentInChildren(); + canvas.worldCamera = Camera.main; + + // Get the username from the PlayerComponent for this object and set it in the UI + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + if (playerComp is null) + { + string inputUsername = UsernameElement.Text; + Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); + Reducer.CreatePlayer(inputUsername); + + // Try again, optimistically assuming success for simplicity + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + } + + Username = playerComp.Username; + + // Get the last location for this player and set the initial position + EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); + transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + + // Register for a callback that is called when the client gets an + // update for a row in the EntityComponent table + EntityComponent.OnUpdate += EntityComponent_OnUpdate; + } +} +``` + +We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. + +**Append to bottom of RemotePlayer class in RemotePlayer.cs:** + +```csharp +private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) +{ + // If the update was made to this object + if(obj.EntityId == EntityId) + { + var movementController = GetComponent(); + + // Update target position, rotation, etc. + movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); + movementController.RemoteTargetRotation = obj.Direction; + movementController.SetMoving(obj.Moving); + } +} +``` + +Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. + +**Append to bottom of Start() function in TutorialGameManager.cs:** + +```csharp +PlayerComponent.OnInsert += PlayerComponent_OnInsert; +``` + +Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. + +**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** + +```csharp +private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) +{ + // If the identity of the PlayerComponent matches our user identity then this is the local player + if(obj.OwnerId == local_identity) + { + // Now that we have our initial position we can start the game + StartGame(); + } + else + { + // Spawn the player object and attach the RemotePlayer component + var remotePlayer = Instantiate(PlayerPrefab); + + // Lookup and apply the position for this new player + var entity = EntityComponent.FilterByEntityId(obj.EntityId); + var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + remotePlayer.transform.position = position; + + var movementController = remotePlayer.GetComponent(); + movementController.RemoteTargetPosition = position; + movementController.RemoteTargetRotation = entity.Direction; + + remotePlayer.AddComponent().EntityId = obj.EntityId; + } +} +``` + +Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. + +**Append to the top of LocalPlayer.cs** + +```csharp +using SpacetimeDB.Types; +using SpacetimeDB; +``` + +**Append to the bottom of LocalPlayer class in LocalPlayer.cs** + +```csharp +private float? lastUpdateTime; +private void FixedUpdate() +{ + float? deltaTime = Time.time - lastUpdateTime; + bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; + bool isConnected = SpacetimeDBClient.instance.IsConnected(); + + if (hasUpdatedRecently || !isConnected) + { + return; + } + + lastUpdateTime = Time.time; + var p = PlayerMovementController.Local.GetModelPosition(); + + Reducer.UpdatePlayerPosition(new StdbVector3 + { + X = p.x, + Y = p.y, + Z = p.z, + }, + PlayerMovementController.Local.GetModelRotation(), + PlayerMovementController.Local.IsMoving()); +} +``` + +Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. + +![GameManager-Inspector2](GameManager-Inspector2.JPG) + +### Play the Game! + +Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. + +![Unity-AddOpenScenes](Unity-AddOpenScenes.JPG) + +When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. + +### Implement Player Logout + +So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. + +**Append to the bottom of Start() function in TutorialGameManager.cs** +```csharp +PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; +``` + +We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. + +Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. + +**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** +```csharp +private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) +{ + OnPlayerComponentChanged(newValue); +} + +private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) +{ + OnPlayerComponentChanged(obj); +} + +private void OnPlayerComponentChanged(PlayerComponent obj) +{ + // If the identity of the PlayerComponent matches our user identity then this is the local player + if(obj.OwnerId == local_identity) + { + // Now that we have our initial position we can start the game + StartGame(); + } + else + { + // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it + var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); + if (obj.LoggedIn) + { + // Only spawn remote players who aren't already spawned + if (existingPlayer == null) + { + // Spawn the player object and attach the RemotePlayer component + var remotePlayer = Instantiate(PlayerPrefab); + + // Lookup and apply the position for this new player + var entity = EntityComponent.FilterByEntityId(obj.EntityId); + var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + remotePlayer.transform.position = position; + + var movementController = remotePlayer.GetComponent(); + movementController.RemoteTargetPosition = position; + movementController.RemoteTargetRotation = entity.Direction; + + remotePlayer.AddComponent().EntityId = obj.EntityId; + } + } + else + { + if (existingPlayer != null) + { + Destroy(existingPlayer.gameObject); + } + } + } +} +``` + +Now you when you play the game you should see remote players disappear when they log out. + +Before updating the client, let's generate the client files and update publish our module. + +**Execute commands in the server/ directory** +```bash +spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp +spacetime publish -c unity-tutorial +``` + +On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. + +**Append to the top of UIChatController.cs:** +```csharp +using SpacetimeDB.Types; +``` + +**REPLACE the OnChatButtonPress function in UIChatController.cs:** + +```csharp +public void OnChatButtonPress() +{ + Reducer.SendChatMessage(_chatInput.text); + _chatInput.text = ""; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: + +**Append to the bottom of the Start() function in TutorialGameManager.cs:** +```csharp +Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; +``` + +Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. + +**Append after the Start() function in TutorialGameManager.cs** +```csharp +private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) +{ + var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); + if (player != null) + { + UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); + } +} +``` + +Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. + +## Conclusion + +This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: + +**Next Unity Tutorial:** [Resources & Scheduling](part-41.md) + +--- + +### Troubleshooting + +- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` + +- If you get this exception when running the project: + +``` +NullReferenceException: Object reference not set to an instance of an object +TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) +``` + +Check to see if your GameManager object in the Scene has the NetworkManager component attached. + +- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. + +``` +Connection error: Unable to connect to the remote server +``` diff --git a/Writerside2/topics/unity/part-4.md b/Writerside2/topics/unity/part-4.md new file mode 100644 index 00000000..f17ac2b0 --- /dev/null +++ b/Writerside2/topics/unity/part-4.md @@ -0,0 +1,261 @@ +# Unity Tutorial - Advanced - Part 4 - Resources and Scheduling + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 3](part-31.md) Tutorial. + +**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** + +In this second part of the lesson, we'll add resource nodes to our project and learn about scheduled reducers. Then we will spawn the nodes on the client so they are visible to the player. + +## Add Resource Node Spawner + +In this section we will add functionality to our server to spawn the resource nodes. + +### Step 1: Add the SpacetimeDB Tables for Resource Nodes + +1. Before we start adding code to the server, we need to add the ability to use the rand crate in our SpacetimeDB module so we can generate random numbers. Open the `Cargo.toml` file in the `Server` directory and add the following line to the `[dependencies]` section. + +```toml +rand = "0.8.5" +``` + +We also need to add the `getrandom` feature to our SpacetimeDB crate. Update the `spacetimedb` line to: + +```toml +spacetimedb = { "0.5", features = ["getrandom"] } +``` + +2. The first entity component we are adding, `ResourceNodeComponent`, stores the resource type. We'll define an enum to describe a `ResourceNodeComponent`'s type. For now, we'll just have one resource type: Iron. In the future, though, we'll add more resources by adding variants to the `ResourceNodeType` enum. Since we are using a custom enum, we need to mark it with the `SpacetimeType` attribute. Add the following code to lib.rs. + +```rust +#[derive(SpacetimeType, Clone)] +pub enum ResourceNodeType { + Iron, +} + +#[spacetimedb(table)] +#[derive(Clone)] +pub struct ResourceNodeComponent { + #[primarykey] + pub entity_id: u64, + + // Resource type of this resource node + pub resource_type: ResourceNodeType, +} +``` + +Because resource nodes never move, the `MobileEntityComponent` is overkill. Instead, we will add a new entity component named `StaticLocationComponent` that only stores the position and rotation. + +```rust +#[spacetimedb(table)] +#[derive(Clone)] +pub struct StaticLocationComponent { + #[primarykey] + pub entity_id: u64, + + pub location: StdbVector2, + pub rotation: f32, +} +``` + +3. We are also going to add a couple of additional column to our Config table. `map_extents` let's our spawner know where it can spawn the nodes. `num_resource_nodes` is the maximum number of nodes to spawn on the map. Update the config table in lib.rs. + +```rust +#[spacetimedb(table)] +pub struct Config { + // Config is a global table with a single row. This table will be used to + // store configuration or global variables + + #[primarykey] + // always 0 + // having a table with a primarykey field which is always zero is a way to store singleton global state + pub version: u32, + + pub message_of_the_day: String, + + // new variables for resource node spawner + // X and Z range of the map (-map_extents to map_extents) + pub map_extents: u32, + // maximum number of resource nodes to spawn on the map + pub num_resource_nodes: u32, +} +``` + +4. In the `init` reducer, we need to set the initial values of our two new variables. Update the following code: + +```rust + Config::insert(Config { + version: 0, + message_of_the_day: "Hello, World!".to_string(), + + // new variables for resource node spawner + map_extents: 25, + num_resource_nodes: 10, + }) + .expect("Failed to insert config."); +``` + +### Step 2: Write our Resource Spawner Repeating Reducer + +1. Add the following code to lib.rs. We are using a special attribute argument called repeat which will automatically schedule the reducer to run every 1000ms. + +```rust +#[spacetimedb(reducer, repeat = 1000ms)] +pub fn resource_spawner_agent(_ctx: ReducerContext, _prev_time: Timestamp) -> Result<(), String> { + let config = Config::filter_by_version(&0).unwrap(); + + // Retrieve the maximum number of nodes we want to spawn from the Config table + let num_resource_nodes = config.num_resource_nodes as usize; + + // Count the number of nodes currently spawned and exit if we have reached num_resource_nodes + let num_resource_nodes_spawned = ResourceNodeComponent::iter().count(); + if num_resource_nodes_spawned >= num_resource_nodes { + log::info!("All resource nodes spawned. Skipping."); + return Ok(()); + } + + // Pick a random X and Z based off the map_extents + let mut rng = rand::thread_rng(); + let map_extents = config.map_extents as f32; + let location = StdbVector2 { + x: rng.gen_range(-map_extents..map_extents), + z: rng.gen_range(-map_extents..map_extents), + }; + // Pick a random Y rotation in degrees + let rotation = rng.gen_range(0.0..360.0); + + // Insert our SpawnableEntityComponent which assigns us our entity_id + let entity_id = SpawnableEntityComponent::insert(SpawnableEntityComponent { entity_id: 0 }) + .expect("Failed to create resource spawnable entity component.") + .entity_id; + + // Insert our static location with the random position and rotation we selected + StaticLocationComponent::insert(StaticLocationComponent { + entity_id, + location: location.clone(), + rotation, + }) + .expect("Failed to insert resource static location component."); + + // Insert our resource node component, so far we only have iron + ResourceNodeComponent::insert(ResourceNodeComponent { + entity_id, + resource_type: ResourceNodeType::Iron, + }) + .expect("Failed to insert resource node component."); + + // Log that we spawned a node with the entity_id and location + log::info!( + "Resource node spawned: {} at ({}, {})", + entity_id, + location.x, + location.z, + ); + + Ok(()) +} +``` + +2. Since this reducer uses `rand::Rng` we need add include it. Add this `use` statement to the top of lib.rs. + +```rust +use rand::Rng; +``` + +3. Even though our reducer is set to repeat, we still need to schedule it the first time. Add the following code to the end of the `init` reducer. You can use this `schedule!` macro to schedule any reducer to run in the future after a certain amount of time. + +```rust + // Start our resource spawner repeating reducer + spacetimedb::schedule!("1000ms", resource_spawner_agent(_, Timestamp::now())); +``` + +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: + +```bash +spacetime generate --out-dir ../Assets/autogen --lang=csharp + +spacetime publish -c yourname/bitcraftmini +``` + +Your resource node spawner will start as soon as you publish since we scheduled it to run in our init reducer. You can watch the log output by using the `--follow` flag on the logs CLI command. + +```bash +spacetime logs -f yourname/bitcraftmini +``` + +### Step 3: Spawn the Resource Nodes on the Client + +1. First we need to update the `GameResource` component in Unity to work for multiplayer. Open GameResource.cs and add `using SpacetimeDB.Types;` to the top of the file. Then change the variable `Type` to be of type `ResourceNodeType` instead of `int`. Also add a new variable called `EntityId` of type `ulong`. + +```csharp + public ulong EntityId; + + public ResourceNodeType Type = ResourceNodeType.Iron; +``` + +2. Now that we've changed the `Type` variable, we need to update the code in the `PlayerAnimator` component that references it. Open PlayerAnimator.cs and update the following section of code. We need to add `using SpacetimeDB.Types;` to this file as well. This fixes the compile errors that result from changing the type of the `Type` variable to our new server generated enum. + +```csharp + var resourceType = res?.Type ?? ResourceNodeType.Iron; + switch (resourceType) + { + case ResourceNodeType.Iron: + _animator.SetTrigger("Mine"); + Interacting = true; + break; + default: + Interacting = false; + break; + } + for (int i = 0; i < _tools.Length; i++) + { + _tools[i].SetActive(((int)resourceType) == i); + } + _target = res; +``` + +3. Now that our `GameResource` is ready to be spawned, lets update the `BitcraftMiniGameManager` component to actually create them. First, we need to add the new tables to our SpacetimeDB subscription. Open BitcraftMiniGameManager.cs and update the following code: + +```csharp + SpacetimeDBClient.instance.Subscribe(new List() + { + "SELECT * FROM Config", + "SELECT * FROM SpawnableEntityComponent", + "SELECT * FROM PlayerComponent", + "SELECT * FROM MobileEntityComponent", + // Our new tables for part 2 of the tutorial + "SELECT * FROM ResourceNodeComponent", + "SELECT * FROM StaticLocationComponent" + }); +``` + +4. Next let's add an `OnInsert` handler for the `ResourceNodeComponent`. Add the following line to the `Start` function. + +```csharp + ResourceNodeComponent.OnInsert += ResourceNodeComponent_OnInsert; +``` + +5. Finally we add the new function to handle the insert event. This function will be called whenever a new `ResourceNodeComponent` is inserted into our local client cache. We can use this to spawn the resource node in the world. Add the following code to the `BitcraftMiniGameManager` class. + +To get the position and the rotation of the node, we look up the `StaticLocationComponent` for this entity by using the EntityId. + +```csharp + private void ResourceNodeComponent_OnInsert(ResourceNodeComponent insertedValue, ReducerEvent callInfo) + { + switch(insertedValue.ResourceType) + { + case ResourceNodeType.Iron: + var iron = Instantiate(IronPrefab); + StaticLocationComponent loc = StaticLocationComponent.FilterByEntityId(insertedValue.EntityId); + Vector3 nodePos = new Vector3(loc.Location.X, 0.0f, loc.Location.Z); + iron.transform.position = new Vector3(nodePos.x, MathUtil.GetTerrainHeight(nodePos), nodePos.z); + iron.transform.rotation = Quaternion.Euler(0.0f, loc.Rotation, 0.0f); + break; + } + } +``` + +### Step 4: Play the Game! + +6. Hit Play in the Unity Editor and you should now see your resource nodes spawning in the world! diff --git a/Writerside2/topics/unity/part-5.md b/Writerside2/topics/unity/part-5.md new file mode 100644 index 00000000..d4274636 --- /dev/null +++ b/Writerside2/topics/unity/part-5.md @@ -0,0 +1,108 @@ +# Unity Tutorial - Advanced - Part 5 - BitCraft Mini + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from the [Part 4](part-31.md) Tutorial. + +**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** + +BitCraft Mini is a game that we developed which extends the code you've already developed in this tutorial. It is inspired by our game [BitCraft](https://bitcraftonline.com) and illustrates how you could build a more complex game from just the components we've discussed. Right now you can walk around, mine ore, and manage your inventory. + +## 1. Download + +You can git-clone BitCraftMini from here: + +```plaintext +git clone ssh://git@github.com/clockworklabs/BitCraftMini +``` + +Once you have downloaded BitCraftMini, you will need to compile the spacetime module. + +## 2. Compile the Spacetime Module + +In order to compile the BitCraftMini module, you will need to install cargo. You can install cargo from here: + +> https://www.rust-lang.org/tools/install + +Once you have cargo installed, you can compile and publish the module with these commands: + +```bash +cd BitCraftMini/Server +spacetime publish +``` + +`spacetime publish` will output an address where your module has been deployed to. You will want to copy/save this address because you will need it in step 3. Here is an example of what it should look like: + +```plaintext +$ spacetime publish +info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date + Finished release [optimized] target(s) in 0.03s +Publish finished successfully. +Created new database with address: c91c17ecdcea8a05302be2bad9dd59b3 +``` + +Optionally, you can specify a name when you publish the module: + +```bash +spacetime publish "unique-module-name" +``` + +Currently, all the named modules exist in the same namespace so if you get a message saying that database is not owned by you, it means that someone else has already published a module with that name. You can either choose a different name or you can use the address instead. If you specify a name when you publish, you can use that name in place of the autogenerated address in both the CLI and in the Unity client. + +In the BitCraftMini module we have a function called `initialize()`. This function should be called immediately after publishing the module to spacetimedb. This function is in charge of generating some initial settings that are required for the server to operate. You can call this function like so: + +```bash +spacetime call "" "initialize" "[]" +``` + +Here we are telling spacetime to invoke the `initialize()` function on our module "bitcraftmini". If the function had some arguments, we would json encode them and put them into the "[]". Since `initialize()` requires no parameters, we just leave it empty. + +After you have called `initialize()` on the spacetime module you shouldgenerate the client files: + +```bash +spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs +``` + +Here is some sample output: + +```plaintext +$ spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang cs +info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date + Finished release [optimized] target(s) in 0.03s +compilation took 234.613518ms +Generate finished successfully. +``` + +If you've gotten this message then everything should be working properly so far. + +## 3. Replace address in BitCraftMiniGameManager + +The following settings are exposed in the `BitCraftMiniGameManager` inspector: Module Address, Host Name, and SSL Enabled. + +Open the Main scene in Unity and click on the `GameManager` object in the heirarchy. The inspector window will look like this: + +![GameManager-Inspector](GameManager-Inspector.JPG) + +Update the module address with the address you got from the `spacetime publish` command. If you are using SpacetimeDB Cloud `testnet`, the host name should be `testnet.spacetimedb.com` and SSL Enabled should be checked. If you are running SpacetimeDB Standalone locally, the host name should be `localhost:3000` and SSL Enabled should be unchecked. For instructions on how to deploy to these environments, see the [Deployment Section](testnet.) + +## 4. Play Mode + +You should now be able to enter play mode and walk around! You can mine some rocks, cut down some trees and if you connect more clients you can trade with other players. + +## 5. Editing the Module + +If you want to make further updates to the module, make sure to use this publish command instead: + +```bash +spacetime publish +``` + +Where `` is your own address. If you do this instead then you won't have to change the address inside of `BitCraftMiniGameManager.cs` + +When you change the server module you should also regenerate the client files as well: + +```bash +spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs +``` + +You may want to consider putting these 2 commands into a simple shell script to make the process a bit cleaner. diff --git a/Writerside2/topics/unity/unity_index.md b/Writerside2/topics/unity/unity_index.md new file mode 100644 index 00000000..a16870f0 --- /dev/null +++ b/Writerside2/topics/unity/unity_index.md @@ -0,0 +1,24 @@ +# Unity Tutorial Overview + +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! + +The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, Git, using a commandline terminal and coding. + +We'll give you some CLI commands to execute. If you are using Windows, we recommend using Git Bash or PowerShell. For Mac, we recommend Terminal. + +Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll be opening .cs files in an IDE like _Visual Studio_ or _Rider_. + +## Unity Tutorial - Basic Multiplayer +Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](rust.) or [C#](c-sharp.): + +- [Part 1 - Setup](part-11.md) +- [Part 2 - Server (C#)](part-21.md) ☼ +- [Part 3 - Client (Unity)](part-31.md) + +☼ While the tutorial uses C#, the repo cloned in [Part 1](part-11.md) does include a legacy Rust example to optionally use, instead. + +## Unity Tutorial - Advanced +By this point, you should already have a basic understanding of SpacetimeDB client, server and CLI: + +- [Part 4 - Resources & Scheduling](part-41.md) +- [Part 5 - BitCraft Mini](part-51.md) diff --git a/Writerside2/topics/webassembly-abi/webassembly-abi_index.md b/Writerside2/topics/webassembly-abi/webassembly-abi_index.md new file mode 100644 index 00000000..ceccfbd1 --- /dev/null +++ b/Writerside2/topics/webassembly-abi/webassembly-abi_index.md @@ -0,0 +1,499 @@ +# Module ABI Reference + +This document specifies the _low level details_ of module-host interactions (_"Module ABI"_). _**Most users**_ looking to interact with the host will want to use derived and higher level functionality like [`bindings`], `#[spacetimedb(table)]`, and `#[derive(SpacetimeType)]` rather than this low level ABI. For more on those, read the [Rust module quick start][module_quick_start] guide and the [Rust module reference][module_ref]. + +The Module ABI is defined in [`bindings_sys::raw`] and is used by modules to interact with their host and perform various operations like: + +- logging, +- transporting data, +- scheduling reducers, +- altering tables, +- inserting and deleting rows, +- querying tables. + +In the next few sections, we'll define the functions that make up the ABI and what these functions do. + +## General notes + +The functions in this ABI all use the [`C` ABI on the `wasm32` platform][wasm_c_abi]. They are specified in a Rust `extern "C" { .. }` block. For those more familiar with the `C` notation, an [appendix][c_header] is provided with equivalent definitions as would occur in a `.h` file. + +Many functions in the ABI take in- or out-pointers, e.g. `*const u8` and `*mut u8`. The WASM host itself does not have undefined behavior. However, what WASM does not consider a memory access violation could be one according to some other language's abstract machine. For example, running the following on a WASM host would violate Rust's rules around writing across allocations: + +```rust +fn main() { + let mut bytes = [0u8; 12]; + let other_bytes = [0u8; 4]; + unsafe { ffi_func_with_out_ptr_and_len(&mut bytes as *mut u8, 16); } + assert_eq!(other_bytes, [0u8; 4]); +} +``` + +When we note in this reference that traps occur or errors are returned on memory access violations, we only mean those that WASM can directly detected, and not cases like the one above. + +Should memory access violations occur, such as a buffer overrun, undefined behavior will never result, as it does not exist in WASM. However, in many cases, an error code will result. + +Some functions will treat UTF-8 strings _lossily_. That is, if the slice identified by a `(ptr, len)` contains non-UTF-8 bytes, these bytes will be replaced with `�` in the read string. + +Most functions return a `u16` value. This is how these functions indicate an error where a `0` value means that there were no errors. Such functions will instead return any data they need to through out pointers. + +## Logging + +```rust +/// The error log level. +const LOG_LEVEL_ERROR: u8 = 0; +/// The warn log level. +const LOG_LEVEL_WARN: u8 = 1; +/// The info log level. +const LOG_LEVEL_INFO: u8 = 2; +/// The debug log level. +const LOG_LEVEL_DEBUG: u8 = 3; +/// The trace log level. +const LOG_LEVEL_TRACE: u8 = 4; +/// The panic log level. +/// +/// A panic level is emitted just before +/// a fatal error causes the WASM module to trap. +const LOG_LEVEL_PANIC: u8 = 101; + +/// Log at `level` a `text` message occuring in `filename:line_number` +/// with `target` being the module path at the `log!` invocation site. +/// +/// These various pointers are interpreted lossily as UTF-8 strings. +/// The data pointed to are copied. Ownership does not transfer. +/// +/// See https://docs.rs/log/latest/log/struct.Record.html#method.target +/// for more info on `target`. +/// +/// Calls to the function cannot fail +/// irrespective of memory access violations. +/// If they occur, no message is logged. +fn _console_log( + // The level we're logging at. + // One of the `LOG_*` constants above. + level: u8, + // The module path, if any, associated with the message + // or to "blame" for the reason we're logging. + // + // This is a pointer to a buffer holding an UTF-8 encoded string. + // When the pointer is `NULL`, `target` is ignored. + target: *const u8, + // The length of the buffer pointed to by `text`. + // Unused when `target` is `NULL`. + target_len: usize, + // The file name, if any, associated with the message + // or to "blame" for the reason we're logging. + // + // This is a pointer to a buffer holding an UTF-8 encoded string. + // When the pointer is `NULL`, `filename` is ignored. + filename: *const u8, + // The length of the buffer pointed to by `text`. + // Unused when `filename` is `NULL`. + filename_len: usize, + // The line number associated with the message + // or to "blame" for the reason we're logging. + line_number: u32, + // A pointer to a buffer holding an UTF-8 encoded message to log. + text: *const u8, + // The length of the buffer pointed to by `text`. + text_len: usize, +); +``` + +## Buffer handling + +```rust +/// Returns the length of buffer `bufh` without +/// transferring ownership of the data into the function. +/// +/// The `bufh` must have previously been allocating using `_buffer_alloc`. +/// +/// Traps if the buffer does not exist. +fn _buffer_len( + // The buffer previously allocated using `_buffer_alloc`. + // Ownership of the buffer is not taken. + bufh: ManuallyDrop +) -> usize; + +/// Consumes the buffer `bufh`, +/// moving its contents to the WASM byte slice `(ptr, len)`. +/// +/// Returns an error if the buffer does not exist +/// or on any memory access violations associated with `(ptr, len)`. +fn _buffer_consume( + // The buffer to consume and move into `(ptr, len)`. + // Ownership of the buffer and its contents are taken. + // That is, `bufh` won't be usable after this call. + bufh: Buffer, + // A WASM out pointer to write the contents of `bufh` to. + ptr: *mut u8, + // The size of the buffer pointed to by `ptr`. + // This size must match that of `bufh` or a trap will occur. + len: usize +); + +/// Creates a buffer of size `data_len` in the host environment. +/// +/// The contents of the byte slice lasting `data_len` bytes +/// at the `data` WASM pointer are read +/// and written into the newly initialized buffer. +/// +/// Traps on any memory access violations. +fn _buffer_alloc(data: *const u8, data_len: usize) -> Buffer; +``` + +## Reducer scheduling + +```rust +/// Schedules a reducer to be called asynchronously at `time`. +/// +/// The reducer is named as the valid UTF-8 slice `(name, name_len)`, +/// and is passed the slice `(args, args_len)` as its argument. +/// +/// A generated schedule id is assigned to the reducer. +/// This id is written to the pointer `out`. +/// +/// Errors on any memory access violations, +/// if `(name, name_len)` does not point to valid UTF-8, +/// or if the `time` delay exceeds `64^6 - 1` milliseconds from now. +fn _schedule_reducer( + // A pointer to a buffer + // with a valid UTF-8 string of `name_len` many bytes. + name: *const u8, + // The number of bytes in the `name` buffer. + name_len: usize, + // A pointer to a byte buffer of `args_len` many bytes. + args: *const u8, + // The number of bytes in the `args` buffer. + args_len: usize, + // When to call the reducer. + time: u64, + // The schedule ID is written to this out pointer on a successful call. + out: *mut u64, +); + +/// Unschedules a reducer +/// using the same `id` generated as when it was scheduled. +/// +/// This assumes that the reducer hasn't already been executed. +fn _cancel_reducer(id: u64); +``` + +## Altering tables + +```rust +/// Creates an index with the name `index_name` and type `index_type`, +/// on a product of the given columns in `col_ids` +/// in the table identified by `table_id`. +/// +/// Here `index_name` points to a UTF-8 slice in WASM memory +/// and `col_ids` points to a byte slice in WASM memory +/// with each element being a column. +/// +/// Currently only single-column-indices are supported +/// and they may only be of the btree index type. +/// In the former case, the function will panic, +/// and in latter, an error is returned. +/// +/// Returns an error on any memory access violations, +/// if `(index_name, index_name_len)` is not valid UTF-8, +/// or when a table with the provided `table_id` doesn't exist. +/// +/// Traps if `index_type /= 0` or if `col_len /= 1`. +fn _create_index( + // A pointer to a buffer holding an UTF-8 encoded index name. + index_name: *const u8, + // The length of the buffer pointed to by `index_name`. + index_name_len: usize, + // The ID of the table to create the index for. + table_id: u32, + // The type of the index. + // Must be `0` currently, that is, a btree-index. + index_type: u8, + // A pointer to a buffer holding a byte slice + // where each element is the position + // of a column to include in the index. + col_ids: *const u8, + // The length of the byte slice in `col_ids`. Must be `1`. + col_len: usize, +) -> u16; +``` + +## Inserting and deleting rows + +```rust +/// Inserts a row into the table identified by `table_id`, +/// where the row is read from the byte slice `row_ptr` in WASM memory, +/// lasting `row_len` bytes. +/// +/// Errors if there were unique constraint violations, +/// if there were any memory access violations in associated with `row`, +/// if the `table_id` doesn't identify a table, +/// or if `(row, row_len)` doesn't decode from BSATN to a `ProductValue` +/// according to the `ProductType` that the table's schema specifies. +fn _insert( + // The table to insert the row into. + // The interpretation of `(row, row_len)` depends on this ID + // as it's table schema determines how to decode the raw bytes. + table_id: u32, + // An in/out pointer to a byte buffer + // holding the BSATN-encoded `ProductValue` row data to insert. + // + // The pointer is written to with the inserted row re-encoded. + // This is due to auto-incrementing columns. + row: *mut u8, + // The length of the buffer pointed to by `row`. + row_len: usize +) -> u16; + +/// Deletes all rows in the table identified by `table_id` +/// where the column identified by `col_id` matches the byte string, +/// in WASM memory, pointed to by `value`. +/// +/// Matching is defined by decoding of `value` to an `AlgebraicValue` +/// according to the column's schema and then `Ord for AlgebraicValue`. +/// +/// The number of rows deleted is written to the WASM pointer `out`. +/// +/// Errors if there were memory access violations +/// associated with `value` or `out`, +/// if no columns were deleted, +/// or if the column wasn't found. +fn _delete_by_col_eq( + // The table to delete rows from. + table_id: u32, + // The position of the column to match `(value, value_len)` against. + col_id: u32, + // A pointer to a byte buffer holding a BSATN-encoded `AlgebraicValue` + // of the `AlgebraicType` that the table's schema specifies + // for the column identified by `col_id`. + value: *const u8, + // The length of the buffer pointed to by `value`. + value_len: usize, + // An out pointer that the number of rows deleted is written to. + out: *mut u32 +) -> u16; +``` + +## Querying tables + +```rust +/// Queries the `table_id` associated with the given (table) `name` +/// where `name` points to a UTF-8 slice +/// in WASM memory of `name_len` bytes. +/// +/// The table id is written into the `out` pointer. +/// +/// Errors on memory access violations associated with `name` +/// or if the table does not exist. +fn _get_table_id( + // A pointer to a buffer holding the name of the table + // as a valid UTF-8 encoded string. + name: *const u8, + // The length of the buffer pointed to by `name`. + name_len: usize, + // An out pointer to write the table ID to. + out: *mut u32 +) -> u16; + +/// Finds all rows in the table identified by `table_id`, +/// where the row has a column, identified by `col_id`, +/// with data matching the byte string, +/// in WASM memory, pointed to at by `val`. +/// +/// Matching is defined by decoding of `value` +/// to an `AlgebraicValue` according to the column's schema +/// and then `Ord for AlgebraicValue`. +/// +/// The rows found are BSATN encoded and then concatenated. +/// The resulting byte string from the concatenation +/// is written to a fresh buffer +/// with the buffer's identifier written to the WASM pointer `out`. +/// +/// Errors if no table with `table_id` exists, +/// if `col_id` does not identify a column of the table, +/// if `(value, value_len)` cannot be decoded to an `AlgebraicValue` +/// typed at the `AlgebraicType` of the column, +/// or if memory access violations occurred associated with `value` or `out`. +fn _iter_by_col_eq( + // Identifies the table to find rows in. + table_id: u32, + // The position of the column in the table + // to match `(value, value_len)` against. + col_id: u32, + // A pointer to a byte buffer holding a BSATN encoded + // value typed at the `AlgebraicType` of the column. + value: *const u8, + // The length of the buffer pointed to by `value`. + value_len: usize, + // An out pointer to which the new buffer's id is written to. + out: *mut Buffer +) -> u16; + +/// Starts iteration on each row, as bytes, +/// of a table identified by `table_id`. +/// +/// The iterator is registered in the host environment +/// under an assigned index which is written to the `out` pointer provided. +/// +/// Errors if the table doesn't exist +/// or if memory access violations occurred in association with `out`. +fn _iter_start( + // The ID of the table to start row iteration on. + table_id: u32, + // An out pointer to which an identifier + // to the newly created buffer is written. + out: *mut BufferIter +) -> u16; + +/// Like [`_iter_start`], starts iteration on each row, +/// as bytes, of a table identified by `table_id`. +/// +/// The rows are filtered through `filter`, which is read from WASM memory +/// and is encoded in the embedded language defined by `spacetimedb_lib::filter::Expr`. +/// +/// The iterator is registered in the host environment +/// under an assigned index which is written to the `out` pointer provided. +/// +/// Errors if `table_id` doesn't identify a table, +/// if `(filter, filter_len)` doesn't decode to a filter expression, +/// or if there were memory access violations +/// in association with `filter` or `out`. +fn _iter_start_filtered( + // The ID of the table to start row iteration on. + table_id: u32, + // A pointer to a buffer holding an encoded filter expression. + filter: *const u8, + // The length of the buffer pointed to by `filter`. + filter_len: usize, + // An out pointer to which an identifier + // to the newly created buffer is written. + out: *mut BufferIter +) -> u16; + +/// Advances the registered iterator with the index given by `iter_key`. +/// +/// On success, the next element (the row as bytes) is written to a buffer. +/// The buffer's index is returned and written to the `out` pointer. +/// If there are no elements left, an invalid buffer index is written to `out`. +/// On failure however, the error is returned. +/// +/// Errors if `iter` does not identify a registered `BufferIter`, +/// or if there were memory access violations in association with `out`. +fn _iter_next( + // An identifier for the iterator buffer to advance. + // Ownership of the buffer nor the identifier is moved into the function. + iter: ManuallyDrop, + // An out pointer to write the newly created buffer's identifier to. + out: *mut Buffer +) -> u16; + +/// Drops the entire registered iterator with the index given by `iter_key`. +/// The iterator is effectively de-registered. +/// +/// Returns an error if the iterator does not exist. +fn _iter_drop( + // An identifier for the iterator buffer to unregister / drop. + iter: ManuallyDrop +) -> u16; +``` + +## Appendix, `bindings.h` + +```c +#include +#include +#include +#include +#include + +typedef uint32_t Buffer; +typedef uint32_t BufferIter; + +void _console_log( + uint8_t level, + const uint8_t *target, + size_t target_len, + const uint8_t *filename, + size_t filename_len, + uint32_t line_number, + const uint8_t *text, + size_t text_len +); + + +Buffer _buffer_alloc( + const uint8_t *data, + size_t data_len +); +void _buffer_consume( + Buffer bufh, + uint8_t *into, + size_t len +); +size_t _buffer_len(Buffer bufh); + + +void _schedule_reducer( + const uint8_t *name, + size_t name_len, + const uint8_t *args, + size_t args_len, + uint64_t time, + uint64_t *out +); +void _cancel_reducer(uint64_t id); + + +uint16_t _create_index( + const uint8_t *index_name, + size_t index_name_len, + uint32_t table_id, + uint8_t index_type, + const uint8_t *col_ids, + size_t col_len +); + + +uint16_t _insert( + uint32_t table_id, + uint8_t *row, + size_t row_len +); +uint16_t _delete_by_col_eq( + uint32_t table_id, + uint32_t col_id, + const uint8_t *value, + size_t value_len, + uint32_t *out +); + + +uint16_t _get_table_id( + const uint8_t *name, + size_t name_len, + uint32_t *out +); +uint16_t _iter_by_col_eq( + uint32_t table_id, + uint32_t col_id, + const uint8_t *value, + size_t value_len, + Buffer *out +); +uint16_t _iter_drop(BufferIter iter); +uint16_t _iter_next(BufferIter iter, Buffer *out); +uint16_t _iter_start(uint32_t table_id, BufferIter *out); +uint16_t _iter_start_filtered( + uint32_t table_id, + const uint8_t *filter, + size_t filter_len, + BufferIter *out +); +``` + +[`bindings_sys::raw`]: https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/bindings-sys/src/lib.rs#L44-L215 +[`bindings`]: https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/bindings/src/lib.rs +[module_ref]: /docs/languages/rust/rust-module-reference +[module_quick_start]: /docs/languages/rust/rust-module-quick-start +[wasm_c_abi]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md +[c_header]: #appendix-bindingsh diff --git a/Writerside2/topics/ws/ws_index.md b/Writerside2/topics/ws/ws_index.md new file mode 100644 index 00000000..4814bb45 --- /dev/null +++ b/Writerside2/topics/ws/ws_index.md @@ -0,0 +1,318 @@ +# The SpacetimeDB WebSocket API + +As an extension of the [HTTP API](http-api-reference.), SpacetimeDB offers a WebSocket API. Clients can subscribe to a database via a WebSocket connection to receive streaming updates as the database changes, and send requests to invoke reducers. Messages received from the server over a WebSocket will follow the same total ordering of transactions as are committed to the database. + +The SpacetimeDB SDKs comminicate with their corresponding database using the WebSocket API. + +## Connecting + +To initiate a WebSocket connection, send a `GET` request to the [`/database/subscribe/:name_or_address` endpoint](database#databasesubscribename_or_address-get.) with headers appropriate to upgrade to a WebSocket connection as per [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). + +To re-connect with an existing identity, include its token in a [SpacetimeDB Authorization header](http.). Otherwise, a new identity and token will be generated for the client. + +## Protocols + +Clients connecting via WebSocket can choose between two protocols, [`v1.bin.spacetimedb`](#binary-protocol.) and [`v1.text.spacetimedb`](#text-protocol.). Clients should include one of these protocols in the `Sec-WebSocket-Protocol` header of their request. + +| `Sec-WebSocket-Protocol` header value | Selected protocol | +| ------------------------------------- | -------------------------- | +| `v1.bin.spacetimedb` | [Binary](#binary-protocol.) | +| `v1.text.spacetimedb` | [Text](#text-protocol.) | + +### Binary Protocol + +The SpacetimeDB binary WebSocket protocol, `v1.bin.spacetimedb`, encodes messages using [ProtoBuf 3](https://protobuf.dev), and reducer and row data using [BSATN](bsatn.). + +The binary protocol's messages are defined in [`client_api.proto`](https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/client-api-messages/protobuf/client_api.proto). + +### Text Protocol + +The SpacetimeDB text WebSocket protocol, `v1.text.spacetimedb`, encodes messages, reducer and row data as JSON. Reducer arguments and table rows are JSON-encoded according to the [SATN JSON format](satn.). + +## Messages + +### Client to server + +| Message | Description | +| ------------------------------- | --------------------------------------------------------------------------- | +| [`FunctionCall`](#functioncall.) | Invoke a reducer. | +| [`Subscribe`](#subscribe.) | Register queries to receive streaming updates for a subset of the database. | + +#### `FunctionCall` + +Clients send a `FunctionCall` message to request that the database run a reducer. The message includes the reducer's name and a SATS `ProductValue` of arguments. + +##### Binary: ProtoBuf definition + +```protobuf +message FunctionCall { + string reducer = 1; + bytes argBytes = 2; +} +``` + +| Field | Value | +| ---------- | -------------------------------------------------------- | +| `reducer` | The name of the reducer to invoke. | +| `argBytes` | The reducer arguments encoded as a BSATN `ProductValue`. | + +##### Text: JSON encoding + +```typescript +{ + "call": { + "fn": string, + "args": array, + } +} +``` + +| Field | Value | +| ------ | ---------------------------------------------- | +| `fn` | The name of the reducer to invoke. | +| `args` | The reducer arguments encoded as a JSON array. | + +#### `Subscribe` + +Clients send a `Subscribe` message to register SQL queries in order to receive streaming updates. + +The client will only receive [`TransactionUpdate`s](#transactionupdate.) for rows to which it is subscribed, and for reducer runs which alter at least one subscribed row. As a special exception, the client is always notified when a reducer run it requests via a [`FunctionCall` message](#functioncall.) fails. + +SpacetimeDB responds to each `Subscribe` message with a [`SubscriptionUpdate` message](#subscriptionupdate.) containing all matching rows at the time the subscription is applied. + +Each `Subscribe` message establishes a new set of subscriptions, replacing all previous subscriptions. Clients which want to add a query to an existing subscription must send a `Subscribe` message containing all the previous queries in addition to the new query. In this case, the returned [`SubscriptionUpdate`](#subscriptionupdate.) will contain all previously-subscribed rows in addition to the newly-subscribed rows. + +Each query must be a SQL `SELECT * FROM` statement on a single table with an optional `WHERE` clause. See the [SQL Reference](sql.) for the subset of SQL supported by SpacetimeDB. + +##### Binary: ProtoBuf definition + +```protobuf +message Subscribe { + repeated string query_strings = 1; +} +``` + +| Field | Value | +| --------------- | ----------------------------------------------------------------- | +| `query_strings` | A sequence of strings, each of which contains a single SQL query. | + +##### Text: JSON encoding + +```typescript +{ + "subscribe": { + "query_strings": array + } +} +``` + +| Field | Value | +| --------------- | --------------------------------------------------------------- | +| `query_strings` | An array of strings, each of which contains a single SQL query. | + +### Server to client + +| Message | Description | +| ------------------------------------------- | -------------------------------------------------------------------------- | +| [`IdentityToken`](#identitytoken.) | Sent once upon successful connection with the client's identity and token. | +| [`SubscriptionUpdate`](#subscriptionupdate.) | Initial message in response to a [`Subscribe` message](#subscribe.). | +| [`TransactionUpdate`](#transactionupdate.) | Streaming update after a reducer runs containing altered rows. | + +#### `IdentityToken` + +Upon establishing a WebSocket connection, the server will send an `IdentityToken` message containing the client's identity and token. If the client included a [SpacetimeDB Authorization header](http.) in their connection request, the `IdentityToken` message will contain the same token used to connect, and its corresponding identity. If the client connected anonymously, SpacetimeDB will generate a new identity and token for the client. + +##### Binary: ProtoBuf definition + +```protobuf +message IdentityToken { + bytes identity = 1; + string token = 2; +} +``` + +| Field | Value | +| ---------- | --------------------------------------- | +| `identity` | The client's public Spacetime identity. | +| `token` | The client's private access token. | + +##### Text: JSON encoding + +```typescript +{ + "IdentityToken": { + "identity": array, + "token": string + } +} +``` + +| Field | Value | +| ---------- | --------------------------------------- | +| `identity` | The client's public Spacetime identity. | +| `token` | The client's private access token. | + +#### `SubscriptionUpdate` + +In response to a [`Subscribe` message](#subscribe.), the database sends a `SubscriptionUpdate` containing all of the matching rows which are resident in the database at the time the `Subscribe` was received. + +##### Binary: ProtoBuf definition + +```protobuf +message SubscriptionUpdate { + repeated TableUpdate tableUpdates = 1; +} + +message TableUpdate { + uint32 tableId = 1; + string tableName = 2; + repeated TableRowOperation tableRowOperations = 3; +} + +message TableRowOperation { + enum OperationType { + DELETE = 0; + INSERT = 1; + } + OperationType op = 1; + bytes row = 3; +} +``` + +Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribed rows. Each `TableUpdate` contains a `TableRowOperation` for each subscribed row. `SubscriptionUpdate`, `TableUpdate` and `TableRowOperation` are also used by the [`TransactionUpdate` message](#transactionupdate.) to encode rows altered by a reducer, so `TableRowOperation` includes an `OperationType` which identifies the row alteration as either an insert or a delete. When a client receives a `SubscriptionUpdate` message in response to a [`Subscribe` message](#subscribe.), all of the `TableRowOperation`s will have `op` of `INSERT`. + +| `TableUpdate` field | Value | +| -------------------- | ------------------------------------------------------------------------------------------------------------- | +| `tableId` | An integer identifier for the table. A table's `tableId` is not stable, so clients should not depend on it. | +| `tableName` | The string name of the table. Clients should use this field to identify the table, rather than the `tableId`. | +| `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`. | + +##### Text: JSON encoding + +```typescript +// SubscriptionUpdate: +{ + "SubscriptionUpdate": { + "table_updates": array + } +} + +// TableUpdate: +{ + "table_id": number, + "table_name": string, + "table_row_operations": array +} + +// TableRowOperation: +{ + "op": "insert" | "delete", + "row": array +} +``` + +Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribed rows. Each `TableUpdate` contains a `TableRowOperation` for each subscribed row. `SubscriptionUpdate`, `TableUpdate` and `TableRowOperation` are also used by the [`TransactionUpdate` message](#transactionupdate.) to encode rows altered by a reducer, so `TableRowOperation` includes an `"op"` field which identifies the row alteration as either an insert or a delete. When a client receives a `SubscriptionUpdate` message in response to a [`Subscribe` message](#subscribe.), all of the `TableRowOperation`s will have `"op"` of `"insert"`. + +| `TableUpdate` field | Value | +| ---------------------- | -------------------------------------------------------------------------------------------------------------- | +| `table_id` | An integer identifier for the table. A table's `table_id` is not stable, so clients should not depend on it. | +| `table_name` | The string name of the table. Clients should use this field to identify the table, rather than the `table_id`. | +| `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. | + +#### `TransactionUpdate` + +Upon a reducer run, a client will receive a `TransactionUpdate` containing information about the reducer which ran and the subscribed rows which it altered. Clients will only receive a `TransactionUpdate` for a reducer invocation if either of two criteria is met: + +1. The reducer ran successfully and altered at least one row to which the client subscribes. +2. The reducer was invoked by the client, and either failed or was terminated due to insufficient energy. + +Each `TransactionUpdate` contains a [`SubscriptionUpdate`](#subscriptionupdate.) with all rows altered by the reducer, including inserts and deletes; and an `Event` with information about the reducer itself, including a [`FunctionCall`](#functioncall.) containing the reducer's name and arguments. + +##### Binary: ProtoBuf definition + +```protobuf +message TransactionUpdate { + Event event = 1; + SubscriptionUpdate subscriptionUpdate = 2; +} + +message Event { + enum Status { + committed = 0; + failed = 1; + out_of_energy = 2; + } + uint64 timestamp = 1; + bytes callerIdentity = 2; + FunctionCall functionCall = 3; + Status status = 4; + string message = 5; + int64 energy_quanta_used = 6; + uint64 host_execution_duration_micros = 7; +} +``` + +| Field | Value | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `event` | An `Event` containing information about the reducer run. | +| `subscriptionUpdate` | A [`SubscriptionUpdate`](#subscriptionupdate.) containing all the row insertions and deletions committed by the transaction. | + +| `Event` field | Value | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `timestamp` | The time when the reducer started, as microseconds since the Unix epoch. | +| `callerIdentity` | The identity of the client which requested the reducer invocation. For event-driven and scheduled reducers, this is the identity of the database owner. | +| `functionCall` | A [`FunctionCall`](#functioncall.) containing the name of the reducer and the arguments passed to it. | +| `status` | `committed` if the reducer ran successfully and its changes were committed to the database; `failed` if the reducer signaled an error; `out_of_energy` if the reducer was canceled due to insufficient energy. | +| `message` | The error message with which the reducer failed if `status` is `failed`, or the empty string otherwise. | +| `energy_quanta_used` | The amount of energy consumed by running the reducer. | +| `host_execution_duration_micros` | The duration of the reducer's execution, in microseconds. | + +##### Text: JSON encoding + +```typescript +// TransactionUpdate: +{ + "TransactionUpdate": { + "event": Event, + "subscription_update": SubscriptionUpdate + } +} + +// Event: +{ + "timestamp": number, + "status": "committed" | "failed" | "out_of_energy", + "caller_identity": string, + "function_call": { + "reducer": string, + "args": array, + }, + "energy_quanta_used": number, + "message": string +} +``` + +| Field | Value | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `event` | An `Event` containing information about the reducer run. | +| `subscription_update` | A [`SubscriptionUpdate`](#subscriptionupdate.) containing all the row insertions and deletions committed by the transaction. | + +| `Event` field | Value | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `timestamp` | The time when the reducer started, as microseconds since the Unix epoch. | +| `status` | `committed` if the reducer ran successfully and its changes were committed to the database; `failed` if the reducer signaled an error; `out_of_energy` if the reducer was canceled due to insufficient energy. | +| `caller_identity` | The identity of the client which requested the reducer invocation. For event-driven and scheduled reducers, this is the identity of the database owner. | +| `function_call.reducer` | The name of the reducer. | +| `function_call.args` | The reducer arguments encoded as a JSON array. | +| `energy_quanta_used` | The amount of energy consumed by running the reducer. | +| `message` | The error message with which the reducer failed if `status` is `failed`, or the empty string otherwise. | diff --git a/Writerside2/v.list b/Writerside2/v.list new file mode 100644 index 00000000..2d12cb39 --- /dev/null +++ b/Writerside2/v.list @@ -0,0 +1,5 @@ + + + + + diff --git a/Writerside2/writerside.cfg b/Writerside2/writerside.cfg new file mode 100644 index 00000000..d390a5e2 --- /dev/null +++ b/Writerside2/writerside.cfg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/nav.js b/docs/nav.js index f013f783..9efcd534 100644 --- a/docs/nav.js +++ b/docs/nav.js @@ -19,7 +19,7 @@ const nav = { 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-2a-c-sharp.md"), + page("2b - Server (C#)", "unity/part-2", "unity/part-2a-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"), diff --git a/docs/unity/homeless.md b/docs/unity/homeless.md index cba27d87..b765bdba 100644 --- a/docs/unity/homeless.md +++ b/docs/unity/homeless.md @@ -11,3 +11,345 @@ spacetime start 💡 Standalone mode will run in the foreground. 💡 Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md). +## Create a Server Module + +Run the following command to initialize the SpacetimeDB server module project with Rust as the language: + +```bash +spacetime init --lang=rust server +``` + +This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. + +### SpacetimeDB Tables + +In this section we'll be making some edits to the file `server/src/lib.cs`. We recommend you open up this file in an IDE like VSCode or RustRover. + +**Important: Open the `server/src/lib.cs` file and delete its contents. We will be writing it from scratch here.** + +First we need to add some imports at the top of the file. + +**Copy and paste into lib.cs:** + +```csharp +// using SpacetimeDB; // Uncomment to omit `SpacetimeDB` attribute prefixes +using SpacetimeDB.Module; +using static SpacetimeDB.Runtime; +``` + +Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](/docs/modules/c-sharp). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. + +**Append to the bottom of lib.cs:** + +```csharp +/// We're using this table as a singleton, +/// so there should typically only be one element where the version is 0. +[SpacetimeDB.Table] +public partial class Config +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public Identity Version; + public string? MessageOfTheDay; +} +``` + +Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. + +**Append to the bottom of lib.cs:** + +```csharp +/// This allows us to store 3D points in tables. +[SpacetimeDB.Type] +public partial class StdbVector3 +{ + public float X; + public float Y; + public float Z; +} +``` + +Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. + +```csharp +/// This stores information related to all entities in our game. In this tutorial +/// all entities must at least have an entity_id, a position, a direction and they +/// must specify whether or not they are moving. +[SpacetimeDB.Table] +public partial class EntityComponent +{ + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong EntityId; + public StdbVector3 Position; + public float Direction; + public bool Moving; +} +``` + +Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. + +**Append to the bottom of lib.cs:** + +```csharp +/// All players have this component and it associates an entity with the user's +/// Identity. It also stores their username and whether or not they're logged in. +[SpacetimeDB.Table] +public partial class PlayerComponent +{ + // An EntityId that matches an EntityId in the `EntityComponent` table. + [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + // The user's identity, which is unique to each player + [SpacetimeDB.Column(ColumnAttrs.Unique)] + public Identity Identity; + public string? Username; + public bool LoggedIn; +} +``` + +Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: + +**Append to the bottom of lib.cs:** + +```csharp +/// This reducer is called when the user logs in for the first time and +/// enters a username. +[SpacetimeDB.Reducer] +public static void CreatePlayer(DbEventArgs dbEvent, string username) +{ + // Get the Identity of the client who called this reducer + Identity sender = dbEvent.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"); + } + + // Create a new entity for this player + try + { + new EntityComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, + Direction = 0, + Moving = false, + }.Insert(); + } + catch + { + Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); + Throw; + } + + // The PlayerComponent uses the same entity_id and stores the identity of + // the owner, username, and whether or not they are logged in. + try + { + new PlayerComponent + { + // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id + Identity = dbEvent.Sender, + Username = username, + LoggedIn = true, + }.Insert(); + } + catch + { + Log("Error: Failed to insert PlayerComponent", LogLevel.Error); + throw; + } + Log($"Player created: {username}"); +} +``` + +--- + +**SpacetimeDB Reducers** + +"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. + +--- + +SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. + +- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. +- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. +- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. + +Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the module is initially published +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void OnInit() +{ + try + { + new Config + { + Version = 0, + MessageOfTheDay = "Hello, World!", + }.Insert(); + } + catch + { + Log("Error: Failed to insert Config", LogLevel.Error); + throw; + } +} +``` + +We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. + +**Append to the bottom of lib.cs:** + +```csharp +/// Called when the client connects, we update the LoggedIn state to true +[SpacetimeDB.Reducer(ReducerKind.Init)] +public static void ClientConnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:true); +``` +```csharp +/// Called when the client disconnects, we update the logged_in state to false +[SpacetimeDB.Reducer(ReducerKind.Disconnect)] +public static void ClientDisonnected(DbEventArgs dbEvent) => + UpdatePlayerLoginState(dbEvent, loggedIn:false); +``` +```csharp +/// This helper function gets the PlayerComponent, sets the LoggedIn +/// variable and updates the PlayerComponent table row. +private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) +{ + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + player.LoggedIn = loggedIn; + PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); +} +``` + +Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. + +Using the `EntityId` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. + +**Append to the bottom of lib.cs:** + +```csharp +/// Updates the position of a player. This is also called when the player stops moving. +[SpacetimeDB.Reducer] +private static void UpdatePlayerPosition( + DbEventArgs dbEvent, + StdbVector3 position, + float direction, + bool moving) +{ + // First, look up the player using the sender identity + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + // Use the Player's EntityId to retrieve and update the EntityComponent + ulong playerEntityId = player.EntityId; + EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); + if (entity is null) + { + throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); + } + + entity.Position = position; + entity.Direction = direction; + entity.Moving = moving; + EntityComponent.UpdateByEntityId(playerEntityId, entity); +} +``` + +--- + +**Server Validation** + +In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. + +--- + +### Publishing a Module to SpacetimeDB + +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. + +```bash +cd server +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. + +### Finally, Add Chat Support + +The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. + +First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +[SpacetimeDB.Table] +public partial class ChatMessage +{ + // The primary key for this table will be auto-incremented + [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] + + // The entity id of the player that sent the message + public ulong SenderId; + + // Message contents + public string? Text; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. + +**Append to the bottom of server/src/lib.cs:** + +```csharp +/// Adds a chat entry to the ChatMessage table +[SpacetimeDB.Reducer] +public static void SendChatMessage(DbEventArgs dbEvent, string text) +{ + // Get the player's entity id + PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); + if (player is null) + { + throw new ArgumentException("Player not found"); + } + + + // Insert the chat message + new ChatMessage + { + SenderId = player.EntityId, + Text = text, + }.Insert(); +} +``` + +## Wrapping Up + +💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. + +From here, the tutorial continues with more-advanced topics. The [next tutorial](/docs/unity/part-4.md) introduces Resources & Scheduling. diff --git a/docs/unity/index.md b/docs/unity/index.md index 2b8e6d67..ecdf6801 100644 --- a/docs/unity/index.md +++ b/docs/unity/index.md @@ -1,20 +1,21 @@ # Unity Tutorial Overview -Need help with the tutorial or CLI commands? [Join our Discord server](https://discord.gg/spacetimedb)! +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! -The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, using a command line terminal and coding. +The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, Git, using a commandline terminal and coding. We'll give you some CLI commands to execute. If you are using Windows, we recommend using Git Bash or PowerShell. For Mac, we recommend Terminal. -Tested with UnityEngine `2022.3.20f1 LTS` (and may also work on newer versions). +Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll be opening .cs files in an IDE like _Visual Studio_ or _Rider_. ## Unity Tutorial - Basic Multiplayer Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](/docs/modules/rust) or [C#](/docs/modules/c-sharp): - [Part 1 - Setup](/docs/unity/part-1.md) -- [Part 2a - Server (Rust)](/docs/unity/part-2a-rust.md) -- [Part 2b - Server (C#)](/docs/unity/part-2b-csharp.md) -- [Part 3 - Client](/docs/unity/part-3.md) +- [Part 2 - Server (C#)](/docs/unity/part-2.md) ☼ +- [Part 3 - Client (Unity)](/docs/unity/part-3.md) + +☼ While the tutorial uses C#, the repo cloned in [Part 1](/docs/unity/part-1.md) does include a legacy Rust example to optionally use, instead. ## Unity Tutorial - Advanced By this point, you should already have a basic understanding of SpacetimeDB client, server and CLI: diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 9b76fce8..c2f913be 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -1,10 +1,13 @@ -# Unity Multiplayer Tutorial +# Unity Multiplayer Tutorial - Part 1 -## Part 1 of 3: Setup +## Project Setup -This tutorial will guide you through setting up a multiplayer game project using Unity and SpacetimeDB. We will start by cloning the project, connecting it to SpacetimeDB and running the project. +This progressive tutorial will guide you to: -💡 Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! +1. Quickly setup up a multiplayer game project demo, using Unity and SpacetimeDB. +2. Publish your demo SpacetimeDB C# server module to `testnet`. + +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! > [!IMPORTANT] > TODO: This draft may link to WIP repos or docs - be sure to replace with final links after prerequisite PRs are approved (that are not yet approved upon writing this) @@ -18,7 +21,7 @@ git clone https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/min This project repo is separated into two sub-projects: -1. [Server](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp) (STDB Module) +1. [Server](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp) (SpacetimeDB Module) 1. [Client](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Client) (Unity project) > [!TIP] @@ -54,5 +57,8 @@ With the same `Main` scene open, press play! You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. -Congratulations! You have successfully set up your multiplayer game project. In the next section, we will start integrating SpacetimeDB functionality to enable multiplayer features. +## Conclusion + +Congratulations! You have successfully set up your multiplayer game project. +In the next section, we will break down how Server Modules work and analyze the demo code. \ No newline at end of file diff --git a/docs/unity/part-2.md b/docs/unity/part-2.md new file mode 100644 index 00000000..062041da --- /dev/null +++ b/docs/unity/part-2.md @@ -0,0 +1,278 @@ +# Unity Multiplayer Tutorial - Part 2 + +# Analyzing the C# Server Module + +This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md). + +In this part of the tutorial, we will: + +1. Learn core concepts of the C# server module. +2. Review limitations and common practices. +3. Breakdown high-level concepts like Types, Tables, and Reducers. +4. Breakdown the initialization reducer and chat support from the demo for real-use examples. + +The server module will handle the game logic and data management for the game. + +💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! + +## The Entity Component Systems (ECS) + +Before we continue to creating the server module, it's important to understand the basics of the ECS. +This is a game development architecture that separates game objects into components for better flexibility and performance. +You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). + +We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, +making it ideal for building multiplayer games with SpacetimeDB. + +## C# Module Limitations & Nuances + +Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), you may run into unexpected issues until aware of the following: + +1. No DateTime-like types in Types or Tables: + - Use `string` for timestamps (exampled at [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs)), or `long` for Unix Epoch time. + + +2. No Timers or async/await, such as those to create repeating loops: + - For repeating invokers, instead **re**schedule it from within a fired [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) function. + + +3. Using `Debug` advanced option in the `Publisher` Unity editor tool will add callstack symbols for easier debugging: + - However, avoid using `Debug` mode when publishing outside a `localhost` server: + - Due to WASM buffer size limitations, this may cause publish failure. + + +4. If you `throw` a new `Exception`, no error logs will appear. Instead, use either: + 1. Use `Log(message, LogLevel.Error);` before you throw. + 2. Use the demo's static [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs) class to `Utils.Throw()` to wrap the error log before throwing. + + +5. `[AutoIncrement]` or `[PrimaryKeyAuto]` will never equal 0: + - Inserting a new row with an Auto key equaling 0 will always return a unique, non-0 value. + + +6. Enums cannot declare values out of the default order: + - For example, `{ Foo = 0, Bar = 3 }` will fail to compile. + +## Namespaces + +Common `using` statements include: + +```csharp +using SpacetimeDB; // Contains class|func|struct attributes like [Table], [Type], [Reducer] +using static SpacetimeDB.Runtime; // Contains Identity DbEventArgs, Log() +using SpacetimeDB.Module; // Contains prop attributes like [Column] +using Module.Utils; // Helper to workaround the `throw` and `DateTime` limitations noted above +``` + +- You will mostly see `SpacetimeDB.Module` in [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs) for schema definitions +- `SpacetimeDB` and `SpacetimeDB.Runtime` can be found in most all SpacetimeDB scripts +- `Module.Utils` parse DateTimeOffset into a timestamp string and wraps `throw` with error logs + +## Partial Classes & Structs + +- Throughout the demo, you will notice most classes or structs with a SpacetimeDB [Attribute] such as `[Table]` or `[Reducer]` will be defined with the `partial` keyword. + +- This allows the _Roslyn Compiler_ to [incrementally generate](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) additions to the SpacetimeDB SDK, such as adding helper functions and utilities. This means SpacetimeDB takes care of all the low-level tooling for you, such as inserting, updating or querying the DB. + - This further allows you to separate your models from logic within the same class. + +* Notice that the module class, itself, is also a `static partial class`. + +## Types & Tables + +`[Table]` attributes are database columns, while `[Type]` attributes are define a schema. + +### Types + +`[Type]` attributes attach to properties containing `[Table]` attributes when you want to use a custom Type that's not [SpacetimeDB natively-supported](../modules/c-sharp#supported-types). These are generally defined as a `partial struct` or `partial class` + +Let's inspect a real example `Type`; open [Server-cs/Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): + +In Unity, you are likely familiar with the `Vector2` type. In SpacetimeDB, let's inspect the `StdbVector2` type to store 2D positions in the database: + +```csharp +/// A spacetime type which can be used in tables & reducers to represent a 2D position (such as movement) +[Type] +public partial class StdbVector2 +{ + public float X; + public float Z; + + // This allows us to use StdbVector2::ZERO in reducers + public static readonly StdbVector2 ZERO = new() + { + X = 0, + Z = 0, + }; +} +``` + +- Since `Types` are used in `Tables`, we can now use a custom SpacetimeDB `StdbVector3` `Type` in a `[Table]`. + +We may optionally include `static readonly` property "helper" functions such as the above-exampled `ZERO`. + +### Tables + +`[Table]` attributes use `[Type]`s - either custom (like `StdbVector2` above) or [SpacetimeDB natively-supported types](../modules/c-sharp#supported-types). +These are generally defined as a `struct` or `class`. + +Let's inspect a real example `Table`, looking again at [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): + +```csharp +/// Represents chat messages within the game, including the sender and message content +[Table] +public partial class ChatMessage +{ + /// Primary key, automatically incremented + [Column(ColumnAttrs.PrimaryKeyAuto)] + public ulong ChatEntityId; + + /// The entity id of the player (or NPC) that sent the message + public ulong SourceEntityId; + + /// Message contents + public string? ChatText; +} +``` + +- The `Id` vars are `ulong` types, commonly used for SpacetimeDB unique identifiers +- Notice how `Timestamp` is a `string` instead of DateTimeOffset (a limitation mentioned earlier). + - 💡 We'll demonstrate how to set a timestamp correctly in the next section. + +```csharp +/// This component will be created for all world objects that can move smoothly throughout the world, keeping track +/// of position, the last time the component was updated & the direction the mobile object is currently moving. +[Table] +public partial class MobileEntityComponent +{ + /// Primary key for the mobile entity + [Column(ColumnAttrs.PrimaryKey)] + public ulong EntityId; + + /// The last known location of this entity + public StdbVector2? Location; + + /// Movement direction, {0,0} if not moving at all. + public StdbVector2? Direction; + + /// Timestamp when movement started. + /// This is a ISO 8601 format string; see Utils.GetTimestamp() + public string? MoveStartTimestamp; +} +``` + +- `EntityId` is the unique identifier for the table, declared as a `ulong` +- Location and Direction are both `StdbVector2` types discussed above +- `MoveStartTimestamp` is a stringified timestamp, as you cannot use `DateTime`-like types within Tables + - One of the [limitations](#limitations) mentioned earlier + + +## Reducers + +Reducers are static cloud functions with a `[Reducer]` attribute that run on the server. +These called from the client, always returning `void`. They are defined with the `[Reducer]` attribute and take +a `DbEventArgs` object as the first argument, followed by any number of arguments that you want to pass to the reducer. + +While there are some premade Reducers, such as for init and [dis]connection, you may also create your own. + +> [!NOTE] +> In a fully developed game, the server would typically perform server-side validation on client-requested actions to ensure +they comply with game boundaries, rules, and mechanics. + +### Overview + +Looking at the most straight-forward example, open [Chat.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Chat.cs): + +```c# +/// Add a chat entry to the ChatMessage table +[Reducer] +public static void SendChatMessage(DbEventArgs dbEvent, string text) +{ + // Get the player component based on the sender identity + PlayerComponent player = PlayerComponent.FindByOwnerId(dbEvent.Sender) ?? + Throw($"{nameof(SendChatMessage)} Error: Player not found"); + + // Now that we have the player we can insert the chat message + // using the player entity id. + new ChatMessage + { + ChatEntityId = 0, // This column auto-increments, so we can set it to 0 + SourceEntityId = player.EntityId, + ChatText = text, + Timestamp = Utils.Timestamp(dbEvent.Time), // ISO 8601 format) + }.Insert(); +} +``` + +- Every reducer starts with `[Reducer] public static void Foo(DbEventArgs dbEvent, ...)`. +- Every reducer contains a `DbEventArgs` as the 1st arg. + - Contains the sender's `Identity`, `DateTimeOffset` sent, and a semi-anonymous `Address` to compare sender vs others. +- The `PlayerComponent` was found by passing the sender's `Identity` to `PlayerComponent.FindByOwnerId()`. +- `Throw()` is the helper function (workaround for one of the [limitations](#limitations) mentioned earlier) that logs an error before throwing. +- This timestamp utilized `Utils.Timestamp()`; an easier-to-remember alias than `dbEvent.Time.ToUniversalTime().ToString("o");`. +- Since `ChatEntityId` is tagged with the `[Column(ColumnAttrs.PrimaryKeyAuto)]` attribute, setting it to 0 will auto-increment. + +### DB Initialization + +Let's find the entry point that gets called every time we **publish** or **clear** the database: +Open [Lib.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Lib.cs): + +> [!TIP] +> Not to be confused with the `Connect` ReducerKind for **player** initialization! + +```csharp +/// Initial configuration for the game world. +/// Called when the module is initially published. +[Reducer(ReducerKind.Init)] +public static void Init() +{ + try + { + Config config = initConfig(); // Create our global config table. + + spawnRocks(config, spawnCount: 10); + List unlocks = initUnlocks(); + ShopComponent shopComponent = getInitShopComponent(unlocks); + initItemCatalog(); + SpawnShop(shopComponent, requiredUnlock: null); + initResourceSpawners(); + } + catch (Exception e) + { + Throw($"{nameof(Init)} Error: {e.Message}"); + } +} +``` + +While this is very high-level, **this is what's happening:** + +1. We init the Config table, treating it as a singleton (there's only 1 Config at row `0`). +2. We spawn rocks into the world. +3. We create an unlockable and init our shop with these in-mind. +4. We init the item catalog, containing static metadata for the items, such as names<>ids. +5. We init our resource spawners, using the [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) to spawn our common and uncommon resources. + - For the common ones, we initially schedule them to fire almost-immediately. + - When the scheduler function fires, we **re**schedule them to infinitely call upon itself in intervals. + +### Other Premade Reducers + +- `[Reducer(ReducerKind.Connect)]` - Their `Identity` can be found in the `Sender` value of the `DbEventArgs`. +- `[Reducer(ReducerKind.Disconnect)]` +- `[Reducer(ReducerKind.Update)]` - Not to be confused with Unity-style Update loops, this calls when a `[Table]` row is updated. + + +## Wrapping Up + +💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) + +Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: + +```bash +spacetime publish -c unity-tutorial +``` + +## Conclusion + +You have now learned the core concepts of the C# server module, reviewed limitations and common practices +and broke down high-level concepts like Types, Tables, and Reducers with real examples from the demo. + +In the next section, we will break down the client-side code and analyze the Unity demo code. \ No newline at end of file diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index 98cfff0a..806cfbcf 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -4,7 +4,7 @@ Need help with the tutorial? [Join our Discord server](https://discord.gg/spacet This progressive tutorial is continued from one of the Part 2 tutorials: - [Rust Server Module](/docs/unity/part-2a-rust.md) -- [C# Server Module](/docs/unity/part-2b-c-sharp.md) +- [C# Server Module](/docs/unity/part-2) ## Updating our Unity Project Client to use SpacetimeDB diff --git a/nav.ts b/nav.ts index d700a268..c613e090 100644 --- a/nav.ts +++ b/nav.ts @@ -25,8 +25,8 @@ function section(title: string): NavSection { 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"), + page("Overview", "index", "index.md"), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? + page("Getting Started", "getting-started", "getting-started.md"), page("Overview", "index", "index.md"), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? page("Getting Started", "getting-started", "getting-started.md"), @@ -39,11 +39,11 @@ const nav: Nav = { 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-2a-c-sharp.md"), + page("2b - Server (C#)", "unity/part-2", "unity/part-2.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("4 - Resources & Scheduling", "unity/part-4", "unity/part-4.md"), page("5 - BitCraft Mini", "unity/part-5", "unity/part-5.md"), section("Server Module Languages"), From 2843f9cf6ecd14460b5908da8fc7639a470b980f Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Wed, 27 Mar 2024 10:15:17 +0800 Subject: [PATCH 16/24] fix: Line number typos --- docs/deploying/index.md | 2 +- docs/index.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/deploying/index.md b/docs/deploying/index.md index 723281a9..415841f5 100644 --- a/docs/deploying/index.md +++ b/docs/deploying/index.md @@ -4,7 +4,7 @@ SpacetimeDB supports both hosted and self-hosted publishing in multiple ways. Be 1. Generally introduce Identities. 1. Generally introduce Servers. -1Choose to proceed with either a [Hosted](/docs/deploying/hosted.md) or [Self-Hosted](/docs/deploying/self-hosted.md) deployment. +1. Choose to proceed with either a [Hosted](/docs/deploying/hosted.md) or [Self-Hosted](/docs/deploying/self-hosted.md) deployment. 💡 This tutorial assumes that you have already [installed](/install) the SpacetimeDB CLI. diff --git a/docs/index.md b/docs/index.md index 904abeb6..738f9a50 100644 --- a/docs/index.md +++ b/docs/index.md @@ -94,10 +94,10 @@ SpacetimeDB was designed first and foremost as the backend for multiplayer Unity 1. What is SpacetimeDB? It's a whole cloud platform within a database that's fast enough to run real-time games. -1. How do I use SpacetimeDB? +2. How do I use SpacetimeDB? Install the `spacetime` command line tool, choose your favorite language, import the SpacetimeDB library, write your application, compile it to WebAssembly, and upload it to the SpacetimeDB cloud platform. Once it's uploaded you can call functions directly on your application and subscribe to changes in application state. -1. How do I get/install SpacetimeDB? +3. How do I get/install SpacetimeDB? Just install our command line tool and then upload your application to the cloud. 4. How do I create a new database with SpacetimeDB? From 801fedba2e2f5a446a39f8d9234dd345621091fc Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Wed, 27 Mar 2024 14:41:43 +0800 Subject: [PATCH 17/24] doc(wip)!: Pt3 skeleton --- docs/unity/homeless.md | 484 +++++++++++++++++++++++++++++++++++++ docs/unity/part-2.md | 9 +- docs/unity/part-2a-rust.md | 316 ------------------------ docs/unity/part-3.md | 80 +++--- 4 files changed, 527 insertions(+), 362 deletions(-) delete mode 100644 docs/unity/part-2a-rust.md diff --git a/docs/unity/homeless.md b/docs/unity/homeless.md index b765bdba..c3d1b47d 100644 --- a/docs/unity/homeless.md +++ b/docs/unity/homeless.md @@ -353,3 +353,487 @@ spacetime publish -c unity-tutorial If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. From here, the tutorial continues with more-advanced topics. The [next tutorial](/docs/unity/part-4.md) introduces Resources & Scheduling. + + +___________________________ + +# Unity Tutorial - Basic Multiplayer - Part 3 - Client + +Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! + +This progressive tutorial is continued from one of the Part 2 tutorials: + +[//]: # (- [Rust Server Module](/docs/unity/part-2a-rust.md)) +- [C# Server Module](/docs/unity/part-2) + +## Updating our Unity Project Client to use SpacetimeDB + +Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. + +### Import the SDK and Generate Module Files + +1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. + +```bash +https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git +``` + +![Unity-PackageManager](/images/unity-tutorial/Unity-PackageManager.JPG) + +3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. + +```bash +mkdir -p ../client/Assets/module_bindings +spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp +``` + +### Connect to Your SpacetimeDB Module + +The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. + +![Unity-AddNetworkManager](/images/unity-tutorial/Unity-AddNetworkManager.JPG) + +Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: + +**Append to the top of TutorialGameManager.cs** + +```csharp +using SpacetimeDB; +using SpacetimeDB.Types; +using System.Linq; +``` + +At the top of the class definition add the following members: + +**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** + +```csharp +// These are connection variables that are exposed on the GameManager +// inspector. +[SerializeField] private string moduleAddress = "unity-tutorial"; +[SerializeField] private string hostName = "localhost:3000"; + +// This is the identity for this player that is automatically generated +// the first time you log in. We set this variable when the +// onIdentityReceived callback is triggered by the SDK after connecting +private Identity local_identity; +``` + +The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. + +Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. + +**REPLACE the Start() function in TutorialGameManager.cs** + +```csharp +// Start is called before the first frame update +void Start() +{ + instance = this; + + SpacetimeDBClient.instance.onConnect += () => + { + Debug.Log("Connected."); + + // Request all tables + SpacetimeDBClient.instance.Subscribe(new List() + { + "SELECT * FROM *", + }); + }; + + // Called when we have an error connecting to SpacetimeDB + SpacetimeDBClient.instance.onConnectError += (error, message) => + { + Debug.LogError($"Connection error: " + message); + }; + + // Called when we are disconnected from SpacetimeDB + SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => + { + Debug.Log("Disconnected."); + }; + + // Called when we receive the client identity from SpacetimeDB + SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { + AuthToken.SaveToken(token); + local_identity = identity; + }; + + // Called after our local cache is populated from a Subscribe call + SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + + // Now that we’ve registered all our callbacks, lets connect to spacetimedb + SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); +} +``` + +In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. + +Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. + +--- + +**Local Client Cache** + +The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. + +--- + +Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. + +**Append after the Start() function in TutorialGameManager.cs** + +```csharp +void OnSubscriptionApplied() +{ + // If we don't have any data for our player, then we are creating a + // new one. Let's show the username dialog, which will then call the + // create player reducer + var player = PlayerComponent.FilterByOwnerId(local_identity); + if (player == null) + { + // Show username selection + UIUsernameChooser.instance.Show(); + } + + // Show the Message of the Day in our Config table of the Client Cache + UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); + + // Now that we've done this work we can unregister this callback + SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; +} +``` + +### Adding the Multiplayer Functionality + +Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. + +**Append to the top of UIUsernameChooser.cs** + +```csharp +using SpacetimeDB.Types; +``` + +Then we're doing a modification to the `ButtonPressed()` function: + +**Modify the ButtonPressed function in UIUsernameChooser.cs** + +```csharp +public void ButtonPressed() +{ + CameraController.RemoveDisabler(GetHashCode()); + _panel.SetActive(false); + + // Call the SpacetimeDB CreatePlayer reducer + Reducer.CreatePlayer(_usernameField.text); +} +``` + +We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. + +First append this using to the top of `RemotePlayer.cs` + +**Create file RemotePlayer.cs, then replace its contents:** + +```csharp +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using SpacetimeDB.Types; +using TMPro; + +public class RemotePlayer : MonoBehaviour +{ + public ulong EntityId; + + public TMP_Text UsernameElement; + + public string Username { set { UsernameElement.text = value; } } + + void Start() + { + // Initialize overhead name + UsernameElement = GetComponentInChildren(); + var canvas = GetComponentInChildren(); + canvas.worldCamera = Camera.main; + + // Get the username from the PlayerComponent for this object and set it in the UI + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + if (playerComp is null) + { + string inputUsername = UsernameElement.Text; + Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); + Reducer.CreatePlayer(inputUsername); + + // Try again, optimistically assuming success for simplicity + PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); + } + + Username = playerComp.Username; + + // Get the last location for this player and set the initial position + EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); + transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + + // Register for a callback that is called when the client gets an + // update for a row in the EntityComponent table + EntityComponent.OnUpdate += EntityComponent_OnUpdate; + } +} +``` + +We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. + +**Append to bottom of RemotePlayer class in RemotePlayer.cs:** + +```csharp +private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) +{ + // If the update was made to this object + if(obj.EntityId == EntityId) + { + var movementController = GetComponent(); + + // Update target position, rotation, etc. + movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); + movementController.RemoteTargetRotation = obj.Direction; + movementController.SetMoving(obj.Moving); + } +} +``` + +Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. + +**Append to bottom of Start() function in TutorialGameManager.cs:** + +```csharp +PlayerComponent.OnInsert += PlayerComponent_OnInsert; +``` + +Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. + +**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** + +```csharp +private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) +{ + // If the identity of the PlayerComponent matches our user identity then this is the local player + if(obj.OwnerId == local_identity) + { + // Now that we have our initial position we can start the game + StartGame(); + } + else + { + // Spawn the player object and attach the RemotePlayer component + var remotePlayer = Instantiate(PlayerPrefab); + + // Lookup and apply the position for this new player + var entity = EntityComponent.FilterByEntityId(obj.EntityId); + var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + remotePlayer.transform.position = position; + + var movementController = remotePlayer.GetComponent(); + movementController.RemoteTargetPosition = position; + movementController.RemoteTargetRotation = entity.Direction; + + remotePlayer.AddComponent().EntityId = obj.EntityId; + } +} +``` + +Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. + +**Append to the top of LocalPlayer.cs** + +```csharp +using SpacetimeDB.Types; +using SpacetimeDB; +``` + +**Append to the bottom of LocalPlayer class in LocalPlayer.cs** + +```csharp +private float? lastUpdateTime; +private void FixedUpdate() +{ + float? deltaTime = Time.time - lastUpdateTime; + bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; + bool isConnected = SpacetimeDBClient.instance.IsConnected(); + + if (hasUpdatedRecently || !isConnected) + { + return; + } + + lastUpdateTime = Time.time; + var p = PlayerMovementController.Local.GetModelPosition(); + + Reducer.UpdatePlayerPosition(new StdbVector3 + { + X = p.x, + Y = p.y, + Z = p.z, + }, + PlayerMovementController.Local.GetModelRotation(), + PlayerMovementController.Local.IsMoving()); +} +``` + +Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. + +![GameManager-Inspector2](/images/unity-tutorial/GameManager-Inspector2.JPG) + +### Play the Game! + +Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. + +![Unity-AddOpenScenes](/images/unity-tutorial/Unity-AddOpenScenes.JPG) + +When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. + +### Implement Player Logout + +So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. + +**Append to the bottom of Start() function in TutorialGameManager.cs** +```csharp +PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; +``` + +We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. + +Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. + +**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** +```csharp +private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) +{ + OnPlayerComponentChanged(newValue); +} + +private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) +{ + OnPlayerComponentChanged(obj); +} + +private void OnPlayerComponentChanged(PlayerComponent obj) +{ + // If the identity of the PlayerComponent matches our user identity then this is the local player + if(obj.OwnerId == local_identity) + { + // Now that we have our initial position we can start the game + StartGame(); + } + else + { + // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it + var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); + if (obj.LoggedIn) + { + // Only spawn remote players who aren't already spawned + if (existingPlayer == null) + { + // Spawn the player object and attach the RemotePlayer component + var remotePlayer = Instantiate(PlayerPrefab); + + // Lookup and apply the position for this new player + var entity = EntityComponent.FilterByEntityId(obj.EntityId); + var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); + remotePlayer.transform.position = position; + + var movementController = remotePlayer.GetComponent(); + movementController.RemoteTargetPosition = position; + movementController.RemoteTargetRotation = entity.Direction; + + remotePlayer.AddComponent().EntityId = obj.EntityId; + } + } + else + { + if (existingPlayer != null) + { + Destroy(existingPlayer.gameObject); + } + } + } +} +``` + +Now you when you play the game you should see remote players disappear when they log out. + +Before updating the client, let's generate the client files and update publish our module. + +**Execute commands in the server/ directory** +```bash +spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp +spacetime publish -c unity-tutorial +``` + +On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. + +**Append to the top of UIChatController.cs:** +```csharp +using SpacetimeDB.Types; +``` + +**REPLACE the OnChatButtonPress function in UIChatController.cs:** + +```csharp +public void OnChatButtonPress() +{ + Reducer.SendChatMessage(_chatInput.text); + _chatInput.text = ""; +} +``` + +Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: + +**Append to the bottom of the Start() function in TutorialGameManager.cs:** +```csharp +Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; +``` + +Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. + +**Append after the Start() function in TutorialGameManager.cs** +```csharp +private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) +{ + var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); + if (player != null) + { + UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); + } +} +``` + +Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. + +## Conclusion + +This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: + +**Next Unity Tutorial:** [Resources & Scheduling](/docs/unity/part-4.md) + +--- + +### Troubleshooting + +- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` + +- If you get this exception when running the project: + +``` +NullReferenceException: Object reference not set to an instance of an object +TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) +``` + +Check to see if your GameManager object in the Scene has the NetworkManager component attached. + +- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. + +``` +Connection error: Unable to connect to the remote server +``` diff --git a/docs/unity/part-2.md b/docs/unity/part-2.md index 062041da..4f7fc496 100644 --- a/docs/unity/part-2.md +++ b/docs/unity/part-2.md @@ -1,6 +1,6 @@ # Unity Multiplayer Tutorial - Part 2 -# Analyzing the C# Server Module +## Analyzing the C# Server Module This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md). @@ -9,12 +9,17 @@ In this part of the tutorial, we will: 1. Learn core concepts of the C# server module. 2. Review limitations and common practices. 3. Breakdown high-level concepts like Types, Tables, and Reducers. -4. Breakdown the initialization reducer and chat support from the demo for real-use examples. +4. Breakdown the initialization reducer (entry point) and chat demo features. The server module will handle the game logic and data management for the game. 💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! +### Prerequisites + +This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md): +* You have already [setup your project](/docs/unity/index.md). + ## The Entity Component Systems (ECS) Before we continue to creating the server module, it's important to understand the basics of the ECS. diff --git a/docs/unity/part-2a-rust.md b/docs/unity/part-2a-rust.md deleted file mode 100644 index aa8f68d4..00000000 --- a/docs/unity/part-2a-rust.md +++ /dev/null @@ -1,316 +0,0 @@ -# Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (Rust) - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 1 Tutorial](/docs/unity/part-1.md) - -## Create a Server Module - -Run the following command to initialize the SpacetimeDB server module project with Rust as the language: - -```bash -spacetime init --lang=rust server -``` - -This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. - -### SpacetimeDB Tables - -In this section we'll be making some edits to the file `server/src/lib.rs`. We recommend you open up this file in an IDE like VSCode or RustRover. - -**Important: Open the `server/src/lib.rs` file and delete its contents. We will be writing it from scratch here.** - -First we need to add some imports at the top of the file. - -**Copy and paste into lib.rs:** - -```rust -use spacetimedb::{spacetimedb, Identity, SpacetimeType, ReducerContext}; -use log; -``` - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [Rust module reference](/docs/modules/rust). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.rs:** - -```rust -// We're using this table as a singleton, so there should typically only be one element where the version is 0. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct Config { - #[primarykey] - pub version: u32, - pub message_of_the_day: String, -} -``` - -Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `#[derive(SpacetimeType)]` and a `#[spacetimedb(table)]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. - -**Append to the bottom of lib.rs:** - -```rust -// This allows us to store 3D points in tables. -#[derive(SpacetimeType, Clone)] -pub struct StdbVector3 { - pub x: f32, - pub y: f32, - pub z: f32, -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```rust -// This stores information related to all entities in our game. In this tutorial -// all entities must at least have an entity_id, a position, a direction and they -// must specify whether or not they are moving. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct EntityComponent { - #[primarykey] - // The autoinc macro here just means every time we insert into this table - // we will receive a new row where this value will be increased by one. This - // allows us to easily get rows where `entity_id` is unique. - #[autoinc] - pub entity_id: u64, - pub position: StdbVector3, - pub direction: f32, - pub moving: bool, -} -``` - -Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `entity_id`. You'll see how this works later in the `create_player` reducer. - -**Append to the bottom of lib.rs:** - -```rust -// All players have this component and it associates an entity with the user's -// Identity. It also stores their username and whether or not they're logged in. -#[derive(Clone)] -#[spacetimedb(table)] -pub struct PlayerComponent { - // An entity_id that matches an entity_id in the `EntityComponent` table. - #[primarykey] - pub entity_id: u64, - - // The user's identity, which is unique to each player - #[unique] - pub owner_id: Identity, - pub username: String, - pub logged_in: bool, -} -``` - -Next, we write our very first reducer, `create_player`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.rs:** - -```rust -// This reducer is called when the user logs in for the first time and -// enters a username -#[spacetimedb(reducer)] -pub fn create_player(ctx: ReducerContext, username: String) -> Result<(), String> { - // Get the Identity of the client who called this reducer - let owner_id = ctx.sender; - - // Make sure we don't already have a player with this identity - if PlayerComponent::filter_by_owner_id(&owner_id).is_some() { - log::info!("Player already exists"); - return Err("Player already exists".to_string()); - } - - // Create a new entity for this player and get a unique `entity_id`. - let entity_id = EntityComponent::insert(EntityComponent - { - entity_id: 0, - position: StdbVector3 { x: 0.0, y: 0.0, z: 0.0 }, - direction: 0.0, - moving: false, - }).expect("Failed to create a unique PlayerComponent.").entity_id; - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - PlayerComponent::insert(PlayerComponent { - entity_id, - owner_id, - username: username.clone(), - logged_in: true, - }).expect("Failed to insert player component."); - - log::info!("Player created: {}({})", username, entity_id); - - Ok(()) -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `sender` value of the `ReducerContext`. -- `disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. - -**Append to the bottom of lib.rs:** - -```rust -// Called when the module is initially published -#[spacetimedb(init)] -pub fn init() { - Config::insert(Config { - version: 0, - message_of_the_day: "Hello, World!".to_string(), - }).expect("Failed to insert config."); -} -``` - -We use the `connect` and `disconnect` reducers to update the logged in state of the player. The `update_player_login_state` helper function looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `logged_in` variable and calls the auto-generated `update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.rs:** - -```rust -// Called when the client connects, we update the logged_in state to true -#[spacetimedb(connect)] -pub fn client_connected(ctx: ReducerContext) { - update_player_login_state(ctx, true); -} -``` -```rust -// Called when the client disconnects, we update the logged_in state to false -#[spacetimedb(disconnect)] -pub fn client_disconnected(ctx: ReducerContext) { - update_player_login_state(ctx, false); -} -``` -```rust -// This helper function gets the PlayerComponent, sets the logged -// in variable and updates the PlayerComponent table row. -pub fn update_player_login_state(ctx: ReducerContext, logged_in: bool) { - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - // We clone the PlayerComponent so we can edit it and pass it back. - let mut player = player.clone(); - player.logged_in = logged_in; - PlayerComponent::update_by_entity_id(&player.entity_id.clone(), player); - } -} -``` - -Our final reducer handles player movement. In `update_player_position` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `create_player` first. - -Using the `entity_id` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `update` function. - -**Append to the bottom of lib.rs:** - -```rust -// Updates the position of a player. This is also called when the player stops moving. -#[spacetimedb(reducer)] -pub fn update_player_position( - ctx: ReducerContext, - position: StdbVector3, - direction: f32, - moving: bool, -) -> Result<(), String> { - // First, look up the player using the sender identity, then use that - // entity_id to retrieve and update the EntityComponent - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - if let Some(mut entity) = EntityComponent::filter_by_entity_id(&player.entity_id) { - entity.position = position; - entity.direction = direction; - entity.moving = moving; - EntityComponent::update_by_entity_id(&player.entity_id, entity); - return Ok(()); - } - } - - // If we can not find the PlayerComponent or EntityComponent for - // this player then something went wrong. - return Err("Player not found".to_string()); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. - -### Finally, Add Chat Support - -The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.rs``. - -**Append to the bottom of server/src/lib.rs:** - -```rust -#[spacetimedb(table)] -pub struct ChatMessage { - // The primary key for this table will be auto-incremented - #[primarykey] - #[autoinc] - pub message_id: u64, - - // The entity id of the player that sent the message - pub sender_id: u64, - // Message contents - pub text: String, -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.rs:** - -```rust -// Adds a chat entry to the ChatMessage table -#[spacetimedb(reducer)] -pub fn send_chat_message(ctx: ReducerContext, text: String) -> Result<(), String> { - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - // Now that we have the player we can insert the chat message using the player entity id. - ChatMessage::insert(ChatMessage { - // this column auto-increments so we can set it to 0 - message_id: 0, - sender_id: player.entity_id, - text, - }) - .unwrap(); - - return Ok(()); - } - - Err("Player not found".into()) -} -``` - -## Wrapping Up - -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. - -From here, the tutorial continues with more-advanced topics. The [next tutorial](/docs/unity/part-4.md) introduces Resources & Scheduling. diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index 806cfbcf..e75968ac 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -1,69 +1,61 @@ -# Unity Tutorial - Basic Multiplayer - Part 3 - Client +# Unity Multiplayer Tutorial - Part 3 -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! +## Prerequisites -This progressive tutorial is continued from one of the Part 2 tutorials: -- [Rust Server Module](/docs/unity/part-2a-rust.md) -- [C# Server Module](/docs/unity/part-2) +This progressive tutorial is continued from [Part 2](/docs/unity/part-2.md): +1. You have already [setup your project](/docs/unity/index.md). +2. You have already [published your server module](/docs/unity/part-2.md). -## Updating our Unity Project Client to use SpacetimeDB +## Analyzing the Unity Client Demo -Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. +In this part of the tutorial, we will: -### Import the SDK and Generate Module Files +1. Explore the SpacetimeDB Unity SDK. +2. Setup your `GameManager` connection properties. +3. Inspect high-level client initialization. +4. Press Play -> Breakdown game features. -1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. +## SpacetimeDB Unity SDK -```bash -https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git -``` +TODO -![Unity-PackageManager](/images/unity-tutorial/Unity-PackageManager.JPG) +## GameManager Connection Setup -3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. +TODO -```bash -mkdir -p ../client/Assets/module_bindings -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -``` +## High-Level Client Initialization -### Connect to Your SpacetimeDB Module +TODO -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. +## Play the Game -![Unity-AddNetworkManager](/images/unity-tutorial/Unity-AddNetworkManager.JPG) +TODO -Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: +## Features Breakdown -**Append to the top of TutorialGameManager.cs** +### Feature: Chat -```csharp -using SpacetimeDB; -using SpacetimeDB.Types; -using System.Linq; -``` +TODO -At the top of the class definition add the following members: +### Feature: Resource Gathering -**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** +TODO -```csharp -// These are connection variables that are exposed on the GameManager -// inspector. -[SerializeField] private string moduleAddress = "unity-tutorial"; -[SerializeField] private string hostName = "localhost:3000"; - -// This is the identity for this player that is automatically generated -// the first time you log in. We set this variable when the -// onIdentityReceived callback is triggered by the SDK after connecting -private Identity local_identity; -``` +### Feature: Inventory + +TODO + +### Feature: Store + +TODO + +### Feature: Unlockables -The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. +TODO -Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. -**REPLACE the Start() function in TutorialGameManager.cs** +______________________ +# OLD >> TODO: MV || DELETE ```csharp // Start is called before the first frame update From faad08894bf563aec8964698cca1d6b68e97c64a Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 28 Mar 2024 17:26:10 +0800 Subject: [PATCH 18/24] feat(unity): Significant part 3 doc updates --- Writerside/c.list | 6 - Writerside/topics/bsatn.md | 115 -- .../topics/deploying/deploying_index.md | 48 - Writerside/topics/deploying/hosted.md | 74 -- Writerside/topics/deploying/self-hosted.md | 60 - Writerside/topics/getting-started.md | 34 - Writerside/topics/http/database.md | 589 -------- Writerside/topics/http/energy.md | 76 -- Writerside/topics/http/http_index.md | 51 - Writerside/topics/http/identity.md | 160 --- Writerside/topics/index.md | 120 -- .../topics/modules/c-sharp/c-sharp_index.md | 307 ----- .../topics/modules/c-sharp/quickstart.md | 312 ----- Writerside/topics/modules/modules_index.md | 30 - Writerside/topics/modules/rust/rust_index.md | 454 ------- .../topics/modules/rust/rust_quickstart.md | 272 ---- Writerside/topics/satn.md | 163 --- .../topics/sdks/c-sharp/c-sharp_quickstart.md | 438 ------ .../topics/sdks/c-sharp/sdks_c-sharp_index.md | 959 ------------- Writerside/topics/sdks/python/python_index.md | 552 -------- .../topics/sdks/python/python_quickstart.md | 379 ------ .../topics/sdks/rust/sdks_rust_index.md | 1183 ----------------- .../topics/sdks/rust/sdks_rust_quickstart.md | 487 ------- Writerside/topics/sdks/sdks_index.md | 74 -- .../sdks/typescript/typescript_index.md | 942 ------------- .../sdks/typescript/typescript_quickstart.md | 502 ------- Writerside/topics/sql/sql_index.md | 407 ------ Writerside/topics/unity/homeless.md | 355 ----- Writerside/topics/unity/part-1.md | 57 - Writerside/topics/unity/part-2.md | 483 ------- Writerside/topics/unity/part-2a-rust.md | 316 ----- Writerside/topics/unity/part-3.md | 479 ------- Writerside/topics/unity/part-4.md | 261 ---- Writerside/topics/unity/part-5.md | 108 -- Writerside/topics/unity/unity_index.md | 24 - .../webassembly-abi/webassembly-abi_index.md | 499 ------- Writerside/topics/ws/ws_index.md | 318 ----- Writerside/v.list | 5 - Writerside/writerside.cfg | 7 - Writerside2/c.list | 6 - Writerside2/cfg/buildprofiles.xml | 13 - Writerside2/s.tree | 72 - Writerside2/topics/bsatn.md | 115 -- .../topics/deploying/deploying_index.md | 48 - Writerside2/topics/deploying/hosted.md | 74 -- Writerside2/topics/deploying/self-hosted.md | 60 - Writerside2/topics/getting-started.md | 34 - Writerside2/topics/http/database.md | 589 -------- Writerside2/topics/http/energy.md | 76 -- Writerside2/topics/http/http_index.md | 51 - Writerside2/topics/http/identity.md | 160 --- Writerside2/topics/index.md | 120 -- .../topics/modules/c-sharp/c-sharp_index.md | 307 ----- .../topics/modules/c-sharp/quickstart.md | 312 ----- Writerside2/topics/modules/modules_index.md | 30 - Writerside2/topics/modules/rust/rust_index.md | 454 ------- .../topics/modules/rust/rust_quickstart.md | 272 ---- Writerside2/topics/satn.md | 163 --- .../topics/sdks/c-sharp/c-sharp_quickstart.md | 438 ------ .../topics/sdks/c-sharp/sdks_c-sharp_index.md | 959 ------------- .../topics/sdks/python/python_index.md | 552 -------- .../topics/sdks/python/python_quickstart.md | 379 ------ .../topics/sdks/rust/sdks_rust_index.md | 1183 ----------------- .../topics/sdks/rust/sdks_rust_quickstart.md | 487 ------- Writerside2/topics/sdks/sdks_index.md | 74 -- .../sdks/typescript/typescript_index.md | 942 ------------- .../sdks/typescript/typescript_quickstart.md | 502 ------- Writerside2/topics/sql/sql_index.md | 407 ------ Writerside2/topics/unity/homeless.md | 355 ----- Writerside2/topics/unity/part-1.md | 57 - Writerside2/topics/unity/part-2.md | 489 ------- Writerside2/topics/unity/part-2a-rust.md | 316 ----- Writerside2/topics/unity/part-3.md | 479 ------- Writerside2/topics/unity/part-4.md | 261 ---- Writerside2/topics/unity/part-5.md | 108 -- Writerside2/topics/unity/unity_index.md | 24 - .../webassembly-abi/webassembly-abi_index.md | 499 ------- Writerside2/topics/ws/ws_index.md | 318 ----- Writerside2/v.list | 5 - Writerside2/writerside.cfg | 8 - docs/unity/homeless.md | 5 + docs/unity/img.png | Bin 0 -> 19806 bytes docs/unity/img_1.png | Bin 0 -> 7218 bytes docs/unity/index.md | 9 +- docs/unity/part-1.md | 8 +- docs/unity/part-2.md | 11 +- docs/unity/part-3.md | 216 ++- 87 files changed, 220 insertions(+), 23533 deletions(-) delete mode 100644 Writerside/c.list delete mode 100644 Writerside/topics/bsatn.md delete mode 100644 Writerside/topics/deploying/deploying_index.md delete mode 100644 Writerside/topics/deploying/hosted.md delete mode 100644 Writerside/topics/deploying/self-hosted.md delete mode 100644 Writerside/topics/getting-started.md delete mode 100644 Writerside/topics/http/database.md delete mode 100644 Writerside/topics/http/energy.md delete mode 100644 Writerside/topics/http/http_index.md delete mode 100644 Writerside/topics/http/identity.md delete mode 100644 Writerside/topics/index.md delete mode 100644 Writerside/topics/modules/c-sharp/c-sharp_index.md delete mode 100644 Writerside/topics/modules/c-sharp/quickstart.md delete mode 100644 Writerside/topics/modules/modules_index.md delete mode 100644 Writerside/topics/modules/rust/rust_index.md delete mode 100644 Writerside/topics/modules/rust/rust_quickstart.md delete mode 100644 Writerside/topics/satn.md delete mode 100644 Writerside/topics/sdks/c-sharp/c-sharp_quickstart.md delete mode 100644 Writerside/topics/sdks/c-sharp/sdks_c-sharp_index.md delete mode 100644 Writerside/topics/sdks/python/python_index.md delete mode 100644 Writerside/topics/sdks/python/python_quickstart.md delete mode 100644 Writerside/topics/sdks/rust/sdks_rust_index.md delete mode 100644 Writerside/topics/sdks/rust/sdks_rust_quickstart.md delete mode 100644 Writerside/topics/sdks/sdks_index.md delete mode 100644 Writerside/topics/sdks/typescript/typescript_index.md delete mode 100644 Writerside/topics/sdks/typescript/typescript_quickstart.md delete mode 100644 Writerside/topics/sql/sql_index.md delete mode 100644 Writerside/topics/unity/homeless.md delete mode 100644 Writerside/topics/unity/part-1.md delete mode 100644 Writerside/topics/unity/part-2.md delete mode 100644 Writerside/topics/unity/part-2a-rust.md delete mode 100644 Writerside/topics/unity/part-3.md delete mode 100644 Writerside/topics/unity/part-4.md delete mode 100644 Writerside/topics/unity/part-5.md delete mode 100644 Writerside/topics/unity/unity_index.md delete mode 100644 Writerside/topics/webassembly-abi/webassembly-abi_index.md delete mode 100644 Writerside/topics/ws/ws_index.md delete mode 100644 Writerside/v.list delete mode 100644 Writerside/writerside.cfg delete mode 100644 Writerside2/c.list delete mode 100644 Writerside2/cfg/buildprofiles.xml delete mode 100644 Writerside2/s.tree delete mode 100644 Writerside2/topics/bsatn.md delete mode 100644 Writerside2/topics/deploying/deploying_index.md delete mode 100644 Writerside2/topics/deploying/hosted.md delete mode 100644 Writerside2/topics/deploying/self-hosted.md delete mode 100644 Writerside2/topics/getting-started.md delete mode 100644 Writerside2/topics/http/database.md delete mode 100644 Writerside2/topics/http/energy.md delete mode 100644 Writerside2/topics/http/http_index.md delete mode 100644 Writerside2/topics/http/identity.md delete mode 100644 Writerside2/topics/index.md delete mode 100644 Writerside2/topics/modules/c-sharp/c-sharp_index.md delete mode 100644 Writerside2/topics/modules/c-sharp/quickstart.md delete mode 100644 Writerside2/topics/modules/modules_index.md delete mode 100644 Writerside2/topics/modules/rust/rust_index.md delete mode 100644 Writerside2/topics/modules/rust/rust_quickstart.md delete mode 100644 Writerside2/topics/satn.md delete mode 100644 Writerside2/topics/sdks/c-sharp/c-sharp_quickstart.md delete mode 100644 Writerside2/topics/sdks/c-sharp/sdks_c-sharp_index.md delete mode 100644 Writerside2/topics/sdks/python/python_index.md delete mode 100644 Writerside2/topics/sdks/python/python_quickstart.md delete mode 100644 Writerside2/topics/sdks/rust/sdks_rust_index.md delete mode 100644 Writerside2/topics/sdks/rust/sdks_rust_quickstart.md delete mode 100644 Writerside2/topics/sdks/sdks_index.md delete mode 100644 Writerside2/topics/sdks/typescript/typescript_index.md delete mode 100644 Writerside2/topics/sdks/typescript/typescript_quickstart.md delete mode 100644 Writerside2/topics/sql/sql_index.md delete mode 100644 Writerside2/topics/unity/homeless.md delete mode 100644 Writerside2/topics/unity/part-1.md delete mode 100644 Writerside2/topics/unity/part-2.md delete mode 100644 Writerside2/topics/unity/part-2a-rust.md delete mode 100644 Writerside2/topics/unity/part-3.md delete mode 100644 Writerside2/topics/unity/part-4.md delete mode 100644 Writerside2/topics/unity/part-5.md delete mode 100644 Writerside2/topics/unity/unity_index.md delete mode 100644 Writerside2/topics/webassembly-abi/webassembly-abi_index.md delete mode 100644 Writerside2/topics/ws/ws_index.md delete mode 100644 Writerside2/v.list delete mode 100644 Writerside2/writerside.cfg create mode 100644 docs/unity/img.png create mode 100644 docs/unity/img_1.png diff --git a/Writerside/c.list b/Writerside/c.list deleted file mode 100644 index c4c77a29..00000000 --- a/Writerside/c.list +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Writerside/topics/bsatn.md b/Writerside/topics/bsatn.md deleted file mode 100644 index f8aeca7f..00000000 --- a/Writerside/topics/bsatn.md +++ /dev/null @@ -1,115 +0,0 @@ -# SATN Binary Format (BSATN) - -The Spacetime Algebraic Type Notation binary (BSATN) format defines -how Spacetime `AlgebraicValue`s and friends are encoded as byte strings. - -Algebraic values and product values are BSATN-encoded for e.g., -module-host communication and for storing row data in the database. - -## Notes on notation - -In this reference, we give a formal definition of the format. -To do this, we use inductive definitions, and define the following notation: - -- `bsatn(x)` denotes a function converting some value `x` to a list of bytes. -- `a: B` means that `a` is of type `B`. -- `Foo(x)` denotes extracting `x` out of some variant or type `Foo`. -- `a ++ b` denotes concatenating two byte lists `a` and `b`. -- `bsatn(A) = bsatn(B) | ... | bsatn(Z)` where `B` to `Z` are variants of `A` - means that `bsatn(A)` is defined as e.g., - `bsatn(B)`, `bsatn(C)`, .., `bsatn(Z)` depending on what variant of `A` it was. -- `[]` denotes the empty list of bytes. - -## Values - -### At a glance - -| Type | Description | -| ---------------- | ---------------------------------------------------------------- | -| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype.). | -| `SumValue` | A value whose type is a [`SumType`](#sumtype.). | -| `ProductValue` | A value whose type is a [`ProductType`](#producttype.). | -| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype.). | - -### `AlgebraicValue` - -The BSATN encoding of an `AlgebraicValue` defers to the encoding of each variant: - -```fsharp -bsatn(AlgebraicValue) = bsatn(SumValue) | bsatn(ProductValue) | bsatn(BuiltinValue) -``` - -### `SumValue` - -An instance of a [`SumType`](#sumtype.). -`SumValue`s are binary-encoded as `bsatn(tag) ++ bsatn(variant_data)` -where `tag: u8` is an index into the [`SumType.variants`](#sumtype.) -array of the value's [`SumType`](#sumtype.), -and where `variant_data` is the data of the variant. -For variants holding no data, i.e., of some zero sized type, -`bsatn(variant_data) = []`. - -### `ProductValue` - -An instance of a [`ProductType`](#producttype.). -`ProductValue`s are binary encoded as: - -```fsharp -bsatn(elems) = bsatn(elem_0) ++ .. ++ bsatn(elem_n) -``` - -Field names are not encoded. - -### `BuiltinValue` - -An instance of a [`BuiltinType`](#builtintype.). -The BSATN encoding of `BuiltinValue`s defers to the encoding of each variant: - -```fsharp -bsatn(BuiltinValue) - = bsatn(Bool) - | bsatn(U8) | bsatn(U16) | bsatn(U32) | bsatn(U64) | bsatn(U128) - | bsatn(I8) | bsatn(I16) | bsatn(I32) | bsatn(I64) | bsatn(I128) - | bsatn(F32) | bsatn(F64) - | bsatn(String) - | bsatn(Array) - | bsatn(Map) - -bsatn(Bool(b)) = bsatn(b as u8) -bsatn(U8(x)) = [x] -bsatn(U16(x: u16)) = to_little_endian_bytes(x) -bsatn(U32(x: u32)) = to_little_endian_bytes(x) -bsatn(U64(x: u64)) = to_little_endian_bytes(x) -bsatn(U128(x: u128)) = to_little_endian_bytes(x) -bsatn(I8(x: i8)) = to_little_endian_bytes(x) -bsatn(I16(x: i16)) = to_little_endian_bytes(x) -bsatn(I32(x: i32)) = to_little_endian_bytes(x) -bsatn(I64(x: i64)) = to_little_endian_bytes(x) -bsatn(I128(x: i128)) = to_little_endian_bytes(x) -bsatn(F32(x: f32)) = bsatn(f32_to_raw_bits(x)) // lossless conversion -bsatn(F64(x: f64)) = bsatn(f64_to_raw_bits(x)) // lossless conversion -bsatn(String(s)) = bsatn(len(s) as u32) ++ bsatn(bytes(s)) -bsatn(Array(a)) = bsatn(len(a) as u32) - ++ bsatn(normalize(a)_0) ++ .. ++ bsatn(normalize(a)_n) -bsatn(Map(map)) = bsatn(len(m) as u32) - ++ bsatn(key(map_0)) ++ bsatn(value(map_0)) - .. - ++ bsatn(key(map_n)) ++ bsatn(value(map_n)) -``` - -Where - -- `f32_to_raw_bits(x)` is the raw transmute of `x: f32` to `u32` -- `f64_to_raw_bits(x)` is the raw transmute of `x: f64` to `u64` -- `normalize(a)` for `a: ArrayValue` converts `a` to a list of `AlgebraicValue`s -- `key(map_i)` extracts the key of the `i`th entry of `map` -- `value(map_i)` extracts the value of the `i`th entry of `map` - -## Types - -All SATS types are BSATN-encoded by converting them to an `AlgebraicValue`, -then BSATN-encoding that meta-value. - -See [the SATN JSON Format](satn-reference-json-format.) -for more details of the conversion to meta values. -Note that these meta values are converted to BSATN and _not JSON_. diff --git a/Writerside/topics/deploying/deploying_index.md b/Writerside/topics/deploying/deploying_index.md deleted file mode 100644 index 658df48d..00000000 --- a/Writerside/topics/deploying/deploying_index.md +++ /dev/null @@ -1,48 +0,0 @@ -# Deploying Overview - -SpacetimeDB supports both hosted and self-hosted publishing in multiple ways. Below, we will: - -1. Generally introduce Identities. -1. Generally introduce Servers. -1Choose to proceed with either a [Hosted](hosted1.md) or [Self-Hosted](self-hosted1.md) deployment. - -💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. - -## About Identities - -An `Identity` is a hash attached to a `Nickname` and `Email`, allowing you to manage your app (such as `Publishing` your app). - -Each `Identity` is bound to one, single `Server`: Unlike GitHub, for example, you would require one identity per server. - -By default, there are no identities created. While the next tutorial will go more in-depth, you may create a new one via CLI: -```bash -spacetime identity new --name {Nickname} --email {Email} -``` - -See the verbose [overview identity explanation](https://spacetimedb.com/docs#identities), [API reference](identity1.md) or CLI help (below) for further managing `Identities`: -```bash -spacetime identity --help -``` - -## About Servers - -You `publish` your app to a target `Server` database: While we recommend to **host** your SpacetimeDB app with us for simplicity and scalability, you may also **self-host** (such as locally). - -By default, there are already two default servers added ([testnet](hosted1.md) and [local](self-hosted1.md)). While the next tutorial will go more in-depth, you may list your servers via CLI: -```bash -spacetime server list -``` - -See the [API reference](database1.md) or CLI help (below) for further managing `Servers`: -```bash -spacetime server --help -``` - ---- - -## Deploying via CLI - -Choose a server for your hosting tutorial path to set a server as default, create an identity, and deploy (`publish`) your app: - -1. [testnet](hosted1.md) (hosted) -2. [local](self-hosted1.md) (self-hosted) diff --git a/Writerside/topics/deploying/hosted.md b/Writerside/topics/deploying/hosted.md deleted file mode 100644 index 187eec4c..00000000 --- a/Writerside/topics/deploying/hosted.md +++ /dev/null @@ -1,74 +0,0 @@ -# Deploying - Hosted - -This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. Via CLI, we will then: - -1. Ensure our hosted server named `testnet` exists as the default. -1. Create an `Identity`. -1. `Publish` your app. - -💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI and that you already have `testnet` server added (exists by default). If you accidentally removed `testnet`, add it back via CLI: - -```bash -spacetime server add "https://testnet.spacetimedb.com" testnet -``` - -## SpacetimeDB Cloud (Hosted) Deployment - -Currently, for hosted deployment, only the `testnet` server is available for SpacetimeDB cloud, which is subject to wipes. - -📢 Stay tuned (such as [via Discord](https://discord.com/invite/SpacetimeDB)) for `mainnet` coming soon! - -## Set the Server Default - -To make CLI commands easier so that we don't need to keep specifying `testnet` as the target server, let's set it as default: - -```bash -spacetime server set-default testnet -``` - -## Creating an Identity - -By default, there are no identities created. Let's create a new one via CLI: -```bash -spacetime identity new --name {Nickname} --email {Email} -``` - -💡If you already created an identity but forgot to attach an email, add it via CLI: -```bash -spacetime identity set-email {Email} -``` - -## Create and Publish a Module - -Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: - -```bash -cd ~ -spacetime init --lang rust HelloSpacetimeDB -cd HelloSpacetimeDB -spacetime publish HelloSpacetimeDB -``` - -## Hosted Web Dashboard - -By earlier associating an email with your CLI identity, you can now view your published modules on the web dashboard. For multiple identities, first list them and copy the hash you want to use: - -```bash -spacetime identity list -``` - -1. Open the SpacetimeDB [login page](https://spacetimedb.com/login) using the same email above. -1. Choose your identity from the dropdown menu. - - \[For multiple identities\] `CTRL+F` to highlight the correct identity you copied earlier. -1. Check your email for a validation link. - -You should now be able to see your published modules on the web dashboard! - ---- - -## Summary - -- We ensured the hosted `testnet` server existed, then set it as the default. -- We added an `identity` to bind with our hosted `testnet` server, ensuring it contained both a Nickname and Email. -- We then logged in the web dashboard via an email `one-time password (OTP)` and were then able to view our published apps. -- With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment. diff --git a/Writerside/topics/deploying/self-hosted.md b/Writerside/topics/deploying/self-hosted.md deleted file mode 100644 index 9c47282f..00000000 --- a/Writerside/topics/deploying/self-hosted.md +++ /dev/null @@ -1,60 +0,0 @@ -# Deploying - Self-Hosted - -This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. Via CLI, we will then: - -1. Ensure our localhost server named `local` exists as the default. -1. Start our localhost server in a separate terminal window. -1. Create an `Identity` with at least a Nickname. -1. `Publish` your app. - -💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI and that you already have `local` server added (exists by default). If you accidentally removed `local`, add it back via CLI with the `--no-fingerprint` flag (since our server is not yet running): - -```bash -spacetime server add "http://127.0.0.1:3000" local --no-fingerprint -``` - -## Set the Server Default - -To make CLI commands easier so that we don't need to keep specifying `local` as the target server, let's set it as default: - -```bash -spacetime server set-default local -``` - -## Start the Local Server - -In a **separate** terminal window, start the local listen server in the foreground: -```bash -spacetime start -``` - -## Creating an Identity - -By default, there are no identities created. Let's create a new one via CLI: -```bash -spacetime identity new --name {Nickname} -``` - -💡We could optionally add `--email {Email}` to the above command, but is currently unnecessary for local deployment since there's no web dashboard. If you already created an identity but forgot to attach a Nickname, add it via CLI to easier identify your modules: -```bash -spacetime identity set-name {Nickname} -``` - -## Create and Publish a Module - -Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: - -```bash -cd ~ -spacetime init --lang rust HelloSpacetimeDB -cd HelloSpacetimeDB -spacetime publish HelloSpacetimeDB -``` - ---- - -## Summary - -- We ensured the self-hosted `local` server existed, then set it as the default. -- We then opened a separate terminal to run the self-hosted `local` server in the foreground. -- We added an `identity` to bind with our self-hosted `local` server set to default, ensuring it contained a Nickname. diff --git a/Writerside/topics/getting-started.md b/Writerside/topics/getting-started.md deleted file mode 100644 index 31e2fc90..00000000 --- a/Writerside/topics/getting-started.md +++ /dev/null @@ -1,34 +0,0 @@ -# Getting Started - -To develop SpacetimeDB applications locally, you will need to run the Standalone version of the server. - -1. [Install](install.) the SpacetimeDB CLI (Command Line Interface) -2. Run the start command: - -```bash -spacetime start -``` - -The server listens on port `3000` by default, customized via `--listen-addr`. - -💡 Standalone mode will run in the foreground. -⚠️ SSL is not supported in standalone mode. - -## What's Next? - -You are ready to start developing SpacetimeDB modules. See below for a quickstart guide for both client and server (module) languages/frameworks. - -### Server (Module) - -- [Rust](quickstart.) -- [C#](quickstart1.) - -⚡**Note:** Rust is [roughly 2x faster](https://faun.dev/c/links/faun/c-vs-rust-vs-go-a-performance-benchmarking-in-kubernetes/) than C# - -### Client - -- [Rust](quickstart2.) -- [C# (Standalone)](quickstart3.) -- [C# (Unity)](part-1.) -- [Typescript](quickstart4.) -- [Python](quickstart5.) \ No newline at end of file diff --git a/Writerside/topics/http/database.md b/Writerside/topics/http/database.md deleted file mode 100644 index 0e7fbe89..00000000 --- a/Writerside/topics/http/database.md +++ /dev/null @@ -1,589 +0,0 @@ -# `/database` HTTP API - -The HTTP endpoints in `/database` allow clients to interact with Spacetime databases in a variety of ways, including retrieving information, creating and deleting databases, invoking reducers and evaluating SQL queries. - -## At a glance - -| Route | Description | -| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -| [`/database/dns/:name GET`](#databasednsname-get.) | Look up a database's address by its name. | -| [`/database/reverse_dns/:address GET`](#databasereverse_dnsaddress-get.) | Look up a database's name by its address. | -| [`/database/set_name GET`](#databaseset_name-get.) | Set a database's name, given its address. | -| [`/database/ping GET`](#databaseping-get.) | No-op. Used to determine whether a client can connect. | -| [`/database/register_tld GET`](#databaseregister_tld-get.) | Register a top-level domain. | -| [`/database/request_recovery_code GET`](#databaserequest_recovery_code-get.) | Request a recovery code to the email associated with an identity. | -| [`/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](ws.). | -| [`/database/call/:name_or_address/:reducer POST`](#databasecallname_or_addressreducer-post.) | Invoke a reducer in a database. | -| [`/database/schema/:name_or_address GET`](#databaseschemaname_or_address-get.) | Get the schema for a database. | -| [`/database/schema/:name_or_address/:entity_type/:entity GET`](#databaseschemaname_or_addressentity_typeentity-get.) | Get a schema for a particular table or reducer. | -| [`/database/info/:name_or_address GET`](#databaseinfoname_or_address-get.) | Get a JSON description of a database. | -| [`/database/logs/:name_or_address GET`](#databaselogsname_or_address-get.) | Retrieve logs from a database. | -| [`/database/sql/:name_or_address POST`](#databasesqlname_or_address-post.) | Run a SQL query against a database. | - -## `/database/dns/:name GET` - -Look up a database's address by its name. - -Accessible through the CLI as `spacetime dns lookup `. - -#### Parameters - -| Name | Value | -| ------- | ------------------------- | -| `:name` | The name of the database. | - -#### Returns - -If a database with that name exists, returns JSON in the form: - -```typescript -{ "Success": { - "domain": string, - "address": string -} } -``` - -If no database with that name exists, returns JSON in the form: - -```typescript -{ "Failure": { - "domain": string -} } -``` - -## `/database/reverse_dns/:address GET` - -Look up a database's name by its address. - -Accessible through the CLI as `spacetime dns reverse-lookup
`. - -#### Parameters - -| Name | Value | -| ---------- | ---------------------------- | -| `:address` | The address of the database. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ "names": array } -``` - -where `` is a JSON array of strings, each of which is a name which refers to the database. - -## `/database/set_name GET` - -Set the name associated with a database. - -Accessible through the CLI as `spacetime dns set-name
`. - -#### Query Parameters - -| Name | Value | -| -------------- | ------------------------------------------------------------------------- | -| `address` | The address of the database to be named. | -| `domain` | The name to register. | -| `register_tld` | A boolean; whether to register the name as a TLD. Should usually be true. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -If the name was successfully set, returns JSON in the form: - -```typescript -{ "Success": { - "domain": string, - "address": string -} } -``` - -If the top-level domain is not registered, and `register_tld` was not specified, returns JSON in the form: - -```typescript -{ "TldNotRegistered": { - "domain": string -} } -``` - -If the top-level domain is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form: - -```typescript -{ "PermissionDenied": { - "domain": string -} } -``` - -> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. - -## `/database/ping GET` - -Does nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB. - -## `/database/register_tld GET` - -Register a new Spacetime top-level domain. A TLD is the part of a database name before the first `/`. For example, in the name `tyler/bitcraft`, the TLD is `tyler`. Each top-level domain is owned by at most one identity, and only the owner can publish databases with that TLD. - -> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. - -Accessible through the CLI as `spacetime dns register-tld `. - -#### Query Parameters - -| Name | Value | -| ----- | -------------------------------------- | -| `tld` | New top-level domain name to register. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -If the domain is successfully registered, returns JSON in the form: - -```typescript -{ "Success": { - "domain": string -} } -``` - -If the domain is already registered to the caller, returns JSON in the form: - -```typescript -{ "AlreadyRegistered": { - "domain": string -} } -``` - -If the domain is already registered to another identity, returns JSON in the form: - -```typescript -{ "Unauthorized": { - "domain": string -} } -``` - -## `/database/request_recovery_code GET` - -Request a recovery code or link via email, in order to recover the token associated with an identity. - -Accessible through the CLI as `spacetime identity recover `. - -#### Query Parameters - -| 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`](identity#identity-post.) or afterwards via [`/identity/:identity/set-email`](identity#identityidentityset_email-post.). | -| `link` | A boolean; whether to send a clickable link rather than a recovery code. | - -## `/database/confirm_recovery_code GET` - -Confirm a recovery code received via email following a [`/database/request_recovery_code GET`](#-database-request_recovery_code-get.) request, and retrieve the identity's token. - -Accessible through the CLI as `spacetime identity recover `. - -#### Query Parameters - -| Name | Value | -| ---------- | --------------------------------------------- | -| `identity` | The identity whose token should be recovered. | -| `email` | The email which received the recovery code. | -| `code` | The recovery code received via email. | - -On success, returns JSON in the form: - -```typescript -{ - "identity": string, - "token": string -} -``` - -## `/database/publish POST` - -Publish a database. - -Accessible through the CLI as `spacetime publish`. - -#### Query Parameters - -| Name | Value | -| ----------------- | ------------------------------------------------------------------------------------------------ | -| `host_type` | Optional; a SpacetimeDB module host type. Currently, only `"wasmer"` is supported. | -| `clear` | A boolean; whether to clear any existing data when updating an existing database. | -| `name_or_address` | The name of the database to publish or update, or the address of an existing database to update. | -| `register_tld` | A boolean; whether to register the database's top-level domain. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Data - -A WebAssembly module in the [binary format](https://webassembly.github.io/spec/core/binary/index.html). - -#### Returns - -If the database was successfully published, returns JSON in the form: - -```typescript -{ "Success": { - "domain": null | string, - "address": string, - "op": "created" | "updated" -} } -``` - -If the top-level domain for the requested name is not registered, returns JSON in the form: - -```typescript -{ "TldNotRegistered": { - "domain": string -} } -``` - -If the top-level domain for the requested name is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form: - -```typescript -{ "PermissionDenied": { - "domain": string -} } -``` - -> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. - -## `/database/delete/:address POST` - -Delete a database. - -Accessible through the CLI as `spacetime delete
`. - -#### Parameters - -| Name | Address | -| ---------- | ---------------------------- | -| `:address` | The address of the database. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -## `/database/subscribe/:name_or_address GET` - -Begin a [WebSocket connection](ws.) with a database. - -#### Parameters - -| Name | Value | -| ------------------ | ---------------------------- | -| `:name_or_address` | The address of the database. | - -#### Required Headers - -For more information about WebSocket headers, see [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). - -| Name | Value | -| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `Sec-WebSocket-Protocol` | [`v1.bin.spacetimedb`](ws#binary-protocol.) or [`v1.text.spacetimedb`](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. | - -#### Optional Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -## `/database/call/:name_or_address/:reducer POST` - -Invoke a reducer in a database. - -#### Parameters - -| Name | Value | -| ------------------ | ------------------------------------ | -| `:name_or_address` | The name or address of the database. | -| `:reducer` | The name of the reducer. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Data - -A JSON array of arguments to the reducer. - -## `/database/schema/:name_or_address GET` - -Get a schema for a database. - -Accessible through the CLI as `spacetime describe `. - -#### Parameters - -| Name | Value | -| ------------------ | ------------------------------------ | -| `:name_or_address` | The name or address of the database. | - -#### Query Parameters - -| Name | Value | -| -------- | ----------------------------------------------------------- | -| `expand` | A boolean; whether to include full schemas for each entity. | - -#### Returns - -Returns a JSON object with two properties, `"entities"` and `"typespace"`. For example, on the default module generated by `spacetime init` with `expand=true`, returns: - -```typescript -{ - "entities": { - "Person": { - "arity": 1, - "schema": { - "elements": [ - { - "algebraic_type": { - "Builtin": { - "String": [] - } - }, - "name": { - "some": "name" - } - } - ] - }, - "type": "table" - }, - "__init__": { - "arity": 0, - "schema": { - "elements": [], - "name": "__init__" - }, - "type": "reducer" - }, - "add": { - "arity": 1, - "schema": { - "elements": [ - { - "algebraic_type": { - "Builtin": { - "String": [] - } - }, - "name": { - "some": "name" - } - } - ], - "name": "add" - }, - "type": "reducer" - }, - "say_hello": { - "arity": 0, - "schema": { - "elements": [], - "name": "say_hello" - }, - "type": "reducer" - } - }, - "typespace": [ - { - "Product": { - "elements": [ - { - "algebraic_type": { - "Builtin": { - "String": [] - } - }, - "name": { - "some": "name" - } - } - ] - } - } - ] -} -``` - -The `"entities"` will be an object whose keys are table and reducer names, and whose values are objects of the form: - -```typescript -{ - "arity": number, - "type": "table" | "reducer", - "schema"?: ProductType -} -``` - -| Entity field | Value | -| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `arity` | For tables, the number of colums; for reducers, the number of arguments. | -| `type` | For tables, `"table"`; for reducers, `"reducer"`. | -| `schema` | A [JSON-encoded `ProductType`](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](satn.) referenced by the module. This can be used to resolve `Ref` types within the schema; the type `{ "Ref": n }` refers to `response["typespace"][n]`. - -## `/database/schema/:name_or_address/:entity_type/:entity GET` - -Get a schema for a particular table or reducer in a database. - -Accessible through the CLI as `spacetime describe `. - -#### Parameters - -| Name | Value | -| ------------------ | ---------------------------------------------------------------- | -| `:name_or_address` | The name or address of the database. | -| `:entity_type` | `reducer` to describe a reducer, or `table` to describe a table. | -| `:entity` | The name of the reducer or table. | - -#### Query Parameters - -| Name | Value | -| -------- | ------------------------------------------------------------- | -| `expand` | A boolean; whether to include the full schema for the entity. | - -#### Returns - -Returns a single entity in the same format as in the `"entities"` returned by [the `/database/schema/:name_or_address GET` endpoint](#databaseschemaname_or_address-get.): - -```typescript -{ - "arity": number, - "type": "table" | "reducer", - "schema"?: ProductType, -} -``` - -| Field | Value | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `arity` | For tables, the number of colums; for reducers, the number of arguments. | -| `type` | For tables, `"table"`; for reducers, `"reducer"`. | -| `schema` | A [JSON-encoded `ProductType`](satn.); for tables, the table schema; for reducers, the argument schema. Only present if `expand` is supplied and true. | - -## `/database/info/:name_or_address GET` - -Get a database's address, owner identity, host type, number of replicas and a hash of its WASM module. - -#### Parameters - -| Name | Value | -| ------------------ | ------------------------------------ | -| `:name_or_address` | The name or address of the database. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "address": string, - "identity": string, - "host_type": "wasmer", - "num_replicas": number, - "program_bytes_address": string -} -``` - -| Field | Type | Meaning | -| ------------------------- | ------ | ----------------------------------------------------------- | -| `"address"` | String | The address of the database. | -| `"identity"` | String | The Spacetime identity of the database's owner. | -| `"host_type"` | String | The module host type; currently always `"wasmer"`. | -| `"num_replicas"` | Number | The number of replicas of the database. Currently always 1. | -| `"program_bytes_address"` | String | Hash of the WASM module for the database. | - -## `/database/logs/:name_or_address GET` - -Retrieve logs from a database. - -Accessible through the CLI as `spacetime logs `. - -#### Parameters - -| Name | Value | -| ------------------ | ------------------------------------ | -| `:name_or_address` | The name or address of the database. | - -#### Query Parameters - -| Name | Value | -| ----------- | --------------------------------------------------------------- | -| `num_lines` | Number of most-recent log lines to retrieve. | -| `follow` | A boolean; whether to continue receiving new logs via a stream. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -Text, or streaming text if `follow` is supplied, containing log lines. - -## `/database/sql/:name_or_address POST` - -Run a SQL query against a database. - -Accessible through the CLI as `spacetime sql `. - -#### Parameters - -| Name | Value | -| ------------------ | --------------------------------------------- | -| `:name_or_address` | The name or address of the database to query. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Data - -SQL queries, separated by `;`. - -#### Returns - -Returns a JSON array of statement results, each of which takes the form: - -```typescript -{ - "schema": ProductType, - "rows": array -} -``` - -The `schema` will be a [JSON-encoded `ProductType`](satn.) describing the type of the returned rows. - -The `rows` will be an array of [JSON-encoded `ProductValue`s](satn.), each of which conforms to the `schema`. diff --git a/Writerside/topics/http/energy.md b/Writerside/topics/http/energy.md deleted file mode 100644 index fabecc30..00000000 --- a/Writerside/topics/http/energy.md +++ /dev/null @@ -1,76 +0,0 @@ -# `/energy` HTTP API - -The HTTP endpoints in `/energy` allow clients to query identities' energy balances. Spacetime databases expend energy from their owners' balances while executing reducers. - -## At a glance - -| Route | Description | -| ------------------------------------------------ | --------------------------------------------------------- | -| [`/energy/:identity GET`](#energyidentity-get.) | Get the remaining energy balance for the user `identity`. | -| [`/energy/:identity POST`](#energyidentity-post.) | Set the energy balance for the user `identity`. | - -## `/energy/:identity GET` - -Get the energy balance of an identity. - -Accessible through the CLI as `spacetime energy status `. - -#### Parameters - -| Name | Value | -| ----------- | ----------------------- | -| `:identity` | The Spacetime identity. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "balance": string -} -``` - -| Field | Value | -| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `balance` | The identity's energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. | - -## `/energy/:identity POST` - -Set the energy balance for an identity. - -Note that in the SpacetimeDB 0.6 Testnet, this endpoint always returns code 401, `UNAUTHORIZED`. Testnet energy balances cannot be refilled. - -Accessible through the CLI as `spacetime energy set-balance `. - -#### Parameters - -| Name | Value | -| ----------- | ----------------------- | -| `:identity` | The Spacetime identity. | - -#### Query Parameters - -| Name | Value | -| --------- | ------------------------------------------ | -| `balance` | A decimal integer; the new balance to set. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "balance": number -} -``` - -| Field | Value | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `balance` | The identity's new energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. | diff --git a/Writerside/topics/http/http_index.md b/Writerside/topics/http/http_index.md deleted file mode 100644 index e9a3d21e..00000000 --- a/Writerside/topics/http/http_index.md +++ /dev/null @@ -1,51 +0,0 @@ -# SpacetimeDB HTTP Authorization - -Rather than a password, each Spacetime identity is associated with a private token. These tokens are generated by SpacetimeDB when the corresponding identity is created, and cannot be changed. - -> Do not share your SpacetimeDB token with anyone, ever. - -### Generating identities and tokens - -Clients can request a new identity and token via [the `/identity POST` HTTP endpoint](identity#identity-post.). - -Alternately, a new identity and token will be generated during an anonymous connection via the [WebSocket API](ws.), and passed to the client as [an `IdentityToken` message](ws#identitytoken.). - -### Encoding `Authorization` headers - -Many SpacetimeDB HTTP endpoints either require or optionally accept a token in the `Authorization` header. SpacetimeDB authorization headers use `Basic` authorization with the username `token` and the token as the password. Because Spacetime tokens are not passwords, and SpacetimeDB Cloud uses TLS, usual security concerns about HTTP `Basic` authorization do not apply. - -To construct an appropriate `Authorization` header value for a `token`: - -1. Prepend the string `token:`. -2. Base64-encode. -3. Prepend the string `Basic `. - -#### Python - -```python -def auth_header_value(token): - username_and_password = f"token:{token}".encode("utf-8") - base64_encoded = base64.b64encode(username_and_password).decode("utf-8") - return f"Basic {base64_encoded}" -``` - -#### Rust - -```rust -fn auth_header_value(token: &str) -> String { - let username_and_password = format!("token:{}", token); - let base64_encoded = base64::prelude::BASE64_STANDARD.encode(username_and_password); - format!("Basic {}", encoded) -} -``` - -#### C# - -```csharp -public string AuthHeaderValue(string token) -{ - var username_and_password = Encoding.UTF8.GetBytes($"token:{auth}"); - var base64_encoded = Convert.ToBase64String(username_and_password); - return "Basic " + base64_encoded; -} -``` diff --git a/Writerside/topics/http/identity.md b/Writerside/topics/http/identity.md deleted file mode 100644 index 544d5d11..00000000 --- a/Writerside/topics/http/identity.md +++ /dev/null @@ -1,160 +0,0 @@ -# `/identity` HTTP API - -The HTTP endpoints in `/identity` allow clients to generate and manage Spacetime public identities and private tokens. - -## At a glance - -| Route | Description | -| ----------------------------------------------------------------------- | ------------------------------------------------------------------ | -| [`/identity GET`](#identity-get.) | Look up an identity by email. | -| [`/identity POST`](#identity-post.) | Generate a new identity and token. | -| [`/identity/websocket_token POST`](#identitywebsocket_token-post.) | Generate a short-lived access token for use in untrusted contexts. | -| [`/identity/:identity/set-email POST`](#identityidentityset-email-post.) | Set the email for an identity. | -| [`/identity/:identity/databases GET`](#identityidentitydatabases-get.) | List databases owned by an identity. | -| [`/identity/:identity/verify GET`](#identityidentityverify-get.) | Verify an identity and token. | - -## `/identity GET` - -Look up Spacetime identities associated with an email. - -Accessible through the CLI as `spacetime identity find `. - -#### Query Parameters - -| Name | Value | -| ------- | ------------------------------- | -| `email` | An email address to search for. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "identities": [ - { - "identity": string, - "email": string - } - ] -} -``` - -The `identities` value is an array of zero or more objects, each of which has an `identity` and an `email`. Each `email` will be the same as the email passed as a query parameter. - -## `/identity POST` - -Create a new identity. - -Accessible through the CLI as `spacetime identity new`. - -#### Query Parameters - -| Name | Value | -| ------- | ----------------------------------------------------------------------------------------------------------------------- | -| `email` | An email address to associate with the new identity. If unsupplied, the new identity will not have an associated email. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "identity": string, - "token": string -} -``` - -## `/identity/websocket_token POST` - -Generate a short-lived access token which can be used in untrusted contexts, e.g. embedded in URLs. - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "token": string -} -``` - -The `token` value is a short-lived [JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519). - -## `/identity/:identity/set-email POST` - -Associate an email with a Spacetime identity. - -Accessible through the CLI as `spacetime identity set-email `. - -#### Parameters - -| Name | Value | -| ----------- | ----------------------------------------- | -| `:identity` | The identity to associate with the email. | - -#### Query Parameters - -| Name | Value | -| ------- | ----------------- | -| `email` | An email address. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -## `/identity/:identity/databases GET` - -List all databases owned by an identity. - -#### Parameters - -| Name | Value | -| ----------- | --------------------- | -| `:identity` | A Spacetime identity. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "addresses": array -} -``` - -The `addresses` value is an array of zero or more strings, each of which is the address of a database owned by the identity passed as a parameter. - -## `/identity/:identity/verify GET` - -Verify the validity of an identity/token pair. - -#### Parameters - -| Name | Value | -| ----------- | ----------------------- | -| `:identity` | The identity to verify. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -Returns no data. - -If the token is valid and matches the identity, returns `204 No Content`. - -If the token is valid but does not match the identity, returns `400 Bad Request`. - -If the token is invalid, or no `Authorization` header is included in the request, returns `401 Unauthorized`. diff --git a/Writerside/topics/index.md b/Writerside/topics/index.md deleted file mode 100644 index 8426e256..00000000 --- a/Writerside/topics/index.md +++ /dev/null @@ -1,120 +0,0 @@ -# SpacetimeDB Documentation - -## Installation - -You can run SpacetimeDB as a standalone database server via the `spacetime` CLI tool. - -You can find the instructions to install the CLI tool for your platform [here](install.). - - - -To get started running your own standalone instance of SpacetimeDB check out our [Getting Started Guide](getting-started.). - - - -## What is SpacetimeDB? - -You can think of SpacetimeDB as a database that is also a server. - -It is a relational database system that lets you upload your application logic directly into the database by way of very fancy stored procedures called "modules". - -Instead of deploying a web or game server that sits in between your clients and your database, your clients connect directly to the database and execute your application logic inside the database itself. You can write all of your permission and authorization logic right inside your module just as you would in a normal server. - -This means that you can write your entire application in a single language, Rust, and deploy it as a single binary. No more microservices, no more containers, no more Kubernetes, no more Docker, no more VMs, no more DevOps, no more infrastructure, no more ops, no more servers. - -
- SpacetimeDB Architecture -
- SpacetimeDB application architecture - (elements in white are provided by SpacetimeDB) -
-
- -It's actually similar to the idea of smart contracts, except that SpacetimeDB is a database, has nothing to do with blockchain, and it's a lot faster than any smart contract system. - -So fast, in fact, that the entire backend our MMORPG [BitCraft Online](https://bitcraftonline.com) is just a SpacetimeDB module. We don't have any other servers or services running, which means that everything in the game, all of the chat messages, items, resources, terrain, and even the locations of the players are stored and processed by the database before being synchronized out to all of the clients in real-time. - -SpacetimeDB is optimized for maximum speed and minimum latency rather than batch processing or OLAP workloads. It is designed to be used for real-time applications like games, chat, and collaboration tools. - -This speed and latency is achieved by holding all of application state in memory, while persisting the data in a write-ahead-log (WAL) which is used to recover application state. - -## State Synchronization - -SpacetimeDB syncs client and server state for you so that you can just write your application as though you're accessing the database locally. No more messing with sockets for a week before actually writing your game. - -## Identities - -A SpacetimeDB `Identity` is a unique identifier that is used to authenticate and authorize access to the database. Importantly, while it represents who someone is, does NOT represent what they can do. Your application's logic will determine what a given identity is able to do by allowing or disallowing a transaction based on the caller's `Identity` along with any module-defined data and logic. - -SpacetimeDB associates each user with a 256-bit (32-byte) integer `Identity`. These identities are usually formatted as 64-digit hexadecimal strings. Identities are public information, and applications can use them to identify users. Identities are a global resource, so a user can use the same identity with multiple applications, so long as they're hosted by the same SpacetimeDB instance. - -Each identity has a corresponding authentication token. The authentication token is private, and should never be shared with anyone. Specifically, authentication tokens are [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519) signed by a secret unique to the SpacetimeDB instance. - -Additionally, each database has an owner `Identity`. Many database maintenance operations, like publishing a new version or evaluating arbitrary SQL queries, are restricted to only authenticated connections by the owner. - -SpacetimeDB provides tools in the CLI and the [client SDKs](sdks.) for managing credentials. - -## Addresses - -A SpacetimeDB `Address` is an opaque identifier for a database or a client connection. An `Address` is a 128-bit integer, usually formatted as a 32-character (16-byte) hexadecimal string. - -Each SpacetimeDB database has an `Address`, generated by the SpacetimeDB host, which can be used to connect to the database or to request information about it. Databases may also have human-readable names, which are mapped to addresses internally. - -Each client connection has an `Address`. These addresses are opaque, and do not correspond to any metadata about the client. They are notably not IP addresses or device identifiers. A client connection can be uniquely identified by its `(Identity, Address)` pair, but client addresses may not be globally unique; it is possible for multiple connections with the same `Address` but different identities to co-exist. SpacetimeDB modules should treat `Identity` as differentiating users, and `Address` as differentiating connections by the same user. - -## Language Support - -### Server-side Libraries - -Currently, Rust is the best-supported language for writing SpacetimeDB modules. Support for lots of other languages is in the works! - -- [Rust](rust.) - [(Quickstart)](quickstart.) -- [C#](c-sharp.) - [(Quickstart)](quickstart1.) -- Python (Coming soon) -- C# (Coming soon) -- Typescript (Coming soon) -- C++ (Planned) -- Lua (Planned) - -### Client-side SDKs - -- [Rust](rust1.) - [(Quickstart)](quickstart2.) -- [C#](c-sharp1.) - [(Quickstart)](quickstart3.) -- [TypeScript](typescript.) - [(Quickstart)](quickstart4.) -- [Python](python.) - [(Quickstart)](quickstart5.) -- C++ (Planned) -- Lua (Planned) - -### Unity - -SpacetimeDB was designed first and foremost as the backend for multiplayer Unity games. To learn more about using SpacetimeDB with Unity, jump on over to the [SpacetimeDB Unity Tutorial](part-1.). - -## FAQ - -1. What is SpacetimeDB? - It's a whole cloud platform within a database that's fast enough to run real-time games. - -1. How do I use SpacetimeDB? - Install the `spacetime` command line tool, choose your favorite language, import the SpacetimeDB library, write your application, compile it to WebAssembly, and upload it to the SpacetimeDB cloud platform. Once it's uploaded you can call functions directly on your application and subscribe to changes in application state. - -1. How do I get/install SpacetimeDB? - Just install our command line tool and then upload your application to the cloud. - -4. How do I create a new database with SpacetimeDB? - Follow our [Quick Start](getting-started.) guide! - -TL;DR in an empty directory, init and publish a barebones app named HelloWorld. - -```bash -spacetime init --lang=rust -spacetime publish HelloWorld -``` - -5. How do I create a Unity game with SpacetimeDB? - Follow our [Unity Project](unity-project.) guide! - -TL;DR after already initializing and publishing (see FAQ #5), generate the SDK: - -```bash -spacetime generate --out-dir --lang=csharp -``` diff --git a/Writerside/topics/modules/c-sharp/c-sharp_index.md b/Writerside/topics/modules/c-sharp/c-sharp_index.md deleted file mode 100644 index 31ebd1d4..00000000 --- a/Writerside/topics/modules/c-sharp/c-sharp_index.md +++ /dev/null @@ -1,307 +0,0 @@ -# SpacetimeDB C# Modules - -You can use the [C# SpacetimeDB library](https://github.com/clockworklabs/SpacetimeDBLibCSharp) to write modules in C# which interact with the SpacetimeDB database. - -It uses [Roslyn incremental generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) to add extra static methods to types, tables and reducers marked with special attributes and registers them with the database runtime. - -## Example - -Let's start with a heavily commented version of the default example from the landing page: - -```csharp -// These imports bring into the scope common APIs you'll need to expose items from your module and to interact with the database runtime. -using SpacetimeDB.Module; -using static SpacetimeDB.Runtime; - -// Roslyn generators are statically generating extra code as-if they were part of the source tree, so, -// in order to inject new methods, types they operate on as well as their parents have to be marked as `partial`. -// -// We start with the top-level `Module` class for the module itself. -static partial class Module -{ - // `[SpacetimeDB.Table]` registers a struct or a class as a SpacetimeDB table. - // - // It generates methods to insert, filter, update, and delete rows of the given type in the table. - [SpacetimeDB.Table] - public partial struct Person - { - // `[SpacetimeDB.Column]` allows to specify column attributes / constraints such as - // "this field should be unique" or "this field should get automatically assigned auto-incremented value". - [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)] - public int Id; - public string Name; - public int Age; - } - - // `[SpacetimeDB.Reducer]` marks a static method as a SpacetimeDB reducer. - // - // Reducers are functions that can be invoked from the database runtime. - // They can't return values, but can throw errors that will be caught and reported back to the runtime. - [SpacetimeDB.Reducer] - public static void Add(string name, int age) - { - // We can skip (or explicitly set to zero) auto-incremented fields when creating new rows. - var person = new Person { Name = name, Age = age }; - - // `Insert()` method is auto-generated and will insert the given row into the table. - person.Insert(); - // After insertion, the auto-incremented fields will be populated with their actual values. - // - // `Log()` function is provided by the runtime and will print the message to the database log. - // It should be used instead of `Console.WriteLine()` or similar functions. - Log($"Inserted {person.Name} under #{person.Id}"); - } - - [SpacetimeDB.Reducer] - public static void SayHello() - { - // Each table type gets a static Iter() method that can be used to iterate over the entire table. - foreach (var person in Person.Iter()) - { - Log($"Hello, {person.Name}!"); - } - Log("Hello, World!"); - } -} -``` - -## API reference - -Now we'll get into details on all the APIs SpacetimeDB provides for writing modules in C#. - -### Logging - -First of all, logging as we're likely going to use it a lot for debugging and reporting errors. - -`SpacetimeDB.Runtime` provides a `Log` function that will print the given message to the database log, along with the source location and a log level it was provided. - -Supported log levels are provided by the `LogLevel` enum: - -```csharp -public enum LogLevel -{ - Error, - Warn, - Info, - Debug, - Trace, - Panic -} -``` - -If omitted, the log level will default to `Info`, so these two forms are equivalent: - -```csharp -Log("Hello, World!"); -Log("Hello, World!", LogLevel.Info); -``` - -### Supported types - -#### Built-in types - -The following types are supported out of the box and can be stored in the database tables directly or as part of more complex types: - -- `bool` -- `byte`, `sbyte` -- `short`, `ushort` -- `int`, `uint` -- `long`, `ulong` -- `float`, `double` -- `string` -- [`Int128`](https://learn.microsoft.com/en-us/dotnet/api/system.int128), [`UInt128`](https://learn.microsoft.com/en-us/dotnet/api/system.uint128) -- `T[]` - arrays of supported values. -- [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1) -- [`Dictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2) - -And a couple of special custom types: - -- `SpacetimeDB.SATS.Unit` - semantically equivalent to an empty struct, sometimes useful in generic contexts where C# doesn't permit `void`. -- `Identity` (`SpacetimeDB.Runtime.Identity`) - a unique identifier for each user; internally a byte blob but can be printed, hashed and compared for equality. -- `Address` (`SpacetimeDB.Runtime.Address`) - an identifier which disamgibuates connections by the same `Identity`; internally a byte blob but can be printed, hashed and compared for equality. - - -#### 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. - -Any `struct` or `class` marked with this attribute, as well as their respective parents, must be `partial`, as the code generator will add methods to them. - -```csharp -[SpacetimeDB.Type] -public partial struct Point -{ - public int x; - public int y; -} -``` - -`enum`s marked with this attribute must not use custom discriminants, as the runtime expects them to be always consecutive starting from zero. Unlike structs and classes, they don't use `partial` as C# doesn't allow to add methods to `enum`s. - -```csharp -[SpacetimeDB.Type] -public enum Color -{ - Red, - Green, - Blue, -} -``` - -#### Tagged enums - -SpacetimeDB has support for tagged enums which can be found in languages like Rust, but not C#. - -To bridge the gap, a special marker interface `SpacetimeDB.TaggedEnum` can be used on any `SpacetimeDB.Type`-marked `struct` or `class` to mark it as a SpacetimeDB tagged enum. It accepts a tuple of 2 or more named items and will generate methods to check which variant is currently active, as well as accessors for each variant. - -It is expected that you will use the `Is*` methods to check which variant is active before accessing the corresponding field, as the accessor will throw an exception on a state mismatch. - -```csharp -// Example declaration: -[SpacetimeDB.Type] -partial struct Option : SpacetimeDB.TaggedEnum<(T Some, Unit None)> { } - -// Usage: -var option = new Option { Some = 42 }; -if (option.IsSome) -{ - Log($"Value: {option.Some}"); -} -``` - -### Tables - -`[SpacetimeDB.Table]` attribute can be used on any `struct` or `class` to mark it as a SpacetimeDB table. It will register a table in the database with the given name and fields as well as will generate C# methods to insert, filter, update, and delete rows of the given type. - -It implies `[SpacetimeDB.Type]`, so you must not specify both attributes on the same type. - -```csharp -[SpacetimeDB.Table] -public partial struct Person -{ - [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)] - public int Id; - public string Name; - public int Age; -} -``` - -The example above will generate the following extra methods: - -```csharp -public partial struct Person -{ - // Inserts current instance as a new row into the table. - public void Insert(); - - // Returns an iterator over all rows in the table, e.g.: - // `for (var person in Person.Iter()) { ... }` - public static IEnumerable Iter(); - - // Returns an iterator over all rows in the table that match the given filter, e.g.: - // `for (var person in Person.Query(p => p.Age >= 18)) { ... }` - public static IEnumerable Query(Expression> filter); - - // Generated for each column: - - // Returns an iterator over all rows in the table that have the given value in the `Name` column. - public static IEnumerable FilterByName(string name); - public static IEnumerable FilterByAge(int age); - - // Generated for each unique column: - - // 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); -} -``` - -#### Column attributes - -Attribute `[SpacetimeDB.Column]` can be used on any field of a `SpacetimeDB.Table`-marked `struct` or `class` to customize column attributes as seen above. - -The supported column attributes are: - -- `ColumnAttrs.AutoInc` - this column should be auto-incremented. -- `ColumnAttrs.Unique` - this column should be unique. -- `ColumnAttrs.PrimaryKey` - this column should be a primary key, it implies `ColumnAttrs.Unique` but also allows clients to subscribe to updates via `OnUpdate` which will use this field to match the old and the new version of the row with each other. - -These attributes are bitflags and can be combined together, but you can also use some predefined shortcut aliases: - -- `ColumnAttrs.Identity` - same as `ColumnAttrs.Unique | ColumnAttrs.AutoInc`. -- `ColumnAttrs.PrimaryKeyAuto` - same as `ColumnAttrs.PrimaryKey | ColumnAttrs.AutoInc`. - -### Reducers - -Attribute `[SpacetimeDB.Reducer]` can be used on any `static void` method to register it as a SpacetimeDB reducer. The method must accept only supported types as arguments. If it throws an exception, those will be caught and reported back to the database runtime. - -```csharp -[SpacetimeDB.Reducer] -public static void Add(string name, int age) -{ - var person = new Person { Name = name, Age = age }; - person.Insert(); - Log($"Inserted {person.Name} under #{person.Id}"); -} -``` - -If a reducer has an argument with a type `DbEventArgs` (`SpacetimeDB.Runtime.DbEventArgs`), it will be provided with event details such as the sender identity (`SpacetimeDB.Runtime.Identity`), sender address (`SpacetimeDB.Runtime.Address?`) and the time (`DateTimeOffset`) of the invocation: - -```csharp -[SpacetimeDB.Reducer] -public static void PrintInfo(DbEventArgs e) -{ - Log($"Sender identity: {e.Sender}"); - Log($"Sender address: {e.Address}"); - Log($"Time: {e.Time}"); -} -``` - -`[SpacetimeDB.Reducer]` also generates a function to schedule the given reducer in the future. - -Since it's not possible to generate extension methods on existing methods, the codegen will instead add a `Schedule`-prefixed method colocated in the same namespace as the original method instead. The generated method will accept `DateTimeOffset` argument for the time when the reducer should be invoked, followed by all the arguments of the reducer itself, except those that have type `DbEventArgs`. - -```csharp -// Example reducer: -[SpacetimeDB.Reducer] -public static void Add(string name, int age) { ... } - -// Auto-generated by the codegen: -public static void ScheduleAdd(DateTimeOffset time, string name, int age) { ... } - -// Usage from another reducer: -[SpacetimeDB.Reducer] -public static void AddIn5Minutes(DbEventArgs e, string name, int age) -{ - // Note that we're using `e.Time` instead of `DateTimeOffset.Now` which is not allowed in modules. - var scheduleToken = ScheduleAdd(e.Time.AddMinutes(5), name, age); - - // We can cancel the scheduled reducer by calling `Cancel()` on the returned token. - scheduleToken.Cancel(); -} -``` - -#### Special reducers - -These are two special kinds of reducers that can be used to respond to module lifecycle events. They're stored in the `SpacetimeDB.Module.ReducerKind` class and can be used as an argument to the `[SpacetimeDB.Reducer]` attribute: - -- `ReducerKind.Init` - this reducer will be invoked when the module is first published. -- `ReducerKind.Update` - this reducer will be invoked when the module is updated. -- `ReducerKind.Connect` - this reducer will be invoked when a client connects to the database. -- `ReducerKind.Disconnect` - this reducer will be invoked when a client disconnects from the database. - - -Example: - -```csharp -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void Init() -{ - Log("...and we're live!"); -} -``` diff --git a/Writerside/topics/modules/c-sharp/quickstart.md b/Writerside/topics/modules/c-sharp/quickstart.md deleted file mode 100644 index fedd7851..00000000 --- a/Writerside/topics/modules/c-sharp/quickstart.md +++ /dev/null @@ -1,312 +0,0 @@ -# C# Module Quickstart - -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. - -Each SpacetimeDB module defines a set of tables and a set of reducers. - -Each table is defined as a C# `class` annotated with `[SpacetimeDB.Table]`, where an instance represents a row, and each field represents a column. - -A reducer is a function which traverses and updates the database. Each reducer call runs in its own transaction, and its updates to the database are only committed if the reducer returns successfully. In C#, reducers are defined as functions annotated with `[SpacetimeDB.Reducer]`. If an exception is thrown, the reducer call fails, the database is not updated, and a failed message is reported to the client. - -## Install SpacetimeDB - -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. - -## Install .NET 8 - -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 -``` - -.NET 8.0 is the earliest to have the `wasi-experimental` workload that we rely on, but requires manual activation: - -```bash -dotnet workload install wasi-experimental -``` - -## Project structure - -Create and enter a directory `quickstart-chat`: - -```bash -mkdir quickstart-chat -cd quickstart-chat -``` - -Now create `server`, our module, which runs in the database: - -```bash -spacetime init --lang csharp server -``` - -## Declare imports - -`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. -3. Clear it out, so we can write a new module that's still pretty simple: a bare-bones chat server. - -To the top of `server/Lib.cs`, add some imports we'll be using: - -```csharp -using System.Runtime.CompilerServices; -using SpacetimeDB.Module; -using static SpacetimeDB.Runtime; -``` - -- `System.Runtime.CompilerServices` -- `SpacetimeDB.Module` - - Contains the special attributes we'll use to define our module. - - Allows us to use the `ModuleInitializer` attribute, which we'll use to register our `OnConnect` and `OnDisconnect` callbacks. -- `SpacetimeDB.Runtime` contains the raw API bindings SpacetimeDB uses to communicate with the database. - -We also need to create our static module class which all of the module code will live in. In `server/Lib.cs`, add: - -```csharp -static partial class Module -{ -} -``` - -## Define tables - -To get our chat server running, we'll need to store two kinds of data: information about each user, and records of all the messages that have been sent. - -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 -[SpacetimeDB.Table] -public partial class User -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public Identity Identity; - public string? Name; - public bool Online; -} -``` - -For each `Message`, we'll store the `Identity` of the user who sent it, the `Timestamp` when it was sent, and the text of the message. - -In `server/Lib.cs`, add the definition of the table `Message` to the `Module` class: - -```csharp -[SpacetimeDB.Table] -public partial class Message -{ - public Identity Sender; - public long Sent; - public string Text = ""; -} -``` - -## Set users' names - -We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `SetName` which clients can invoke to set their `User.Name`. It will validate the caller's chosen name, using a function `ValidateName` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail. - -Each reducer may accept as its first argument a `DbEventArgs`, 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`, `dbEvent.Sender`. - -It's also possible to call `SetName` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though. - -In `server/Lib.cs`, add to the `Module` class: - -```csharp -[SpacetimeDB.Reducer] -public static void SetName(DbEventArgs dbEvent, string name) -{ - name = ValidateName(name); - - var user = User.FindByIdentity(dbEvent.Sender); - if (user is not null) - { - user.Name = name; - User.UpdateByIdentity(dbEvent.Sender, user); - } -} -``` - -For now, we'll just do a bare minimum of validation, rejecting the empty name. You could extend this in various ways, like: - -- Comparing against a blacklist for moderation purposes. -- Unicode-normalizing names. -- Rejecting names that contain non-printable characters, or removing characters or replacing them with a placeholder. -- Rejecting or truncating long names. -- Rejecting duplicate names. - -In `server/Lib.cs`, add to the `Module` class: - -```csharp -/// Takes a name and checks if it's acceptable as a user's name. -public static string ValidateName(string name) -{ - if (string.IsNullOrEmpty(name)) - { - throw new Exception("Names must not be empty"); - } - return name; -} -``` - -## Send messages - -We define a reducer `SendMessage`, 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 `Time` timestamp taken from the `DbEventArgs`. - -In `server/Lib.cs`, add to the `Module` class: - -```csharp -[SpacetimeDB.Reducer] -public static void SendMessage(DbEventArgs dbEvent, string text) -{ - text = ValidateMessage(text); - Log(text); - new Message - { - Sender = dbEvent.Sender, - Text = text, - Sent = dbEvent.Time.ToUnixTimeMilliseconds(), - }.Insert(); -} -``` - -We'll want to validate messages' texts in much the same way we validate users' chosen names. As above, we'll do the bare minimum, rejecting only empty messages. - -In `server/Lib.cs`, add to the `Module` class: - -```csharp -/// Takes a message's text and checks if it's acceptable to send. -public static string ValidateMessage(string text) -{ - if (string.IsNullOrEmpty(text)) - { - throw new ArgumentException("Messages must not be empty"); - } - return text; -} -``` - -You could extend the validation in `ValidateMessage` in similar ways to `ValidateName`, or add additional checks to `SendMessage`, like: - -- Rejecting messages from senders who haven't set their names. -- Rate-limiting users so they can't send new messages too quickly. - -## Set users' online status - -In C# modules, you can register for `Connect` and `Disconnect` events by using a special `ReducerKind`. We'll use the `Connect` event to create a `User` record for the client if it doesn't yet exist, and to set its online status. - -We'll use `User.FindByIdentity` to look up a `User` row for `dbEvent.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. - -In `server/Lib.cs`, add the definition of the connect reducer to the `Module` class: - -```csharp -[SpacetimeDB.Reducer(ReducerKind.Connect)] -public static void OnConnect(DbEventArgs dbEventArgs) -{ - Log($"Connect {dbEventArgs.Sender}"); - var user = User.FindByIdentity(dbEventArgs.Sender); - - if (user is not null) - { - // If this is a returning user, i.e., we already have a `User` with this `Identity`, - // set `Online: true`, but leave `Name` and `Identity` unchanged. - user.Online = true; - User.UpdateByIdentity(dbEventArgs.Sender, user); - } - else - { - // If this is a new user, create a `User` object for the `Identity`, - // which is online, but hasn't set a name. - new User - { - Name = null, - Identity = dbEventArgs.Sender, - Online = true, - }.Insert(); - } -} -``` - -Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.Disconnect`. We'll use it to un-set the `Online` status of the `User` for the disconnected client. - -Add the following code after the `OnConnect` lambda: - -```csharp -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void OnDisconnect(DbEventArgs dbEventArgs) -{ - var user = User.FindByIdentity(dbEventArgs.Sender); - - if (user is not null) - { - // This user should exist, so set `Online: false`. - user.Online = false; - User.UpdateByIdentity(dbEventArgs.Sender, user); - } - else - { - // User does not exist, log warning - Log("Warning: No user found for disconnected client."); - } -} -``` - -## Publish the module - -And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more pleasant. Come up with a unique name, and fill it in where we've written ``. - -From the `quickstart-chat` directory, run: - -```bash -spacetime publish --project-path server -``` - -```bash -npm i wasm-opt -g -``` - -## Call Reducers - -You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. - -```bash -spacetime call send_message "Hello, World!" -``` - -Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. - -```bash -spacetime logs -``` - -You should now see the output that your module printed in the database. - -```bash -info: Hello, World! -``` - -## SQL Queries - -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" -``` - -```bash - text ---------- - "Hello, World!" -``` - -## What's next? - -You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](rust-sdk-quickstart-guide.), [C#](csharp-sdk-quickstart-guide.), [TypeScript](typescript-sdk-quickstart-guide.) or [Python](python-sdk-quickstart-guide.). - -If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](part-1.) or check out our example game, [BitcraftMini](part-3.). diff --git a/Writerside/topics/modules/modules_index.md b/Writerside/topics/modules/modules_index.md deleted file mode 100644 index fd1a7e62..00000000 --- a/Writerside/topics/modules/modules_index.md +++ /dev/null @@ -1,30 +0,0 @@ -# Server Module Overview - -Server modules are the core of a SpacetimeDB application. They define the structure of the database and the server-side logic that processes and handles client requests. These functions are called reducers and are transactional, meaning they ensure data consistency and integrity. Reducers can perform operations such as inserting, updating, and deleting data in the database. - -In the following sections, we'll cover the basics of server modules and how to create and deploy them. - -## Supported Languages - -### Rust - -As of SpacetimeDB 0.6, Rust is the only fully supported language for server modules. Rust is a great option for server modules because it is fast, safe, and has a small runtime. - -- [Rust Module Reference](rust.) -- [Rust Module Quickstart Guide](quickstart.) - -### C# - -We have C# support available in experimental status. C# can be a good choice for developers who are already using Unity or .net for their client applications. - -- [C# Module Reference](c-sharp.) -- [C# Module Quickstart Guide](quickstart1.) - -### Coming Soon - -We have plans to support additional languages in the future. - -- Python -- Typescript -- C++ -- Lua diff --git a/Writerside/topics/modules/rust/rust_index.md b/Writerside/topics/modules/rust/rust_index.md deleted file mode 100644 index 05d62bdc..00000000 --- a/Writerside/topics/modules/rust/rust_index.md +++ /dev/null @@ -1,454 +0,0 @@ -# SpacetimeDB Rust Modules - -Rust clients of SpacetimeDB use the [Rust SpacetimeDB module library][module library] to write modules which interact with the SpacetimeDB database. - -First, the `spacetimedb` library provides a number of macros for creating tables and Rust `struct`s corresponding to rows in those tables. - -Then the client API allows interacting with the database inside special functions called reducers. - -This guide assumes you are familiar with some basics of Rust. At the very least, you should be familiar with the idea of using attribute macros. An extremely common example is `derive` macros. - -Derive macros look at the type they are attached to and generate some related code. In this example, `#[derive(Debug)]` generates the formatting code needed to print out a `Location` for debugging purposes. - -```rust -#[derive(Debug)] -struct Location { - x: u32, - y: u32, -} -``` - -## SpacetimeDB Macro basics - -Let's start with a highly commented example, straight from the [demo]. This Rust package defines a SpacetimeDB module, with types we can operate on and functions we can run. - -```rust -// In this small example, we have two rust imports: -// |spacetimedb::spacetimedb| is the most important attribute we'll be using. -// |spacetimedb::println| is like regular old |println|, but outputting to the module's logs. -use spacetimedb::{spacetimedb, println}; - -// This macro lets us interact with a SpacetimeDB table of Person rows. -// We can insert and delete into, and query, this table by the collection -// of functions generated by the macro. -#[spacetimedb(table)] -pub struct Person { - name: String, -} - -// This is the other key macro we will be using. A reducer is a -// stored procedure that lives in the database, and which can -// be invoked remotely. -#[spacetimedb(reducer)] -pub fn add(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) -} - -// 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 -// functions, not methods. -#[spacetimedb(reducer)] -pub fn say_hello() { - // 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() { - // Reducers run in a very constrained and sandboxed environment, - // and in particular, can't do most I/O from the Rust standard library. - // We provide an alternative |spacetimedb::println| which is just like - // the std version, excepted it is redirected out to the module's logs. - println!("Hello, {}!", person.name); - } - println!("Hello, World!"); -} - -// Reducers can't return values, but can return errors. To do so, -// the reducer must have a return type of `Result<(), T>`, for any `T` that -// implements `Debug`. Such errors returned from reducers will be formatted and -// printed out to logs. -#[spacetimedb(reducer)] -pub fn add_person(name: String) -> Result<(), String> { - if name.is_empty() { - return Err("Name cannot be empty"); - } - - Person::insert(Person { name }) -} -``` - -## Macro API - -Now we'll get into details on all the macro APIs SpacetimeDB provides, starting with all the variants of the `spacetimedb` attribute. - -### Defining tables - -`#[spacetimedb(table)]` takes no further arguments, and is applied to a Rust struct with named fields: - -```rust -#[spacetimedb(table)] -struct Table { - field1: String, - field2: u32, -} -``` - -This attribute is applied to Rust structs in order to create corresponding tables in SpacetimeDB. Fields of the Rust struct correspond to columns of the database table. - -The fields of the struct have to be types that spacetimedb knows how to encode into the database. This is captured in Rust by the `SpacetimeType` trait. - -This is automatically defined for built in numeric types: - -- `bool` -- `u8`, `u16`, `u32`, `u64`, `u128` -- `i8`, `i16`, `i32`, `i64`, `i128` -- `f32`, `f64` - -And common data structures: - -- `String` and `&str`, utf-8 string data -- `()`, the unit type -- `Option where T: SpacetimeType` -- `Vec where T: SpacetimeType` - -All `#[spacetimedb(table)]` types are `SpacetimeType`s, and accordingly, all of their fields have to be. - -```rust -#[spacetimedb(table)] -struct AnotherTable { - // Fine, some builtin types. - id: u64, - name: Option, - - // Fine, another table type. - table: Table, - - // Fine, another type we explicitly make serializable. - serial: Serial, -} -``` - -If you want to have a field that is not one of the above primitive types, and not a table of its own, you can derive the `SpacetimeType` attribute on it. - -We can derive `SpacetimeType` on `struct`s and `enum`s with members that are themselves `SpacetimeType`s. - -```rust -#[derive(SpacetimeType)] -enum Serial { - Builtin(f64), - Compound { - s: String, - bs: Vec, - } -} -``` - -Once the table is created via the macro, other attributes described below can control more aspects of the table. For instance, a particular column can be indexed, or take on values of an automatically incremented counter. These are described in detail below. - -```rust -#[spacetimedb(table)] -struct Person { - #[unique] - id: u64, - - name: String, - address: String, -} -``` - -### Defining reducers - -`#[spacetimedb(reducer)]` optionally takes a single argument, which is a frequency at which the reducer will be automatically called by the database. - -`#[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>`. - -```rust -#[spacetimedb(reducer)] -fn give_player_item(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::filter_by_item_id(id).ok_or(GameErr::InvalidId)?; - item.owner = Some(player_id); - Item::update_by_id(id, item); - Ok(()) -} - -struct Item { - #[unique] - item_id: u64, - - owner: Option, -} -``` - -Note that reducers can call non-reducer functions, including standard library functions. - -Reducers that are called periodically take an additional macro argument specifying the frequency at which they will be invoked. Durations are parsed according to https://docs.rs/humantime/latest/humantime/fn.parse_duration.html and will usually be a number of milliseconds or seconds. - -Both of these examples are invoked every second. - -```rust -#[spacetimedb(reducer, repeat = 1s)] -fn every_second() {} - -#[spacetimedb(reducer, repeat = 1000ms)] -fn every_thousand_milliseconds() {} -``` - -Finally, reducers can also receive a ReducerContext object, or the Timestamp at which they are invoked, just by taking parameters of those types first. - -```rust -#[spacetimedb(reducer, repeat = 1s)] -fn tick_timestamp(time: Timestamp) { - println!("tick at {time}"); -} - -#[spacetimedb(reducer, repeat = 500ms)] -fn tick_ctx(ctx: ReducerContext) { - println!("tick at {}", ctx.timestamp) -} -``` - -Note that each distinct time a repeating reducer is invoked, a seperate schedule is created for that reducer. So invoking `every_second` three times from the spacetimedb cli will result in the reducer being called times times each second. - -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. - -#[SpacetimeType] - -#[sats] - -## Client API - -Besides the macros for creating tables and reducers, there's two other parts of the Rust SpacetimeDB library. One is a collection of macros for logging, and the other is all the automatically generated functions for operating on those tables. - -### `println!` and friends - -Because reducers run in a WASM sandbox, they don't have access to general purpose I/O from the Rust standard library. There's no filesystem or network access, and no input or output. This means no access to things like `std::println!`, which prints to standard output. - -SpacetimeDB modules have access to logging output. These are exposed as macros, just like their `std` equivalents. The names, and all the Rust formatting machinery, work the same; just the location of the output is different. - -Logs for a module can be viewed with the `spacetime logs` command from the CLI. - -```rust -use spacetimedb::{ - println, - print, - eprintln, - eprint, - dbg, -}; - -#[spacetimedb(reducer)] -fn output(i: i32) { - // These will be logged at log::Level::Info. - println!("an int with a trailing newline: {i}"); - print!("some more text...\n"); - - // These log at log::Level::Error. - eprint!("Oops..."); - eprintln!(", we hit an error"); - - // Just like std::dbg!, this prints its argument and returns the value, - // as a drop-in way to print expressions. So this will print out |i| - // before passing the value of |i| along to the calling function. - // - // The output is logged log::Level::Debug. - OutputtedNumbers::insert(dbg!(i)); -} -``` - -### Generated functions on a SpacetimeDB table - -We'll work off these structs to see what functions SpacetimeDB generates: - -This table has a plain old column. - -```rust -#[spacetimedb(table)] -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. - -```rust -#[spacetimedb(table)] -struct Unique { - // A unique column: - #[unique] - unique_field: u64, -} -``` - -This table has an automatically incrementing column. SpacetimeDB automatically provides an incrementing sequence of values for this field, and sets the field to that value when you insert the row. - -Only integer types can be `#[unique]`: `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64` and `i128`. - -```rust -#[spacetimedb(table)] -struct Autoinc { - #[autoinc] - autoinc_field: u64, -} -``` - -These attributes can be combined, to create an automatically assigned ID usable for filtering. - -```rust -#[spacetimedb(table)] -struct Identity { - #[autoinc] - #[unique] - id_field: u64, -} -``` - -### Insertion - -We'll talk about insertion first, as there a couple of special semantics to know about. - -When we define |Ordinary| as a spacetimedb table, we get the ability to insert into it with the generated `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) { - let ordinary = Ordinary { ordinary_field: value }; - let result = Ordinary::insert(ordinary); - assert_eq!(ordinary.ordinary_field, result.ordinary_field); -} -``` - -When there is a unique column constraint on the table, insertion can fail if a uniqueness constraint is violated. - -If we insert two rows which have the same value of a unique column, the second will fail. - -```rust -#[spacetimedb(reducer)] -fn insert_unique(value: u64) { - let result = Ordinary::insert(Unique { unique_field: value }); - assert!(result.is_ok()); - - let result = Ordinary::insert(Unique { unique_field: value }); - assert!(result.is_err()); -} -``` - -When inserting a table with an `#[autoinc]` column, the database will automatically overwrite whatever we give it with an atomically increasing value. - -The returned row has the `autoinc` column set to the value that was actually written into the database. - -```rust -#[spacetimedb(reducer)] -fn insert_autoinc() { - 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 }) - assert_eq!(actual.autoinc_field, i); - } -} - -#[spacetimedb(reducer)] -fn insert_id() { - 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 }) - } -} -``` - -### Iterating - -Given a table, we can iterate over all the rows in it. - -```rust -#[spacetimedb(table)] -struct Person { - #[unique] - id: u64, - - age: u32, - name: String, - address: String, -} -``` - -// Every table structure an iter function, like: - -```rust -fn MyTable::iter() -> TableIter -``` - -`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() { - let mut addresses = HashSet::new(); - - for person in Person::iter() { - addresses.insert(person.address); - } - - for address in addresses.iter() { - println!("{address}"); - } -} -``` - -### Filtering - -Often, we don't need to look at the entire table, and instead are looking for rows with specific values in certain columns. - -Our `Person` table has a unique id column, so we can filter for a row matching that ID. Since it is unique, we will find either 0 or 1 matching rows in the database. This gets represented naturally as an `Option` in Rust. SpacetimeDB automatically creates and uses indexes for filtering on unique columns, so it is very efficient. - -The name of the filter method just corresponds to the column name. - -```rust -#[spacetimedb(reducer)] -fn filtering(id: u64) { - match Person::filter_by_id(&id) { - Some(person) => println!("Found {person}"), - None => println!("No person with id {id}"), - } -} -``` - -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::filter_by_age(&21) { - println!("{person} has turned 21"); - } -} -``` - -### Deleting - -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) -} -``` - -[macro library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/bindings-macro -[module library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/lib -[demo]: /#demo diff --git a/Writerside/topics/modules/rust/rust_quickstart.md b/Writerside/topics/modules/rust/rust_quickstart.md deleted file mode 100644 index baa62a0d..00000000 --- a/Writerside/topics/modules/rust/rust_quickstart.md +++ /dev/null @@ -1,272 +0,0 @@ -# Rust Module Quickstart - -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. - -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. - -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. - -## 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. - -## 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: - -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - -If you're on Windows, go [here](https://learn.microsoft.com/en-us/windows/dev-environment/rust/setup). - -## Project structure - -Create and enter a directory `quickstart-chat`: - -```bash -mkdir quickstart-chat -cd quickstart-chat -``` - -Now create `server`, our module, which runs in the database: - -```bash -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. - -To the top of `server/src/lib.rs`, add some imports we'll be using: - -```rust -use spacetimedb::{spacetimedb, ReducerContext, Identity, Timestamp}; -``` - -From `spacetimedb`, we import: - -- `spacetimedb`, an attribute macro we'll use to define tables and reducers. -- `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. - -## Define tables - -To get our chat server running, we'll need to store two kinds of data: information about each user, and records of all the messages that have been sent. - -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. - -To `server/src/lib.rs`, add the definition of the table `User`: - -```rust -#[spacetimedb(table)] -pub struct User { - #[primarykey] - identity: Identity, - name: Option, - online: bool, -} -``` - -For each `Message`, we'll store the `Identity` of the user who sent it, the `Timestamp` when it was sent, and the text of the message. - -To `server/src/lib.rs`, add the definition of the table `Message`: - -```rust -#[spacetimedb(table)] -pub struct Message { - sender: Identity, - sent: Timestamp, - text: String, -} -``` - -## Set users' names - -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`. - -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> { - 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 }); - Ok(()) - } else { - Err("Cannot set name for unknown user".to_string()) - } -} -``` - -For now, we'll just do a bare minimum of validation, rejecting the empty name. You could extend this in various ways, like: - -- Comparing against a blacklist for moderation purposes. -- Unicode-normalizing names. -- Rejecting names that contain non-printable characters, or removing characters or replacing them with a placeholder. -- Rejecting or truncating long names. -- Rejecting duplicate names. - -To `server/src/lib.rs`, add: - -```rust -/// Takes a name and checks if it's acceptable as a user's name. -fn validate_name(name: String) -> Result { - if name.is_empty() { - Err("Names must not be empty".to_string()) - } else { - Ok(name) - } -} -``` - -## 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`. - -To `server/src/lib.rs`, add: - -```rust -#[spacetimedb(reducer)] -/// Clients invoke this reducer to send messages. -pub fn send_message(ctx: ReducerContext, text: String) -> Result<(), String> { - let text = validate_message(text)?; - log::info!("{}", text); - Message::insert(Message { - sender: ctx.sender, - text, - sent: ctx.timestamp, - }); - Ok(()) -} -``` - -We'll want to validate messages' texts in much the same way we validate users' chosen names. As above, we'll do the bare minimum, rejecting only empty messages. - -To `server/src/lib.rs`, add: - -```rust -/// Takes a message's text and checks if it's acceptable to send. -fn validate_message(text: String) -> Result { - if text.is_empty() { - Err("Messages must not be empty".to_string()) - } else { - Ok(text) - } -} -``` - -You could extend the validation in `validate_message` in similar ways to `validate_name`, or add additional checks to `send_message`, like: - -- Rejecting messages from senders who haven't set their names. -- Rate-limiting users so they can't send new messages too quickly. - -## 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. - -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)]` attribute, 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`. - -To `server/src/lib.rs`, add the definition of the connect reducer: - -```rust -#[spacetimedb(connect)] -// Called when a client connects to the SpacetimeDB -pub fn identity_connected(ctx: ReducerContext) { - if let Some(user) = User::filter_by_identity(&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 }); - } 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 { - 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. - -```rust -#[spacetimedb(disconnect)] -// 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 }); - } else { - // This branch should be unreachable, - // as it doesn't make sense for a client to disconnect without connecting first. - log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender); - } -} -``` - -## 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 ``. - -From the `quickstart-chat` directory, run: - -```bash -spacetime publish --project-path server -``` - -## Call Reducers - -You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. - -```bash -spacetime call send_message 'Hello, World!' -``` - -Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. - -```bash -spacetime logs -``` - -You should now see the output that your module printed in the database. - -```bash -info: Hello, World! -``` - -## SQL Queries - -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" -``` - -```bash - text ---------- - "Hello, World!" -``` - -## What's next? - -You can find the full code for this module [in the SpacetimeDB module examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/modules/quickstart-chat). - -You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quickstart guide for your client language of choice: [Rust](quickstart2.), [C#](quickstart3.), [TypeScript](quickstart4.) or [Python](quickstart5.). - -If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](part-1.) or check out our example game, [BitcraftMini](part-3.). diff --git a/Writerside/topics/satn.md b/Writerside/topics/satn.md deleted file mode 100644 index 774ff1b3..00000000 --- a/Writerside/topics/satn.md +++ /dev/null @@ -1,163 +0,0 @@ -# SATN JSON Format - -The Spacetime Algebraic Type Notation JSON format defines how Spacetime `AlgebraicType`s and `AlgebraicValue`s are encoded as JSON. Algebraic types and values are JSON-encoded for transport via the [HTTP Databases API](database.) and the [WebSocket text protocol](ws#text-protocol.). - -## Values - -### At a glance - -| Type | Description | -| ---------------- | ---------------------------------------------------------------- | -| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype.). | -| `SumValue` | A value whose type is a [`SumType`](#sumtype.). | -| `ProductValue` | A value whose type is a [`ProductType`](#producttype.). | -| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype.). | -| | | - -### `AlgebraicValue` - -```json -SumValue | ProductValue | BuiltinValue -``` - -### `SumValue` - -An instance of a [`SumType`](#sumtype.). `SumValue`s are encoded as a JSON object with a single key, a non-negative integer tag which identifies the variant. The value associated with this key is the variant data. Variants which hold no data will have an empty array as their value. - -The tag is an index into the [`SumType.variants`](#sumtype.) array of the value's [`SumType`](#sumtype.). - -```json -{ - "": AlgebraicValue -} -``` - -### `ProductValue` - -An instance of a [`ProductType`](#producttype.). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#productype.) array of the value's [`ProductType`](#producttype.). - -```json -array -``` - -### `BuiltinValue` - -An instance of a [`BuiltinType`](#builtintype.). `BuiltinValue`s are encoded as JSON values of corresponding types. - -```json -boolean | number | string | array | map -``` - -| [`BuiltinType`](#builtintype.) | JSON type | -| ----------------------------- | ------------------------------------- | -| `Bool` | `boolean` | -| Integer types | `number` | -| Float types | `number` | -| `String` | `string` | -| Array types | `array` | -| Map types | `map` | - -All SATS integer types are encoded as JSON `number`s, so values of 64-bit and 128-bit integer types may lose precision when encoding values larger than 2⁵². - -## Types - -All SATS types are JSON-encoded by converting them to an `AlgebraicValue`, then JSON-encoding that meta-value. - -### At a glance - -| Type | Description | -| --------------------------------------- | ------------------------------------------------------------------------------------ | -| [`AlgebraicType`](#algebraictype.) | Any SATS type. | -| [`SumType`](#sumtype.) | Sum types, i.e. tagged unions. | -| [`ProductType`](#productype.) | Product types, i.e. structures. | -| [`BuiltinType`](#builtintype.) | Built-in and primitive types, including booleans, numbers, strings, arrays and maps. | -| [`AlgebraicTypeRef`](#algebraictyperef.) | An indirect reference to a type, used to implement recursive types. | - -#### `AlgebraicType` - -`AlgebraicType` is the most general meta-type in the Spacetime Algebraic Type System. Any SATS type can be represented as an `AlgebraicType`. `AlgebraicType` is encoded as a tagged union, with variants for [`SumType`](#sumtype.), [`ProductType`](#producttype.), [`BuiltinType`](#builtintype.) and [`AlgebraicTypeRef`](#algebraictyperef.). - -```json -{ "Sum": SumType } -| { "Product": ProductType } -| { "Builtin": BuiltinType } -| { "Ref": AlgebraicTypeRef } -``` - -#### `SumType` - -The meta-type `SumType` represents sum types, also called tagged unions or Rust `enum`s. A sum type has some number of variants, each of which has an `AlgebraicType` of variant data, and an optional string discriminant. For each instance, exactly one variant will be active. The instance will contain only that variant's data. - -A `SumType` with zero variants is called an empty type or never type because it is impossible to construct an instance. - -Instances of `SumType`s are [`SumValue`s](#sumvalue.), and store a tag which identifies the active variant. - -```json -// SumType: -{ - "variants": array, -} - -// SumTypeVariant: -{ - "algebraic_type": AlgebraicType, - "name": { "some": string } | { "none": [] } -} -``` - -### `ProductType` - -The meta-type `ProductType` represents product types, also called structs or tuples. A product type has some number of fields, each of which has an `AlgebraicType` of field data, and an optional string field name. Each instance will contain data for all of the product type's fields. - -A `ProductType` with zero fields is called a unit type because it has a single instance, the unit, which is empty. - -Instances of `ProductType`s are [`ProductValue`s](#productvalue.), and store an array of field data. - -```json -// ProductType: -{ - "elements": array, -} - -// ProductTypeElement: -{ - "algebraic_type": AlgebraicType, - "name": { "some": string } | { "none": [] } -} -``` - -### `BuiltinType` - -The meta-type `BuiltinType` represents SATS primitive types: booleans, integers, floating-point numbers, strings, arrays and maps. `BuiltinType` is encoded as a tagged union, with a variant for each SATS primitive type. - -SATS integer types are identified by their signedness and width in bits. SATS supports the same set of integer types as Rust, i.e. 8, 16, 32, 64 and 128-bit signed and unsigned integers. - -SATS floating-point number types are identified by their width in bits. SATS supports 32 and 64-bit floats, which correspond to [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) single- and double-precision binary floats, respectively. - -SATS array and map types are homogeneous, meaning that each array has a single element type to which all its elements must conform, and each map has a key type and a value type to which all of its keys and values must conform. - -```json -{ "Bool": [] } -| { "I8": [] } -| { "U8": [] } -| { "I16": [] } -| { "U16": [] } -| { "I32": [] } -| { "U32": [] } -| { "I64": [] } -| { "U64": [] } -| { "I128": [] } -| { "U128": [] } -| { "F32": [] } -| { "F64": [] } -| { "String": [] } -| { "Array": AlgebraicType } -| { "Map": { - "key_ty": AlgebraicType, - "ty": AlgebraicType, - } } -``` - -### `AlgebraicTypeRef` - -`AlgebraicTypeRef`s are JSON-encoded as non-negative integers. These are indices into a typespace, like the one returned by the [`/database/schema/:name_or_address GET` HTTP endpoint](database#databaseschemaname_or_address-get.). diff --git a/Writerside/topics/sdks/c-sharp/c-sharp_quickstart.md b/Writerside/topics/sdks/c-sharp/c-sharp_quickstart.md deleted file mode 100644 index 4d5b1e92..00000000 --- a/Writerside/topics/sdks/c-sharp/c-sharp_quickstart.md +++ /dev/null @@ -1,438 +0,0 @@ -# C# Client SDK Quick Start - -In this guide we'll show you how to get up and running with a simple SpacetimeDB app with a client written in C#. - -We'll implement a command-line client for the module created in our [Rust](rust_quickstart.md) or [C# Module](quickstart.md) Quickstart guides. Ensure you followed one of these guides before continuing. - -## Project structure - -Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: - -```bash -cd quickstart-chat -``` - -Within it, create a new C# console application project called `client` using either Visual Studio, Rider or the .NET CLI: - -```bash -dotnet new console -o client -``` - -Open the project in your IDE of choice. - -## Add the NuGet package for the C# SpacetimeDB SDK - -Add the `SpacetimeDB.ClientSDK` [NuGet package](https://www.nuget.org/packages/spacetimedbsdk) using Visual Studio or Rider _NuGet Package Manager_ or via the .NET CLI: - -```bash -dotnet add package SpacetimeDB.ClientSDK -``` - -## Generate your module types - -The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. - -In your `quickstart-chat` directory, run: - -```bash -mkdir -p client/module_bindings -spacetime generate --lang csharp --out-dir client/module_bindings --project-path server -``` - -Take a look inside `client/module_bindings`. The CLI should have generated five files: - -``` -module_bindings -├── Message.cs -├── ReducerEvent.cs -├── SendMessageReducer.cs -├── SetNameReducer.cs -└── User.cs -``` - -## Add imports to Program.cs - -Open `client/Program.cs` and add the following imports: - -```csharp -using SpacetimeDB; -using SpacetimeDB.Types; -using System.Collections.Concurrent; -``` - -We will also need to create some global variables that will be explained when we use them later. Add the following to the top of `Program.cs`: - -```csharp -// our local client SpacetimeDB identity -Identity? local_identity = null; - -// declare a thread safe queue to store commands in format (command, args) -ConcurrentQueue<(string,string)> input_queue = new ConcurrentQueue<(string, string)>(); - -// declare a threadsafe cancel token to cancel the process loop -CancellationTokenSource cancel_token = new CancellationTokenSource(); -``` - -## Define 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 several things: - -1. Initialize the `AuthToken` module, which loads and stores our authentication token to/from local storage. -2. Create the `SpacetimeDBClient` instance. -3. Register callbacks on any events we want to handle. These will print to standard output messages received from the database and updates about users' names and online statuses. -4. Start our processing thread which connects to the SpacetimeDB module, updates the SpacetimeDB client and processes commands that come in from the input loop running in the main thread. -5. Start the input loop, which reads commands from standard input and sends them to the processing thread. -6. When the input loop exits, stop the processing thread and wait for it to exit. - -```csharp -void Main() -{ - AuthToken.Init(".spacetime_csharp_quickstart"); - - // create the client, pass in a logger to see debug messages - SpacetimeDBClient.CreateInstance(new ConsoleLogger()); - - RegisterCallbacks(); - - // spawn a thread to call process updates and process commands - var thread = new Thread(ProcessThread); - thread.Start(); - - InputLoop(); - - // this signals the ProcessThread to stop - cancel_token.Cancel(); - thread.Join(); -} -``` - -## Register callbacks - -We need to handle several sorts of events: - -1. `onConnect`: When we connect, we will call `Subscribe` to tell the module what tables we care about. -2. `onIdentityReceived`: When we receive our credentials, we'll use the `AuthToken` module to save our token so that the next time we connect, we can re-authenticate as the same user. -3. `onSubscriptionApplied`: When we get the onSubscriptionApplied callback, that means our local client cache has been fully populated. At this time we'll print the user menu. -4. `User.OnInsert`: When a new user joins, we'll print a message introducing them. -5. `User.OnUpdate`: When a user is updated, we'll print their new name, or declare their new online status. -6. `Message.OnInsert`: When we receive a new message, we'll print it. -7. `Reducer.OnSetNameEvent`: If the server rejects our attempt to set our name, we'll print an error. -8. `Reducer.OnSendMessageEvent`: If the server rejects a message we send, we'll print an error. - -```csharp -void RegisterCallbacks() -{ - SpacetimeDBClient.instance.onConnect += OnConnect; - SpacetimeDBClient.instance.onIdentityReceived += OnIdentityReceived; - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - - User.OnInsert += User_OnInsert; - User.OnUpdate += User_OnUpdate; - - Message.OnInsert += Message_OnInsert; - - Reducer.OnSetNameEvent += Reducer_OnSetNameEvent; - Reducer.OnSendMessageEvent += Reducer_OnSendMessageEvent; -} -``` - -### 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 `OnInsert` and `OnDelete` methods, which are automatically generated 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.OnInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. - -`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `ReducerEvent`. This will be `null` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument. - -Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `UserNameOrIdentity` to handle this. - -```csharp -string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()!.Substring(0, 8); - -void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) -{ - if (insertedValue.Online) - { - Console.WriteLine($"{UserNameOrIdentity(insertedValue)} is online"); - } -} -``` - -### Notify about updated users - -Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User::update_by_identity` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column. - -`OnUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. - -In our module, users can be updated for three reasons: - -1. They've set their name using the `SetName` reducer. -2. They're an existing user re-connecting, so their `Online` has been set to `true`. -3. They've disconnected, so their `Online` has been set to `false`. - -We'll print an appropriate message in each of these cases. - -```csharp -void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) -{ - if (oldValue.Name != newValue.Name) - { - Console.WriteLine($"{UserNameOrIdentity(oldValue)} renamed to {newValue.Name}"); - } - - if (oldValue.Online == newValue.Online) - return; - - if (newValue.Online) - { - Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); - } - else - { - Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); - } -} -``` - -### 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 `SendMessage` reducer invocation. We have to handle the backlog we receive when our subscription is initialized separately, to ensure they're printed in the correct order. To that effect, our `OnInsert` callback will check if its `ReducerEvent` argument is not `null`, and only print in that case. - -To find the `User` based on the message's `Sender` identity, we'll use `User::FilterByIdentity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `FilterByIdentity` accepts a `byte[]`, rather than an `Identity`. The `Sender` identity stored in the message is also a `byte[]`, not an `Identity`, so we can just pass it to the filter method. - -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`. - -```csharp -void PrintMessage(Message message) -{ - var sender = User.FilterByIdentity(message.Sender); - var senderName = "unknown"; - if (sender != null) - { - senderName = UserNameOrIdentity(sender); - } - - Console.WriteLine($"{senderName}: {message.Text}"); -} - -void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent) -{ - if (dbEvent != null) - { - PrintMessage(insertedValue); - } -} -``` - -### Warn if our name was rejected - -We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducerEvent` method of the `Reducer` namespace, which is automatically implemented for each reducer by `spacetime generate`. - -Each reducer callback takes one fixed argument: - -The ReducerEvent that triggered the callback. It contains several fields. The ones we care about are: - -1. The `Identity` of the client that called the reducer. -2. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`. -3. The error message, if any, that the reducer returned. - -It also takes a variable amount of additional arguments that match the reducer's arguments. - -These callbacks will be invoked in one of two cases: - -1. If the reducer was successful and altered any of our subscribed rows. -2. If we requested an invocation which failed. - -Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. - -We already handle successful `SetName` invocations using our `User.OnUpdate` 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 `Reducer_OnSetNameEvent` as a `Reducer.OnSetNameEvent` callback which checks if the reducer failed, and if it did, prints an error message including the rejected name. - -We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. - -```csharp -void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) -{ - bool localIdentityFailedToChangeName = - reducerEvent.Identity == local_identity && - reducerEvent.Status == ClientApi.Event.Types.Status.Failed; - - if (localIdentityFailedToChangeName) - { - Console.Write($"Failed to change name to {name}"); - } -} -``` - -### 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. - -```csharp -void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) -{ - bool localIdentityFailedToSendMessage = - reducerEvent.Identity == local_identity && - reducerEvent.Status == ClientApi.Event.Types.Status.Failed; - - if (localIdentityFailedToSendMessage) - { - Console.Write($"Failed to send message {text}"); - } -} -``` - -## Connect callback - -Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. - -```csharp -void OnConnect() -{ - SpacetimeDBClient.instance.Subscribe(new List - { - "SELECT * FROM User", "SELECT * FROM Message" - }); -} -``` - -## OnIdentityReceived callback - -This callback is executed when we receive our credentials from the SpacetimeDB module. We'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app. - -```csharp -void OnIdentityReceived(string authToken, Identity identity, Address _address) -{ - local_identity = identity; - AuthToken.SaveToken(authToken); -} -``` - -## OnSubscriptionApplied callback - -Once our subscription is applied, we'll print all the previously sent messages. We'll define a function `PrintMessagesInOrder` to do this. `PrintMessagesInOrder` calls the automatically generated `Iter` function on our `Message` table, which returns an iterator over all rows in the table. We'll use the `OrderBy` method on the iterator to sort the messages by their `Sent` timestamp. - -```csharp -void PrintMessagesInOrder() -{ - foreach (Message message in Message.Iter().OrderBy(item => item.Sent)) - { - PrintMessage(message); - } -} - -void OnSubscriptionApplied() -{ - Console.WriteLine("Connected"); - PrintMessagesInOrder(); -} -``` - - - -## Process thread - -Since the input loop will be blocking, we'll run our processing code in a separate thread. This thread will: - -1. Connect to the module. We'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`. We will also store if SSL is enabled in a constant called `SSL_ENABLED`. This only needs to be `true` if we are using `SpacetimeDB Cloud`. Replace `` with the name you chose when publishing your module during the module quickstart. - -`Connect` takes an auth token, which is `null` for a new connection, or a stored string for a returning user. We are going to use the optional AuthToken module which uses local storage to store the auth token. If you want to use your own way to associate an auth token with a user, you can pass in your own auth token here. - -2. Loop until the thread is signaled to exit, calling `Update` on the SpacetimeDBClient to process any updates received from the module, and `ProcessCommand` to process any commands received from the input loop. - -3. Finally, Close the connection to the module. - -```csharp -const string HOST = "http://localhost:3000"; -const string DBNAME = "module"; - -void ProcessThread() -{ - SpacetimeDBClient.instance.Connect(AuthToken.Token, HOST, DBNAME); - - // loop until cancellation token - while (!cancel_token.IsCancellationRequested) - { - SpacetimeDBClient.instance.Update(); - - ProcessCommands(); - - Thread.Sleep(100); - } - - SpacetimeDBClient.instance.Close(); -} -``` - -## Input loop and ProcessCommands - -The input loop will read commands from standard input and send them to the processing thread using the input queue. The `ProcessCommands` function is called every 100ms by the processing thread to process any pending commands. - -Supported Commands: - -1. Send a message: `message`, send the message to the module by calling `Reducer.SendMessage` which is automatically generated by `spacetime generate`. - -2. Set name: `name`, will send the new name to the module by calling `Reducer.SetName` which is automatically generated by `spacetime generate`. - -```csharp -void InputLoop() -{ - while (true) - { - var input = Console.ReadLine(); - if (input == null) - { - break; - } - - if (input.StartsWith("/name ")) - { - input_queue.Enqueue(("name", input.Substring(6))); - continue; - } - else - { - input_queue.Enqueue(("message", input)); - } - } -} - -void ProcessCommands() -{ - // process input queue commands - while (input_queue.TryDequeue(out var command)) - { - switch (command.Item1) - { - case "message": - Reducer.SendMessage(command.Item2); - break; - case "name": - Reducer.SetName(command.Item2); - break; - } - } -} -``` - -## Run the client - -Finally we just need to add a call to `Main` in `Program.cs`: - -```csharp -Main(); -``` - -Now, we can run the client by hitting start in Visual Studio or Rider; or by running the following command in the `client` directory: - -```bash -dotnet run --project client -``` - -## What's next? - -Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example. diff --git a/Writerside/topics/sdks/c-sharp/sdks_c-sharp_index.md b/Writerside/topics/sdks/c-sharp/sdks_c-sharp_index.md deleted file mode 100644 index a0f1c7f3..00000000 --- a/Writerside/topics/sdks/c-sharp/sdks_c-sharp_index.md +++ /dev/null @@ -1,959 +0,0 @@ -# The SpacetimeDB C# client SDK - -The SpacetimeDB client C# for Rust contains all the tools you need to build native clients for SpacetimeDB modules using C#. - -## Table of Contents - -- [The SpacetimeDB C# client SDK](#the-spacetimedb-c-client-sdk.) - - [Table of Contents](#table-of-contents.) - - [Install the SDK](#install-the-sdk.) - - [Using the `dotnet` CLI tool](#using-the-dotnet-cli-tool.) - - [Using Unity](#using-unity.) - - [Generate module bindings](#generate-module-bindings.) - - [Initialization](#initialization.) - - [Static Method `SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) - - [Property `SpacetimeDBClient.instance`](#property-spacetimedbclientinstance.) - - [Class `NetworkManager`](#class-networkmanager.) - - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect.) - - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived.) - - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect.) - - [Query subscriptions & one-time actions](#subscribe-to-queries.) - - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.) - - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied.) - - [Method `SpacetimeDBClient.OneOffQuery`](#event-spacetimedbclientoneoffquery.) - - [View rows of subscribed tables](#view-rows-of-subscribed-tables.) - - [Class `{TABLE}`](#class-table.) - - [Static Method `{TABLE}.Iter`](#static-method-tableiter.) - - [Static Method `{TABLE}.FilterBy{COLUMN}`](#static-method-tablefilterbycolumn.) - - [Static Method `{TABLE}.Count`](#static-method-tablecount.) - - [Static Event `{TABLE}.OnInsert`](#static-event-tableoninsert.) - - [Static Event `{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) - - [Static Event `{TABLE}.OnDelete`](#static-event-tableondelete.) - - [Static Event `{TABLE}.OnUpdate`](#static-event-tableonupdate.) - - [Observe and invoke reducers](#observe-and-invoke-reducers.) - - [Class `Reducer`](#class-reducer.) - - [Static Method `Reducer.{REDUCER}`](#static-method-reducerreducer.) - - [Static Event `Reducer.On{REDUCER}`](#static-event-reduceronreducer.) - - [Class `ReducerEvent`](#class-reducerevent.) - - [Enum `Status`](#enum-status.) - - [Variant `Status.Committed`](#variant-statuscommitted.) - - [Variant `Status.Failed`](#variant-statusfailed.) - - [Variant `Status.OutOfEnergy`](#variant-statusoutofenergy.) - - [Identity management](#identity-management.) - - [Class `AuthToken`](#class-authtoken.) - - [Static Method `AuthToken.Init`](#static-method-authtokeninit.) - - [Static Property `AuthToken.Token`](#static-property-authtokentoken.) - - [Static Method `AuthToken.SaveToken`](#static-method-authtokensavetoken.) - - [Class `Identity`](#class-identity.) - - [Class `Identity`](#class-identity-1.) - - [Customizing logging](#customizing-logging.) - - [Interface `ISpacetimeDBLogger`](#interface-ispacetimedblogger.) - - [Class `ConsoleLogger`](#class-consolelogger.) - - [Class `UnityDebugLogger`](#class-unitydebuglogger.) - -## Install the SDK - -### Using the `dotnet` CLI tool - -If you would like to create a console application using .NET, you can create a new project using `dotnet new console` and add the SpacetimeDB SDK to your dependencies: - -```bash -dotnet add package spacetimedbsdk -``` - -(See also the [CSharp Quickstart](quickstart1.) for an in-depth example of such a console application.) - -### Using Unity - -To install the SpacetimeDB SDK into a Unity project, [download the SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest), packaged as a `.unitypackage`. - -In Unity navigate to the `Assets > Import Package > Custom Package` menu in the menu bar. Select your `SpacetimeDB.Unity.Comprehensive.Tutorial.unitypackage` file and leave all folders checked. - -(See also the [Unity Tutorial](part-1.)) - -## Generate module bindings - -Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the C# interface files using the Spacetime CLI. From your project directory, run: - -```bash -mkdir -p module_bindings -spacetime generate --lang cs --out-dir module_bindings --project-path PATH-TO-MODULE-DIRECTORY -``` - -Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. - -## Initialization - -### Static Method `SpacetimeDBClient.CreateInstance` - -```cs -namespace SpacetimeDB { - -public class SpacetimeDBClient { - public static void CreateInstance(ISpacetimeDBLogger loggerToUse); -} - -} -``` - -Create a global SpacetimeDBClient instance, accessible via [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance.) - -| Argument | Type | Meaning | -| ------------- | ----------------------------------------------------- | --------------------------------- | -| `loggerToUse` | [`ISpacetimeDBLogger`](#interface-ispacetimedblogger.) | The logger to use to log messages | - -There is a provided logger called [`ConsoleLogger`](#class-consolelogger.) which logs to `System.Console`, and can be used as follows: - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; -SpacetimeDBClient.CreateInstance(new ConsoleLogger()); -``` - -### Property `SpacetimeDBClient.instance` - -```cs -namespace SpacetimeDB { - -public class SpacetimeDBClient { - public static SpacetimeDBClient instance; -} - -} -``` - -This is the global instance of a SpacetimeDB client in a particular .NET/Unity process. Much of the SDK is accessible through this instance. - -### Class `NetworkManager` - -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. - -![Unity-AddNetworkManager](Unity-AddNetworkManager.JPG) - -This component will handle calling [`SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect.), you still need to handle that yourself. See the [Unity Quickstart](UnityQuickStart.) and [Unity Tutorial](UnityTutorialPart1.) for more information. - -### Method `SpacetimeDBClient.Connect` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public void Connect( - string? token, - string host, - string addressOrName, - bool sslEnabled = true - ); -} - -} -``` - - - -Connect to a database named `addressOrName` accessible over the internet at the URI `host`. - -| Argument | Type | Meaning | -| --------------- | --------- | -------------------------------------------------------------------------- | -| `token` | `string?` | Identity token to use, if one is available. | -| `host` | `string` | URI of the SpacetimeDB instance running the module. | -| `addressOrName` | `string` | Address or name of the module. | -| `sslEnabled` | `bool` | Whether or not to use SSL when connecting to SpacetimeDB. Default: `true`. | - -If a `token` is supplied, it will be passed to the new connection to identify and authenticate the user. Otherwise, a new token and [`Identity`](#class-identity.) will be generated by the server and returned in [`onConnect`](#event-spacetimedbclientonconnect.). - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -const string DBNAME = "chat"; - -// Connect to a local DB with a fresh identity -SpacetimeDBClient.instance.Connect(null, "localhost:3000", DBNAME, false); - -// Connect to cloud with a fresh identity -SpacetimeDBClient.instance.Connect(null, "dev.spacetimedb.net", DBNAME, true); - -// Connect to cloud using a saved identity from the filesystem, or get a new one and save it -AuthToken.Init(); -Identity localIdentity; -SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); -SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { - AuthToken.SaveToken(authToken); - localIdentity = identity; -} -``` - -(You should probably also store the returned `Identity` somewhere; see the [`onIdentityReceived`](#event-spacetimedbclientonidentityreceived.) event.) - -### Event `SpacetimeDBClient.onIdentityReceived` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public event Action onIdentityReceived; -} - -} -``` - -+Called when we receive an auth token, [`Identity`](#class-identity.) and [`Address`](#class-address.) from the server. The [`Identity`](#class-identity.) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The [`Address`](#class-address.) is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity.). - -To store the auth token to the filesystem, use the static method [`AuthToken.SaveToken`](#static-method-authtokensavetoken.). You may also want to store the returned [`Identity`](#class-identity.) in a local variable. - -If an existing auth token is used to connect to the database, the same auth token and the identity it came with will be returned verbatim in `onIdentityReceived`. - -```cs -// Connect to cloud using a saved identity from the filesystem, or get a new one and save it -AuthToken.Init(); -Identity localIdentity; -SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); -SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { - AuthToken.SaveToken(authToken); - localIdentity = identity; -} -``` - -### Event `SpacetimeDBClient.onConnect` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public event Action onConnect; -} - -} -``` - -Allows registering delegates to be invoked upon authentication with the database. - -Once this occurs, the SDK is prepared for calls to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.). - -## Subscribe to queries - -### Method `SpacetimeDBClient.Subscribe` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public void Subscribe(List queries); -} - -} -``` - -| Argument | Type | Meaning | -| --------- | -------------- | ---------------------------- | -| `queries` | `List` | SQL queries to subscribe to. | - -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -`Subscribe` will return an error if called before establishing a connection with the [`SpacetimeDBClient.Connect`](#method-connect.) function. In that case, the queries are not registered. - -The `Subscribe` method does not return data directly. `spacetime generate` will generate classes [`SpacetimeDB.Types.{TABLE}`](#class-table.) for each table in your module. These classes are used to reecive information from the database. See the section [View Rows of Subscribed Tables](#view-rows-of-subscribed-tables.) for more information. - -A new call to `Subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache, and [`{TABLE}.OnDelete`](#event-tableondelete.) callbacks will be invoked for them. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -void Main() -{ - AuthToken.Init(); - SpacetimeDBClient.CreateInstance(new ConsoleLogger()); - - SpacetimeDBClient.instance.onConnect += OnConnect; - - // Our module contains a table named "Loot" - Loot.OnInsert += Loot_OnInsert; - - SpacetimeDBClient.instance.Connect(/* ... */); -} - -void OnConnect() -{ - SpacetimeDBClient.instance.Subscribe(new List { - "SELECT * FROM Loot" - }); -} - -void Loot_OnInsert( - Loot loot, - ReducerEvent? event -) { - Console.Log($"Loaded loot {loot.itemType} at coordinates {loot.position}"); -} -``` - -### Event `SpacetimeDBClient.onSubscriptionApplied` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public event Action onSubscriptionApplied; -} - -} -``` - -Register a delegate to be invoked when a subscription is registered with the database. - -```cs -using SpacetimeDB; - -void OnSubscriptionApplied() -{ - Console.WriteLine("Now listening on queries."); -} - -void Main() -{ - // ...initialize... - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; -} -``` - -### Method [`OneTimeQuery`](#method-spacetimedbclientsubscribe.) - -You may not want to subscribe to a query, but instead want to run a query once and receive the results immediately via a `Task` result: - -```csharp -// Query all Messages from the sender "bob" -SpacetimeDBClient.instance.OneOffQuery("WHERE sender = \"bob\""); -``` - -## View rows of subscribed tables - -The SDK maintains a local view of the database called the "client cache". This cache contains whatever rows are selected via a call to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.). These rows are represented in the SpacetimeDB .Net SDK as instances of [`SpacetimeDB.Types.{TABLE}`](#class-table.). - -ONLY the rows selected in a [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.) call will be available in the client cache. All operations in the client sdk operate on these rows exclusively, and have no information about the state of the rest of the database. - -In particular, SpacetimeDB does not support foreign key constraints. This means that if you are using a column as a foreign key, SpacetimeDB will not automatically bring in all of the rows that key might reference. You will need to manually subscribe to all tables you need information from. - -To optimize network performance, prefer selecting as few rows as possible in your [`Subscribe`](#method-spacetimedbclientsubscribe.) query. Processes that need to view the entire state of the database are better run inside the database -- that is, inside modules. - -### Class `{TABLE}` - -For each table defined by a module, `spacetime generate` will generate a class [`SpacetimeDB.Types.{TABLE}`](#class-table.) whose name is that table's name converted to `PascalCase`. The generated class contains a property for each of the table's columns, whose names are the column names converted to `camelCase`. It also contains various static events and methods. - -Static Methods: - -- [`{TABLE}.Iter()`](#static-method-tableiter.) iterates all subscribed rows in the client cache. -- [`{TABLE}.FilterBy{COLUMN}(value)`](#static-method-tablefilterbycolumn.) filters subscribed rows in the client cache by a column value. -- [`{TABLE}.Count()`](#static-method-tablecount.) counts the number of subscribed rows in the client cache. - -Static Events: - -- [`{TABLE}.OnInsert`](#static-event-tableoninsert.) is called when a row is inserted into the client cache. -- [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) is called when a row is about to be removed from the client cache. -- If the table has a primary key attribute, [`{TABLE}.OnUpdate`](#static-event-tableonupdate.) is called when a row is updated. -- [`{TABLE}.OnDelete`](#static-event-tableondelete.) is called while a row is being removed from the client cache. You should almost always use [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) instead. - -Note that it is not possible to directly insert into the database from the client SDK! All insertion validation should be performed inside serverside modules for security reasons. You can instead [invoke reducers](#observe-and-invoke-reducers.), which run code inside the database that can insert rows for you. - -#### Static Method `{TABLE}.Iter` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public static System.Collections.Generic.IEnumerable
Iter(); -} - -} -``` - -Iterate over all the subscribed rows in the table. This method is only available after [`SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied.) has occurred. - -When iterating over rows and filtering for those containing a particular column, [`TableType::filter`](#method-filter.) will be more efficient, so prefer it when possible. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { - SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); -}; -SpacetimeDBClient.instance.onSubscriptionApplied += () => { - // Will print a line for each `User` row in the database. - foreach (var user in User.Iter()) { - Console.WriteLine($"User: {user.Name}"); - } -}; -SpacetimeDBClient.instance.connect(/* ... */); -``` - -#### Static Method `{TABLE}.FilterBy{COLUMN}` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - // If the column has no #[unique] or #[primarykey] constraint - public static System.Collections.Generic.IEnumerable
FilterBySender(COLUMNTYPE value); - - // If the column has a #[unique] or #[primarykey] constraint - public static TABLE? FilterBySender(COLUMNTYPE value); -} - -} -``` - -For each column of a table, `spacetime generate` generates a static method on the [table class](#class-table.) to filter or seek subscribed rows where that column matches a requested value. These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `PascalCase`. - -The method's return type depends on the column's attributes: - -- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filterBy{COLUMN}` method returns a `{TABLE}?`, where `{TABLE}` is the [table class](#class-table.). -- For non-unique columns, the `filter_by` method returns an `IEnumerator<{TABLE}>`. - -#### Static Method `{TABLE}.Count` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public static int Count(); -} - -} -``` - -Return the number of subscribed rows in the table, or 0 if there is no active connection. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { - SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); -}; -SpacetimeDBClient.instance.onSubscriptionApplied += () => { - Console.WriteLine($"There are {User.Count()} users in the database."); -}; -SpacetimeDBClient.instance.connect(/* ... */); -``` - -#### Static Event `{TABLE}.OnInsert` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void InsertEventHandler( - TABLE insertedValue, - ReducerEvent? dbEvent - ); - public static event InsertEventHandler OnInsert; -} - -} -``` - -Register a delegate for when a subscribed row is newly inserted into the database. - -The delegate takes two arguments: - -- A [`{TABLE}`](#class-table.) instance with the data of the inserted row -- A [`ReducerEvent?`], which contains the data of the reducer that inserted the row, or `null` if the row is being inserted while initializing a subscription. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnInsert += (User user, ReducerEvent? reducerEvent) => { - if (reducerEvent == null) { - Console.WriteLine($"New user '{user.Name}' received during subscription update."); - } else { - Console.WriteLine($"New user '{user.Name}' inserted by reducer {reducerEvent.Reducer}."); - } -}; -``` - -#### Static Event `{TABLE}.OnBeforeDelete` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void DeleteEventHandler( - TABLE deletedValue, - ReducerEvent dbEvent - ); - public static event DeleteEventHandler OnBeforeDelete; -} - -} -``` - -Register a delegate for when a subscribed row is about to be deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked for each of those rows before any of them is deleted. - -The delegate takes two arguments: - -- A [`{TABLE}`](#class-table.) instance with the data of the deleted row -- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that deleted the row. - -This event should almost always be used instead of [`OnDelete`](#static-event-tableondelete.). This is because often, many rows will be deleted at once, and `OnDelete` can be invoked in an arbitrary order on these rows. This means that data related to a row may already be missing when `OnDelete` is called. `OnBeforeDelete` does not have this problem. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { - Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); -}; -``` - -#### Static Event `{TABLE}.OnDelete` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void DeleteEventHandler( - TABLE deletedValue, - SpacetimeDB.ReducerEvent dbEvent - ); - public static event DeleteEventHandler OnDelete; -} - -} -``` - -Register a delegate for when a subscribed row is being deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked on those rows in arbitrary order, and data for some rows may already be missing when it is invoked. For this reason, prefer the event [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.). - -The delegate takes two arguments: - -- A [`{TABLE}`](#class-table.) instance with the data of the deleted row -- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that deleted the row. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { - Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); -}; -``` - -#### Static Event `{TABLE}.OnUpdate` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void UpdateEventHandler( - TABLE oldValue, - TABLE newValue, - ReducerEvent dbEvent - ); - public static event UpdateEventHandler OnUpdate; -} - -} -``` - -Register a delegate for when a subscribed row is being updated. This event is only available if the row has a column with the `#[primary_key]` attribute. - -The delegate takes three arguments: - -- A [`{TABLE}`](#class-table.) instance with the old data of the updated row -- A [`{TABLE}`](#class-table.) instance with the new data of the updated row -- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that updated the row. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnUpdate += (User oldUser, User newUser, ReducerEvent reducerEvent) => { - Debug.Assert(oldUser.UserId == newUser.UserId, "Primary key never changes in an update"); - - Console.WriteLine($"User with ID {oldUser.UserId} had name changed "+ - $"from '{oldUser.Name}' to '{newUser.Name}' by reducer {reducerEvent.Reducer}."); -}; -``` - -## Observe and invoke reducers - -"Reducer" is SpacetimeDB's name for the stored procedures that run in modules inside the database. You can invoke reducers from a connected client SDK, and also receive information about which reducers are running. - -`spacetime generate` generates a class [`SpacetimeDB.Types.Reducer`](#class-reducer.) that contains methods and events for each reducer defined in a module. To invoke a reducer, use the method [`Reducer.{REDUCER}`](#static-method-reducerreducer.) generated for it. To receive a callback each time a reducer is invoked, use the static event [`Reducer.On{REDUCER}`](#static-event-reduceronreducer.). - -### Class `Reducer` - -```cs -namespace SpacetimeDB.Types { - -class Reducer {} - -} -``` - -This class contains a static method and event for each reducer defined in a module. - -#### Static Method `Reducer.{REDUCER}` - -```cs -namespace SpacetimeDB.Types { -class Reducer { - -/* void {REDUCER_NAME}(...ARGS...) */ - -} -} -``` - -For each reducer defined by a module, `spacetime generate` generates a static method which sends a request to the database to invoke that reducer. The generated function's name is the reducer's name converted to `PascalCase`. - -Reducers don't run immediately! They run as soon as the request reaches the database. Don't assume data inserted by a reducer will be available immediately after you call this method. - -For reducers which accept a `ReducerContext` as their first argument, the `ReducerContext` is not included in the generated function's argument list. - -For example, if we define a reducer in Rust as follows: - -```rust -#[spacetimedb(reducer)] -pub fn set_name( - ctx: ReducerContext, - user_id: u64, - name: String -) -> Result<(), Error>; -``` - -The following C# static method will be generated: - -```cs -namespace SpacetimeDB.Types { -class Reducer { - -public static void SendMessage(UInt64 userId, string name); - -} -} -``` - -#### Static Event `Reducer.On{REDUCER}` - -```cs -namespace SpacetimeDB.Types { -class Reducer { - -public delegate void /*{REDUCER}*/Handler(ReducerEvent reducerEvent, /* {ARGS...} */); - -public static event /*{REDUCER}*/Handler On/*{REDUCER}*/Event; - -} -} -``` - -For each reducer defined by a module, `spacetime generate` generates an event to run each time the reducer is invoked. The generated functions are named `on{REDUCER}Event`, where `{REDUCER}` is the reducer's name converted to `PascalCase`. - -The first argument to the event handler is an instance of [`SpacetimeDB.Types.ReducerEvent`](#class-reducerevent.) describing the invocation -- its timestamp, arguments, and whether it succeeded or failed. The remaining arguments are the arguments passed to the reducer. Reducers cannot have return values, so no return value information is included. - -For example, if we define a reducer in Rust as follows: - -```rust -#[spacetimedb(reducer)] -pub fn set_name( - ctx: ReducerContext, - user_id: u64, - name: String -) -> Result<(), Error>; -``` - -The following C# static method will be generated: - -```cs -namespace SpacetimeDB.Types { -class Reducer { - -public delegate void SetNameHandler( - ReducerEvent reducerEvent, - UInt64 userId, - string name -); -public static event SetNameHandler OnSetNameEvent; - -} -} -``` - -Which can be used as follows: - -```cs -/* initialize, wait for onSubscriptionApplied... */ - -Reducer.SetNameHandler += ( - ReducerEvent reducerEvent, - UInt64 userId, - string name -) => { - if (reducerEvent.Status == ClientApi.Event.Types.Status.Committed) { - Console.WriteLine($"User with id {userId} set name to {name}"); - } else if (reducerEvent.Status == ClientApi.Event.Types.Status.Failed) { - Console.WriteLine( - $"User with id {userId} failed to set name to {name}:" - + reducerEvent.ErrMessage - ); - } else if (reducerEvent.Status == ClientApi.Event.Types.Status.OutOfEnergy) { - Console.WriteLine( - $"User with id {userId} failed to set name to {name}:" - + "Invoker ran out of energy" - ); - } -}; -Reducer.SetName(USER_ID, NAME); -``` - -### Class `ReducerEvent` - -`spacetime generate` defines an class `ReducerEvent` containing an enum `ReducerType` with a variant for each reducer defined by a module. The variant's name will be the reducer's name converted to `PascalCase`. - -For example, the example project shown in the Rust Module quickstart will generate the following (abridged) code. - -```cs -namespace SpacetimeDB.Types { - -public enum ReducerType -{ - /* A member for each reducer in the module, with names converted to PascalCase */ - None, - SendMessage, - SetName, -} -public partial class SendMessageArgsStruct -{ - /* A member for each argument of the reducer SendMessage, with names converted to PascalCase. */ - public string Text; -} -public partial class SetNameArgsStruct -{ - /* A member for each argument of the reducer SetName, with names converted to PascalCase. */ - public string Name; -} -public partial class ReducerEvent : ReducerEventBase { - // Which reducer was invoked - public ReducerType Reducer { get; } - // If event.Reducer == ReducerType.SendMessage, the arguments - // sent to the SendMessage reducer. Otherwise, accesses will - // throw a runtime error. - public SendMessageArgsStruct SendMessageArgs { get; } - // If event.Reducer == ReducerType.SetName, the arguments - // passed to the SetName reducer. Otherwise, accesses will - // throw a runtime error. - public SetNameArgsStruct SetNameArgs { get; } - - /* Additional information, present on any ReducerEvent */ - // The name of the reducer. - public string ReducerName { get; } - // The timestamp of the reducer invocation inside the database. - public ulong Timestamp { get; } - // The identity of the client that invoked the reducer. - public SpacetimeDB.Identity Identity { get; } - // Whether the reducer succeeded, failed, or ran out of energy. - public ClientApi.Event.Types.Status Status { get; } - // If event.Status == Status.Failed, the error message returned from inside the module. - public string ErrMessage { get; } -} - -} -``` - -#### Enum `Status` - -```cs -namespace ClientApi { -public sealed partial class Event { -public static partial class Types { - -public enum Status { - Committed = 0, - Failed = 1, - OutOfEnergy = 2, -} - -} -} -} -``` - -An enum whose variants represent possible reducer completion statuses of a reducer invocation. - -##### Variant `Status.Committed` - -The reducer finished successfully, and its row changes were committed to the database. - -##### Variant `Status.Failed` - -The reducer failed, either by panicking or returning a `Err`. - -##### Variant `Status.OutOfEnergy` - -The reducer was canceled because the module owner had insufficient energy to allow it to run to completion. - -## Identity management - -### Class `AuthToken` - -The AuthToken helper class handles creating and saving SpacetimeDB identity tokens in the filesystem. - -#### Static Method `AuthToken.Init` - -```cs -namespace SpacetimeDB { - -class AuthToken { - public static void Init( - string configFolder = ".spacetime_csharp_sdk", - string configFile = "settings.ini", - string? configRoot = null - ); -} - -} -``` - -Creates a file `$"{configRoot}/{configFolder}/{configFile}"` to store tokens. -If no arguments are passed, the default is `"%HOME%/.spacetime_csharp_sdk/settings.ini"`. - -| Argument | Type | Meaning | -| -------------- | -------- | ---------------------------------------------------------------------------------- | -| `configFolder` | `string` | The folder to store the config file in. Default is `"spacetime_csharp_sdk"`. | -| `configFile` | `string` | The name of the config file. Default is `"settings.ini"`. | -| `configRoot` | `string` | The root folder to store the config file in. Default is the user's home directory. | - -#### Static Property `AuthToken.Token` - -```cs -namespace SpacetimeDB { - -class AuthToken { - public static string? Token { get; } -} - -} -``` - -The auth token stored on the filesystem, if one exists. - -#### Static Method `AuthToken.SaveToken` - -```cs -namespace SpacetimeDB { - -class AuthToken { - public static void SaveToken(string token); -} - -} -``` - -Save a token to the filesystem. - -### Class `Identity` - -```cs -namespace SpacetimeDB -{ - public struct Identity : IEquatable - { - public byte[] Bytes { get; } - public static Identity From(byte[] bytes); - public bool Equals(Identity other); - public static bool operator ==(Identity a, Identity b); - public static bool operator !=(Identity a, Identity b); - } -} -``` - -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. - -### Class `Identity` -```cs -namespace SpacetimeDB -{ - public struct Address : IEquatable
- { - public byte[] Bytes { get; } - public static Address? From(byte[] bytes); - public bool Equals(Address other); - public static bool operator ==(Address a, Address b); - public static bool operator !=(Address a, Address b); - } -} -``` - -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity.). - -## Customizing logging - -The SpacetimeDB C# SDK performs internal logging. Instances of [`ISpacetimeDBLogger`](#interface-ispacetimedblogger.) can be passed to [`SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) to customize how SDK logs are delivered to your application. - -This is set up automatically for you if you use Unity-- adding a [`NetworkManager`](#class-networkmanager.) component to your unity scene will automatically initialize the `SpacetimeDBClient` with a [`UnityDebugLogger`](#class-unitydebuglogger.). - -Outside of unity, all you need to do is the following: - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; -SpacetimeDBClient.CreateInstance(new ConsoleLogger()); -``` - -### Interface `ISpacetimeDBLogger` - -```cs -namespace SpacetimeDB -{ - -public interface ISpacetimeDBLogger -{ - void Log(string message); - void LogError(string message); - void LogWarning(string message); - void LogException(Exception e); -} - -} -``` - -This interface provides methods that are invoked when the SpacetimeDB C# SDK needs to log at various log levels. You can create custom implementations if needed to integrate with existing logging solutions. - -### Class `ConsoleLogger` - -```cs -namespace SpacetimeDB { - -public class ConsoleLogger : ISpacetimeDBLogger {} - -} -``` - -An `ISpacetimeDBLogger` implementation for regular .NET applications, using `Console.Write` when logs are received. - -### Class `UnityDebugLogger` - -```cs -namespace SpacetimeDB { - -public class UnityDebugLogger : ISpacetimeDBLogger {} - -} -``` - -An `ISpacetimeDBLogger` implementation for Unity, using the Unity `Debug.Log` api. diff --git a/Writerside/topics/sdks/python/python_index.md b/Writerside/topics/sdks/python/python_index.md deleted file mode 100644 index a87d8ac5..00000000 --- a/Writerside/topics/sdks/python/python_index.md +++ /dev/null @@ -1,552 +0,0 @@ -# The SpacetimeDB Python client SDK - -The SpacetimeDB client SDK for Python contains all the tools you need to build native clients for SpacetimeDB modules using Python. - -## Install the SDK - -Use pip to install the SDK: - -```bash -pip install spacetimedb-sdk -``` - -## Generate module bindings - -Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the Python interface files using the Spacetime CLI. From your project directory, run: - -```bash -mkdir -p module_bindings -spacetime generate --lang python \ - --out-dir module_bindings \ - --project-path PATH-TO-MODULE-DIRECTORY -``` - -Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. - -Import your bindings in your client's code: - -```python -import module_bindings -``` - -## Basic vs Async SpacetimeDB Client - -This SDK provides two different client modules for interacting with your SpacetimeDB module. - -The Basic client allows you to have control of the main loop of your application and you are responsible for regularly calling the client's `update` function. This is useful in settings like PyGame where you want to have full control of the main loop. - -The Async client has a run function that you call after you set up all your callbacks and it will take over the main loop and handle updating the client for you. With the async client, you can have a regular "tick" function by using the `schedule_event` function. - -## Common Client Reference - -The following functions and types are used in both the Basic and Async clients. - -### API at a glance - -| Definition | Description | -|---------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------| -| Type [`Identity`](#type-identity.) | A unique public identifier for a client. | -| Type [`Address`](#type-address.) | An opaque identifier for differentiating connections by the same `Identity`. | -| Type [`ReducerEvent`](#type-reducerevent.) | `class` containing information about the reducer that triggered a row update event. | -| Type [`module_bindings::{TABLE}`](#type-table.) | Autogenerated `class` type for a table, holding one row. | -| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](#method-filter_by_column.) | Autogenerated method to iterate over or seek subscribed rows where a column matches a value. | -| Method [`module_bindings::{TABLE}::iter`](#method-iter.) | Autogenerated method to iterate over all subscribed rows. | -| Method [`module_bindings::{TABLE}::register_row_update`](#method-register_row_update.) | Autogenerated method to register a callback that fires when a row changes. | -| Function [`module_bindings::{REDUCER_NAME}::{REDUCER_NAME}`](#function-reducer.) | Autogenerated function to invoke a reducer. | -| Function [`module_bindings::{REDUCER_NAME}::register_on_{REDUCER_NAME}`](#function-register_on_reducer.) | Autogenerated function to register a callback to run whenever the reducer is invoked. | - -### Type `Identity` - -```python -class Identity: - @staticmethod - def from_string(string) - - @staticmethod - def from_bytes(data) - - def __str__(self) - - def __eq__(self, other) -``` - -| Member | Args | Meaning | -| ------------- | ---------- | ------------------------------------ | -| `from_string` | `str` | Create an Identity from a hex string | -| `from_bytes` | `bytes` | Create an Identity from raw bytes | -| `__str__` | `None` | Convert the Identity to a hex string | -| `__eq__` | `Identity` | Compare two Identities for equality | - -A unique public identifier for a user of a database. - -### Type `Address` - -```python -class Address: - @staticmethod - def from_string(string) - - @staticmethod - def from_bytes(data) - - def __str__(self) - - def __eq__(self, other) -``` - -| Member | Type | Meaning | -|---------------|-----------|-------------------------------------| -| `from_string` | `str` | Create an Address from a hex string | -| `from_bytes` | `bytes` | Create an Address from raw bytes | -| `__str__` | `None` | Convert the Address to a hex string | -| `__eq__` | `Address` | Compare two Identities for equality | - -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity.). - -### Type `ReducerEvent` - -```python -class ReducerEvent: - def __init__(self, caller_identity, reducer_name, status, message, args): - self.caller_identity = caller_identity - self.reducer_name = reducer_name - self.status = status - self.message = message - self.args = args -``` - -| Member | Type | Meaning | -|-------------------|---------------------|------------------------------------------------------------------------------------| -| `caller_identity` | `Identity` | The identity of the user who invoked the reducer | -| `caller_address` | `Optional[Address]` | The address of the user who invoked the reducer, or `None` for scheduled reducers. | -| `reducer_name` | `str` | The name of the reducer that was invoked | -| `status` | `str` | The status of the reducer invocation ("committed", "failed", "outofenergy") | -| `message` | `str` | The message returned by the reducer if it fails | -| `args` | `List[str]` | The arguments passed to the reducer | - -This class contains the information about a reducer event to be passed to row update callbacks. - -### Type `{TABLE}` - -```python -class TABLE: - is_table_class = True - - primary_key = "identity" - - @classmethod - def register_row_update(cls, callback: Callable[[str,TABLE,TABLE,ReducerEvent], None]) - - @classmethod - def iter(cls) -> Iterator[User] - - @classmethod - def filter_by_COLUMN_NAME(cls, COLUMN_VALUE) -> TABLE -``` - -This class is autogenerated for each table in your module. It contains methods for filtering and iterating over subscribed rows. - -### Method `filter_by_{COLUMN}` - -```python -def filter_by_COLUMN(self, COLUMN_VALUE) -> TABLE -``` - -| Argument | Type | Meaning | -| -------------- | ------------- | ---------------------- | -| `column_value` | `COLUMN_TYPE` | The value to filter by | - -For each column of a table, `spacetime generate` generates a `classmethod` on the [table class](#type-table.) to filter or seek 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 depends on the column's attributes: - -- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filter_by` method returns a `{TABLE}` or None, where `{TABLE}` is the [table struct](#type-table.). -- For non-unique columns, the `filter_by` method returns an `Iterator` that can be used in a `for` loop. - -### Method `iter` - -```python -def iter(self) -> Iterator[TABLE] -``` - -Iterate over all the subscribed rows in the table. - -### Method `register_row_update` - -```python -def register_row_update(self, callback: Callable[[str,TABLE,TABLE,ReducerEvent], None]) -``` - -| Argument | Type | Meaning | -| ---------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | -| `callback` | `Callable[[str,TABLE,TABLE,ReducerEvent]` | Callback to be invoked when a row is updated (Args: row_op, old_value, new_value, reducer_event) | - -Register a callback function to be executed when a row is updated. Callback arguments are: - -- `row_op`: The type of row update event. One of `"insert"`, `"delete"`, or `"update"`. -- `old_value`: The previous value of the row, `None` if the row was inserted. -- `new_value`: The new value of the row, `None` if the row was deleted. -- `reducer_event`: The [`ReducerEvent`](#type-reducerevent.) that caused the row update, or `None` if the row was updated as a result of a subscription change. - -### Function `{REDUCER_NAME}` - -```python -def {REDUCER_NAME}(arg1, arg2) -``` - -This function is autogenerated for each reducer in your module. It is used to invoke the reducer. The arguments match the arguments defined in the reducer's `#[reducer]` attribute. - -### Function `register_on_{REDUCER_NAME}` - -```python -def register_on_{REDUCER_NAME}(callback: Callable[[Identity, Optional[Address], str, str, ARG1_TYPE, ARG1_TYPE], None]) -``` - -| Argument | Type | Meaning | -| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -| `callback` | `Callable[[Identity, str, str, ARG1_TYPE, ARG1_TYPE], None]` | Callback to be invoked when the reducer is invoked (Args: caller_identity, status, message, args) | - -Register a callback function to be executed when the reducer is invoked. Callback arguments are: - -- `caller_identity`: The identity of the user who invoked the reducer. -- `caller_address`: The address of the user who invoked the reducer, or `None` for scheduled reducers. -- `status`: The status of the reducer invocation ("committed", "failed", "outofenergy"). -- `message`: The message returned by the reducer if it fails. -- `args`: Variable number of arguments passed to the reducer. - -## Async Client Reference - -### API at a glance - -| Definition | Description | -| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| Function [`SpacetimeDBAsyncClient::run`](#function-run.) | Run the client. This function will not return until the client is closed. | -| Function [`SpacetimeDBAsyncClient::subscribe`](#function-subscribe.) | Subscribe to receive data and transaction updates for the provided queries. | -| Function [`SpacetimeDBAsyncClient::register_on_subscription_applied`](#function-register_on_subscription_applied.) | Register a callback when the local cache is updated as a result of a change to the subscription queries. | -| Function [`SpacetimeDBAsyncClient::force_close`](#function-force_close.) | Signal the client to stop processing events and close the connection to the server. | -| Function [`SpacetimeDBAsyncClient::schedule_event`](#function-schedule_event.) | Schedule an event to be fired after a delay | - -### Function `run` - -```python -async def run( - self, - auth_token, - host, - address_or_name, - ssl_enabled, - on_connect, - subscription_queries=[], - ) -``` - -Run the client. This function will not return until the client is closed. - -| Argument | Type | Meaning | -| ---------------------- | --------------------------------- | -------------------------------------------------------------- | -| `auth_token` | `str` | Auth token to authenticate the user. (None if new user) | -| `host` | `str` | Hostname of SpacetimeDB server | -| `address_or_name` | `&str` | Name or address of the module. | -| `ssl_enabled` | `bool` | Whether to use SSL when connecting to the server. | -| `on_connect` | `Callable[[str, Identity], None]` | Callback to be invoked when the client connects to the server. | -| `subscription_queries` | `List[str]` | List of queries to subscribe to. | - -If `auth_token` is not None, they will be passed to the new connection to identify and authenticate the user. Otherwise, a new Identity and auth token will be generated by the server. An optional [local_config](#local_config.) module can be used to store the user's auth token to local storage. - -If you are connecting to SpacetimeDB Cloud `testnet` the host should be `testnet.spacetimedb.com` and `ssl_enabled` should be `True`. If you are connecting to SpacetimeDB Standalone locally, the host should be `localhost:3000` and `ssl_enabled` should be `False`. For instructions on how to deploy to these environments, see the [Deployment Section](testnet.) - -```python -asyncio.run( - spacetime_client.run( - AUTH_TOKEN, - "localhost:3000", - "my-module-name", - False, - on_connect, - ["SELECT * FROM User", "SELECT * FROM Message"], - ) -) -``` - -### Function `subscribe` - -```rust -def subscribe(self, queries: List[str]) -``` - -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -| Argument | Type | Meaning | -| --------- | ----------- | ---------------------------- | -| `queries` | `List[str]` | SQL queries to subscribe to. | - -The `queries` should be a slice of strings representing SQL queries. - -A new call to `subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache. Row update events will be dispatched for any inserts and deletes that occur as a result of the new queries. For these events, the [`ReducerEvent`](#type-reducerevent.) argument will be `None`. - -This should be called before the async client is started with [`run`](#function-run.). - -```python -spacetime_client.subscribe(["SELECT * FROM User;", "SELECT * FROM Message;"]) -``` - -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -### Function `register_on_subscription_applied` - -```python -def register_on_subscription_applied(self, callback) -``` - -Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. - -| Argument | Type | Meaning | -| ---------- | -------------------- | ------------------------------------------------------ | -| `callback` | `Callable[[], None]` | Callback to be invoked when subscriptions are applied. | - -The callback will be invoked after a successful [`subscribe`](#function-subscribe.) call when the initial set of matching rows becomes available. - -```python -spacetime_client.register_on_subscription_applied(on_subscription_applied) -``` - -### Function `force_close` - -```python -def force_close(self) -) -``` - -Signal the client to stop processing events and close the connection to the server. - -```python -spacetime_client.force_close() -``` - -### Function `schedule_event` - -```python -def schedule_event(self, delay_secs, callback, *args) -``` - -Schedule an event to be fired after a delay - -To create a repeating event, call schedule_event() again from within the callback function. - -| Argument | Type | Meaning | -| ------------ | -------------------- | -------------------------------------------------------------- | -| `delay_secs` | `float` | number of seconds to wait before firing the event | -| `callback` | `Callable[[], None]` | Callback to be invoked when the event fires. | -| `args` | `*args` | Variable number of arguments to pass to the callback function. | - -```python -def application_tick(): - # ... do some work - - spacetime_client.schedule_event(0.1, application_tick) - -spacetime_client.schedule_event(0.1, application_tick) -``` - -## Basic Client Reference - -### API at a glance - -| Definition | Description | -|------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| -| Function [`SpacetimeDBClient::init`](#function-init.) | Create a network manager instance. | -| Function [`SpacetimeDBClient::subscribe`](#function-subscribe.) | Subscribe to receive data and transaction updates for the provided queries. | -| Function [`SpacetimeDBClient::register_on_event`](#function-register_on_event.) | Register a callback function to handle transaction update events. | -| Function [`SpacetimeDBClient::unregister_on_event`](#function-unregister_on_event.) | Unregister a callback function that was previously registered using `register_on_event`. | -| Function [`SpacetimeDBClient::register_on_subscription_applied`](#function-register_on_subscription_applied.) | Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. | -| Function [`SpacetimeDBClient::unregister_on_subscription_applied`](#function-unregister_on_subscription_applied.) | Unregister a callback function from the subscription update event. | -| Function [`SpacetimeDBClient::update`](#function-update.) | Process all pending incoming messages from the SpacetimeDB module. | -| Function [`SpacetimeDBClient::close`](#function-close.) | Close the WebSocket connection. | -| Type [`TransactionUpdateMessage`](#type-transactionupdatemessage.) | Represents a transaction update message. | - -### Function `init` - -```python -@classmethod -def init( - auth_token: str, - host: str, - address_or_name: str, - ssl_enabled: bool, - autogen_package: module, - on_connect: Callable[[], NoneType] = None, - on_disconnect: Callable[[str], NoneType] = None, - on_identity: Callable[[str, Identity, Address], NoneType] = None, - on_error: Callable[[str], NoneType] = None -) -``` - -Create a network manager instance. - -| Argument | Type | Meaning | -|-------------------|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `auth_token` | `str` | This is the token generated by SpacetimeDB that matches the user's identity. If None, token will be generated | -| `host` | `str` | Hostname:port for SpacetimeDB connection | -| `address_or_name` | `str` | The name or address of the database to connect to | -| `ssl_enabled` | `bool` | Whether to use SSL when connecting to the server. | -| `autogen_package` | `ModuleType` | Python package where SpacetimeDB module generated files are located. | -| `on_connect` | `Callable[[], None]` | Optional callback called when a connection is made to the SpacetimeDB module. | -| `on_disconnect` | `Callable[[str], None]` | Optional callback called when the Python client is disconnected from the SpacetimeDB module. The argument is the close message. | -| `on_identity` | `Callable[[str, Identity, Address], None]` | Called when the user identity is recieved from SpacetimeDB. First argument is the auth token used to login in future sessions. Third argument is the client connection's [`Address`](#type-address.). | -| `on_error` | `Callable[[str], None]` | Optional callback called when the Python client connection encounters an error. The argument is the error message. | - -This function creates a new SpacetimeDBClient instance. It should be called before any other functions in the SpacetimeDBClient class. This init will call connect for you. - -```python -SpacetimeDBClient.init(autogen, on_connect=self.on_connect) -``` - -### Function `subscribe` - -```python -def subscribe(queries: List[str]) -``` - -Subscribe to receive data and transaction updates for the provided queries. - -| Argument | Type | Meaning | -| --------- | ----------- | -------------------------------------------------------------------------------------------------------- | -| `queries` | `List[str]` | A list of queries to subscribe to. Each query is a string representing an sql formatted query statement. | - -This function sends a subscription request to the SpacetimeDB module, indicating that the client wants to receive data and transaction updates related to the specified queries. - -```python -queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] -SpacetimeDBClient.instance.subscribe(queries) -``` - -### Function `register_on_event` - -```python -def register_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) -``` - -Register a callback function to handle transaction update events. - -| Argument | Type | Meaning | -| ---------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `callback` | `Callable[[TransactionUpdateMessage], None]` | A callback function that takes a single argument of type `TransactionUpdateMessage`. This function will be invoked with a `TransactionUpdateMessage` instance containing information about the transaction update event. | - -This function registers a callback function that will be called when a reducer modifies a table matching any of the subscribed queries or if a reducer called by this Python client encounters a failure. - -```python -def handle_event(transaction_update): - # Code to handle the transaction update event - -SpacetimeDBClient.instance.register_on_event(handle_event) -``` - -### Function `unregister_on_event` - -```python -def unregister_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) -``` - -Unregister a callback function that was previously registered using `register_on_event`. - -| Argument | Type | Meaning | -| ---------- | -------------------------------------------- | ------------------------------------ | -| `callback` | `Callable[[TransactionUpdateMessage], None]` | The callback function to unregister. | - -```python -SpacetimeDBClient.instance.unregister_on_event(handle_event) -``` - -### Function `register_on_subscription_applied` - -```python -def register_on_subscription_applied(callback: Callable[[], NoneType]) -``` - -Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. - -| Argument | Type | Meaning | -| ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `callback` | `Callable[[], None]` | A callback function that will be invoked on each subscription update. The callback function should not accept any arguments and should not return any value. | - -```python -def subscription_callback(): - # Code to be executed on each subscription update - -SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) -``` - -### Function `unregister_on_subscription_applied` - -```python -def unregister_on_subscription_applied(callback: Callable[[], NoneType]) -``` - -Unregister a callback function from the subscription update event. - -| Argument | Type | Meaning | -| ---------- | -------------------- | -------------------------------------------------------------------------------------------------------- | -| `callback` | `Callable[[], None]` | A callback function that was previously registered with the `register_on_subscription_applied` function. | - -```python -def subscription_callback(): - # Code to be executed on each subscription update - -SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) -``` - -### Function `update` - -```python -def update() -``` - -Process all pending incoming messages from the SpacetimeDB module. - -This function must be called on a regular interval in the main loop to process incoming messages. - -```python -while True: - SpacetimeDBClient.instance.update() # Call the update function in a loop to process incoming messages - # Additional logic or code can be added here -``` - -### Function `close` - -```python -def close() -``` - -Close the WebSocket connection. - -This function closes the WebSocket connection to the SpacetimeDB module. - -```python -SpacetimeDBClient.instance.close() -``` - -### Type `TransactionUpdateMessage` - -```python -class TransactionUpdateMessage: - def __init__( - self, - caller_identity: Identity, - status: str, - message: str, - reducer_name: str, - args: Dict - ) -``` - -| Member | Args | Meaning | -| ----------------- | ---------- | ------------------------------------------------- | -| `caller_identity` | `Identity` | The identity of the caller. | -| `status` | `str` | The status of the transaction. | -| `message` | `str` | A message associated with the transaction update. | -| `reducer_name` | `str` | The reducer used for the transaction. | -| `args` | `Dict` | Additional arguments for the transaction. | - -Represents a transaction update message. Used in on_event callbacks. - -For more details, see [`register_on_event`](#function-register_on_event.). diff --git a/Writerside/topics/sdks/python/python_quickstart.md b/Writerside/topics/sdks/python/python_quickstart.md deleted file mode 100644 index fe6dbc22..00000000 --- a/Writerside/topics/sdks/python/python_quickstart.md +++ /dev/null @@ -1,379 +0,0 @@ -# Python Client SDK Quick Start - -In this guide, we'll show you how to get up and running with a simple SpacetimDB app with a client written in Python. - -We'll implement a command-line client for the module created in our [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart6.) guides. Make sure you follow one of these guides before you start on this one. - -## Install the SpacetimeDB SDK Python Package - -1. Run pip install - -```bash -pip install spacetimedb_sdk -``` - -## Project structure - -Enter the directory `quickstart-chat` you created in the Rust or C# Module Quickstart guides and create a `client` folder: - -```bash -cd quickstart-chat -mkdir client -``` - -## Create the Python main file - -Create a file called `main.py` in the `client` and open it in your favorite editor. We prefer [VS Code](https://code.visualstudio.com/). - -## Add imports - -We need to add several imports for this quickstart: - -- [`asyncio`](https://docs.python.org/3/library/asyncio.html) is required to run the async code in the SDK. -- [`multiprocessing.Queue`](https://docs.python.org/3/library/multiprocessing.html) allows us to pass our input to the async code, which we will run in a separate thread. -- [`threading`](https://docs.python.org/3/library/threading.html) allows us to spawn our async code in a separate thread so the main thread can run the input loop. - -- `spacetimedb_sdk.spacetimedb_async_client.SpacetimeDBAsyncClient` is the async wrapper around the SpacetimeDB client which we use to interact with our SpacetimeDB module. -- `spacetimedb_sdk.local_config` is an optional helper module to load the auth token from local storage. - -```python -import asyncio -from multiprocessing import Queue -import threading - -from spacetimedb_sdk.spacetimedb_async_client import SpacetimeDBAsyncClient -import spacetimedb_sdk.local_config as local_config -``` - -## Generate your module types - -The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. - -In your `client` directory, run: - -```bash -mkdir -p module_bindings -spacetime generate --lang python --out-dir module_bindings --project-path ../server -``` - -Take a look inside `client/module_bindings`. The CLI should have generated five files: - -``` -module_bindings -+-- message.py -+-- send_message_reducer.py -+-- set_name_reducer.py -+-- user.py -``` - -Now we import these types by adding the following lines to `main.py`: - -```python -import module_bindings -from module_bindings.user import User -from module_bindings.message import Message -import module_bindings.send_message_reducer as send_message_reducer -import module_bindings.set_name_reducer as set_name_reducer -``` - -## Global variables - -Next we will add our global `input_queue` and `local_identity` variables which we will explain later when they are used. - -```python -input_queue = Queue() -local_identity = None -``` - -## Define 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 four things: - -1. Init the optional local config module. The first parameter is the directory name to be created in the user home directory. -1. Create our async SpacetimeDB client. -1. Register our callbacks. -1. Start the async client in a thread. -1. Run a loop to read user input and send it to a repeating event in the async client. -1. When the user exits, stop the async client and exit the program. - -```python -if __name__ == "__main__": - local_config.init(".spacetimedb-python-quickstart") - - spacetime_client = SpacetimeDBAsyncClient(module_bindings) - - register_callbacks(spacetime_client) - - thread = threading.Thread(target=run_client, args=(spacetime_client,)) - thread.start() - - input_loop() - - spacetime_client.force_close() - thread.join() -``` - -## Register callbacks - -We need to handle several sorts of events: - -1. OnSubscriptionApplied is a special callback that is executed when the local client cache is populated. We will talk more about this later. -2. When a new user joins or a user is updated, we'll print an appropriate message. -3. When we receive a new message, we'll print it. -4. If the server rejects our attempt to set our name, we'll print an error. -5. If the server rejects a message we send, we'll print an error. -6. We use the `schedule_event` function to register a callback to be executed after 100ms. This callback will check the input queue for any user input and execute the appropriate command. - -Because python requires functions to be defined before they're used, the following code must be added to `main.py` before main block: - -```python -def register_callbacks(spacetime_client): - spacetime_client.client.register_on_subscription_applied(on_subscription_applied) - - User.register_row_update(on_user_row_update) - Message.register_row_update(on_message_row_update) - - set_name_reducer.register_on_set_name(on_set_name_reducer) - send_message_reducer.register_on_send_message(on_send_message_reducer) - - spacetime_client.schedule_event(0.1, check_commands) -``` - -### Handling User row updates - -For each table, we can register a row update callback to be run whenever a subscribed row is inserted, updated or deleted. We register these callbacks using the `register_row_update` methods that are generated automatically 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::row_update` callbacks may be invoked with users who are offline. We'll only notify about online users. - -We are also going to check for updates to the user row. This can happen for three reasons: - -1. They've set their name using the `set_name` reducer. -2. They're an existing user re-connecting, so their `online` has been set to `true`. -3. They've disconnected, so their `online` has been set to `false`. - -We'll print an appropriate message in each of these cases. - -`row_update` callbacks take four arguments: the row operation ("insert", "update", or "delete"), the old row if it existed, the new or updated row, and a `ReducerEvent`. This will `None` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an class that contains information about the reducer that triggered this row update event. - -Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `user_name_or_identity` handle this. - -Add these functions before the `register_callbacks` function: - -```python -def user_name_or_identity(user): - if user.name: - return user.name - else: - return (str(user.identity))[:8] - -def on_user_row_update(row_op, user_old, user, reducer_event): - if row_op == "insert": - if user.online: - print(f"User {user_name_or_identity(user)} connected.") - elif row_op == "update": - if user_old.online and not user.online: - print(f"User {user_name_or_identity(user)} disconnected.") - elif not user_old.online and user.online: - print(f"User {user_name_or_identity(user)} connected.") - - if user_old.name != user.name: - print( - f"User {user_name_or_identity(user_old)} renamed to {user_name_or_identity(user)}." - ) -``` - -### 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 `on_message_row_update` callback will check if its `reducer_event` argument is not `None`, and only print in that case. - -To find the `User` based on the message's `sender` identity, we'll use `User::filter_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filter_by_identity` accepts a `bytes`, rather than an `&Identity`. The `sender` identity stored in the message is also a `bytes`, not an `Identity`, so we can just pass it to the filter method. - -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`. - -Add these functions before the `register_callbacks` function: - -```python -def on_message_row_update(row_op, message_old, message, reducer_event): - if reducer_event is not None and row_op == "insert": - print_message(message) - -def print_message(message): - user = User.filter_by_identity(message.sender) - user_name = "unknown" - if user is not None: - user_name = user_name_or_identity(user) - - print(f"{user_name}: {message.text}") -``` - -### Warn if our name was rejected - -We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `register_on_` method, which is automatically implemented for each reducer by `spacetime generate`. - -Each reducer callback takes four fixed arguments: - -1. The `Identity` of the client who requested the reducer invocation. -2. The `Address` of the client who requested the reducer invocation, or `None` for scheduled reducers. -3. The `Status` of the reducer run, one of `committed`, `failed` or `outofenergy`. -4. The `Message` returned by the reducer in error cases, or `None` if the reducer succeeded. - -It also takes a variable number of arguments which match the calling arguments of the reducer. - -These callbacks will be invoked in one of two cases: - -1. If the reducer was successful and altered any of our subscribed rows. -2. If we requested an invocation which failed. - -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 `on_set_name_reducer` as a callback which checks if the reducer failed, and if it did, prints an error message including the rejected name. - -We'll test both that our identity matches the sender and that the status is `failed`, even though the latter implies the former, for demonstration purposes. - -Add this function before the `register_callbacks` function: - -```python -def on_set_name_reducer(sender_id, sender_address, status, message, name): - if sender_id == local_identity: - if status == "failed": - print(f"Failed to set name: {message}") -``` - -### 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. - -Add this function before the `register_callbacks` function: - -```python -def on_send_message_reducer(sender_id, sender_address, status, message, msg): - if sender_id == local_identity: - if status == "failed": - print(f"Failed to send message: {message}") -``` - -### OnSubscriptionApplied callback - -This callback fires after the client cache is updated as a result in a change to the client subscription. This happens after connect and if after calling `subscribe` to modify the subscription. - -In this case, we want to print all the existing messages when the subscription is applied. `print_messages_in_order` iterates over all the `Message`s we've received, sorts them, and then prints them. `Message.iter()` is generated for all table types, and returns an iterator over all the messages in the client's cache. - -Add these functions before the `register_callbacks` function: - -```python -def print_messages_in_order(): - all_messages = sorted(Message.iter(), key=lambda x: x.sent) - for entry in all_messages: - print(f"{user_name_or_identity(User.filter_by_identity(entry.sender))}: {entry.text}") - -def on_subscription_applied(): - print(f"\nSYSTEM: Connected.") - print_messages_in_order() -``` - -### Check commands repeating event - -We'll use a repeating event to check the user input queue every 100ms. If there's a command in the queue, we'll execute it. If not, we'll just keep waiting. Notice that at the end of the function we call `schedule_event` again to so the event will repeat. - -If the command is to send a message, we'll call the `send_message` reducer. If the command is to set our name, we'll call the `set_name` reducer. - -Add these functions before the `register_callbacks` function: - -```python -def check_commands(): - global input_queue - - if not input_queue.empty(): - choice = input_queue.get() - if choice[0] == "name": - set_name_reducer.set_name(choice[1]) - else: - send_message_reducer.send_message(choice[1]) - - spacetime_client.schedule_event(0.1, check_commands) -``` - -### OnConnect callback - -This callback fires after the client connects to the server. We'll use it to save our credentials to a file so that we can re-authenticate as the same user next time we connect. - -The `on_connect` callback takes three arguments: - -1. The `Auth Token` is the equivalent of your private key. This is the only way to authenticate with the SpacetimeDB module as this user. -2. The `Identity` is the equivalent of your public key. This is used to uniquely identify this user and will be sent to other clients. We store this in a global variable so we can use it to identify that a given message or transaction was sent by us. -3. The `Address` is an opaque identifier 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. - -To store our auth token, we use the optional component `local_config`, which provides a simple interface for storing and retrieving a single `Identity` from a file. We'll use the `local_config::set_string` method to store the auth token. Other projects might want to associate this token with some other identifier such as an email address or Steam ID. - -The `on_connect` callback is passed to the client connect function so it just needs to be defined before the `run_client` described next. - -```python -def on_connect(auth_token, identity): - global local_identity - local_identity = identity - - local_config.set_string("auth_token", auth_token) -``` - -## Async client thread - -We are going to write a function that starts the async client, which will be executed on a separate thread. - -```python -def run_client(spacetime_client): - asyncio.run( - spacetime_client.run( - local_config.get_string("auth_token"), - "localhost:3000", - "chat", - False, - on_connect, - ["SELECT * FROM User", "SELECT * FROM Message"], - ) - ) -``` - -## Input loop - -Finally, we need a function to be executed on the main loop which listens for user input and adds it to the queue. - -```python -def input_loop(): - global input_queue - - while True: - user_input = input() - if len(user_input) == 0: - return - elif user_input.startswith("/name "): - input_queue.put(("name", user_input[6:])) - else: - input_queue.put(("message", user_input)) -``` - -## Run the client - -Make sure your module from the Rust or C# module quickstart is published. If you used a different module name than `chat`, you will need to update the `connect` call in the `run_client` function. - -Run the client: - -```bash -python main.py -``` - -If you want to connect another client, you can use the --client command line option, which is built into the local_config module. This will create different settings file for the new client's auth token. - -```bash -python main.py --client 2 -``` - -## Next steps - -Congratulations! You've built a simple chat app with a Python client. You can now use this as a starting point for your own SpacetimeDB apps. - -For a more complex example of the Spacetime Python SDK, check out our [AI Agent](https://github.com/clockworklabs/spacetime-mud/tree/main/ai-agent-python-client) for the [Spacetime Multi-User Dungeon](https://github.com/clockworklabs/spacetime-mud). The AI Agent uses the OpenAI API to create dynamic content on command. diff --git a/Writerside/topics/sdks/rust/sdks_rust_index.md b/Writerside/topics/sdks/rust/sdks_rust_index.md deleted file mode 100644 index 239cabff..00000000 --- a/Writerside/topics/sdks/rust/sdks_rust_index.md +++ /dev/null @@ -1,1183 +0,0 @@ -# The SpacetimeDB Rust client SDK - -The SpacetimeDB client SDK for Rust contains all the tools you need to build native clients for SpacetimeDB modules using Rust. - -## Install the SDK - -First, create a new project using `cargo new` and add the SpacetimeDB SDK to your dependencies: - -```bash -cargo add spacetimedb -``` - -## Generate module bindings - -Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's `src` directory and generate the Rust interface files using the Spacetime CLI. From your project directory, run: - -```bash -mkdir -p src/module_bindings -spacetime generate --lang rust \ - --out-dir src/module_bindings \ - --project-path PATH-TO-MODULE-DIRECTORY -``` - -Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. - -Declare a `mod` for the bindings in your client's `src/main.rs`: - -```rust -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`](rust_#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`](rust_#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`](rust_#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`](rust_#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}`](rust_#type-table.) | Autogenerated `struct` type for a table, holding one row. | -| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](rust_#method-filter_by_column.) | Autogenerated method to iterate over or seek subscribed rows where a 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`](rust_#method-iter.) | Iterate over all subscribed rows. | -| Method [`spacetimedb_sdk::table::TableType::filter`](rust_#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`](rust_#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}`](rust_#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` - -```rust -module_bindings::connect( - spacetimedb_uri: impl TryInto, - db_name: &str, - credentials: Option, -) -> anyhow::Result<()> -``` - -Connect to a database named `db_name` accessible over the internet at the URI `spacetimedb_uri`. - -| 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` - -```rust -spacetimedb_sdk::disconnect() -``` - -Gracefully close the current WebSocket connection. - -If there is no active connection, this operation does nothing. - -```rust -connect(SPACETIMEDB_URI, MODULE_NAME, credentials) - .expect("Connection failed"); - -run_app(); - -disconnect(); -``` - -### 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!" -``` - -### Function `once_on_disconnect` - -```rust -spacetimedb_sdk::once_on_disconnect( - callback: impl FnOnce() + Send + 'static, -) -> DisconnectCallbackId -``` - -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. - -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. - -```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. -``` - -### Function `remove_on_disconnect` - -```rust -spacetimedb_sdk::remove_on_disconnect( - id: DisconnectCallbackId, -) -``` - -Unregister a previously-registered [`on_disconnect`](#function-on_disconnect.) callback. - -| Argument | Type | Meaning | -| -------- | ---------------------- | ------------------------------------------ | -| `id` | `DisconnectCallbackId` | 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_disconnect(|| unreachable!()); - -remove_on_disconnect(id); - -disconnect(); - -// No `unreachable` panic. -``` - -## Subscribe to queries - -### Function `subscribe` - -```rust -spacetimedb_sdk::subscribe(queries: &[&str]) -> anyhow::Result<()> -``` - -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}`](rust_#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. - -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. - -```rust -subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) - .expect("Called `subscribe` before `connect`"); -``` - -### Function `subscribe_owned` - -```rust -spacetimedb_sdk::subscribe_owned(queries: Vec) -> 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. - -A new call to `subscribe_owned` (or [`subscribe`](rust_#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. - -`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. - -```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` - -```rust -spacetimedb_sdk::on_subscription_applied( - callback: impl FnMut() + Send + 'static, -) -> SubscriptionCallbackId -``` - -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`](rust_#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. -``` - -### Function `once_on_subscription_applied` - -```rust -spacetimedb_sdk::once_on_subscription_applied( - callback: impl FnOnce() + Send + 'static, -) -> SubscriptionCallbackId -``` - -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`](rust_#function-subscribe.) or [`subscribe_owned`](#function-subscribe_owned.) call when the initial set of matching rows becomes available. - -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. - -```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. -``` - -### 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)); - -// Will print "Subscription applied!" - -remove_on_subscription_applied(id); - -subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) - .expect("Called `subscribe` before `connect`"); - -// Nothing printed this time. -``` - -## Identify a client - -### Type `Identity` - -```rust -spacetimedb_sdk::identity::Identity -``` - -A unique public identifier for a client connected to a database. - -### Type `Token` - -```rust -spacetimedb_sdk::identity::Token -``` - -A private access token for a client connected to a database. - -### Type `Credentials` - -```rust -spacetimedb_sdk::identity::Credentials -``` - -Credentials, including a private access token, sufficient to authenticate a client connected to a database. - -| Field | Type | -| ---------- | ---------------------------- | -| `identity` | [`Identity`](rust_#type-identity.) | -| `token` | [`Token`](#type-token.) | - -### Type `Address` - -```rust -spacetimedb_sdk::Address -``` - -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](rust_#type-identity.). - -### Function `identity` - -```rust -spacetimedb_sdk::identity::identity() -> Result -``` - -Read the current connection's public [`Identity`](rust_#type-identity.). - -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 identity is {:?}", identity()); - -// Prints "My identity is Ok(Identity { bytes: [...several u8s...] })" -``` - -### Function `token` - -```rust -spacetimedb_sdk::identity::token() -> Result -``` - -Read the current connection's private [`Token`](#type-token.). - -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` - -```rust -spacetimedb_sdk::identity::credentials() -> Result -``` - -Read the current connection's [`Credentials`](#type-credentials.), including a public [`Identity`](rust_#type-identity.) and a private [`Token`](#type-token.). - -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 credentials are {:?}", credentials()); - -// Prints "My credentials are Ok(Credentials { -// identity: Identity { bytes: [...several u8s...] }, -// token: Token { string: "...several Base64 digits..."}, -// })" -``` - -### Function `address` - -```rust -spacetimedb_sdk::identity::address() -> Result
-``` - -Read the current connection's [`Address`](rust_#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()); -``` - -### Function `on_connect` - -```rust -spacetimedb_sdk::identity::on_connect( - callback: impl FnMut(&Credentials, Address) + Send + 'static, -) -> ConnectCallbackId -``` - -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`](rust_#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. - -The returned `ConnectCallbackId` can be passed to [`remove_on_connect`](#function-remove_on_connect.) to unregister the callback. - -```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`. -``` - -### Function `once_on_connect` - -```rust -spacetimedb_sdk::identity::once_on_connect( - callback: impl FnOnce(&Credentials, Address) + Send + 'static, -) -> ConnectCallbackId -``` - -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`](rust_#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. - -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` - -```rust -spacetimedb_sdk::identity::remove_on_connect(id: ConnectCallbackId) -``` - -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. | - -If `id` does not refer to a currently-registered callback, this operation does nothing. - -```rust -let id = on_connect(|_creds, _addr| unreachable!()); - -remove_on_connect(id); - -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -sleep(Duration::from_secs(1)); - -// No `unreachable` panic. -``` - -### Function `load_credentials` - -```rust -spacetimedb_sdk::identity::load_credentials( - dirname: &str, -) -> Result> -``` - -Load a saved [`Credentials`](#type-credentials.) from a file within `~/dirname`, if one exists. - -| Argument | Type | Meaning | -| --------- | ------ | ----------------------------------------------------- | -| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | - -`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. - -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.). - -```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"); -``` - -### Function `save_credentials` - -```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.). - -| Argument | Type | Meaning | -| ------------- | -------------- | ----------------------------------------------------- | -| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | -| `credentials` | `&Credentials` | [`Credentials`](#type-credentials.) to store. | - -`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. - -Returns `Err` when IO or serialization fails. - -```rust -const CREDENTIALS_DIR = ".my-module"; - -let creds = load_credentials(CREDENTIALS_DIRectory) - .expect("Error while loading credentials"); - -on_connect(|creds, _addr| { - if let Err(e) = save_credentials(CREDENTIALS_DIR, creds) { - eprintln!("Error while saving credentials: {:?}", e); - } -}); - -connect(SPACETIMEDB_URI, DB_NAME, creds) - .expect("Failed to connect"); -``` - -## View subscribed rows of tables - -### Type `{TABLE}` - -```rust -module_bindings::{TABLE} -``` - -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}` - -```rust -module_bindings::{TABLE}::filter_by_{COLUMN}( - value: {COLUMN_TYPE}, -) -> {FILTER_RESULT}<{TABLE}> -``` - -For each column of a table, `spacetime generate` generates a static method on the [table struct](rust_#type-table.) to filter or seek 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 depends on the column's attributes: - -- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filter_by` method returns an `Option<{TABLE}>`, where `{TABLE}` is the [table struct](rust_#type-table.). -- For non-unique columns, the `filter_by` method returns an `impl Iterator`. - -### Trait `TableType` - -```rust -spacetimedb_sdk::table::TableType -``` - -Every [generated table struct](rust_#type-table.) implements the trait `TableType`. - -#### Method `count` - -```rust -TableType::count() -> usize -``` - -Return the number of subscribed rows in the table, or 0 if there is no active connection. - -This method acquires a global lock. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -on_subscription_applied(|| println!("There are {} users", User::count())); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -sleep(Duration::from_secs(1)); - -// Will the number of `User` rows in the database. -``` - -#### Method `iter` - -```rust -TableType::iter() -> impl Iterator -``` - -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`](rust_#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. - -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](rust_#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` - -```rust -TableType::find( - predicate: impl FnMut(&Self) -> bool, -) -> Option -``` - -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 `filter_by_{COLUMN}` methods](rust_#method-filter_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. -``` - -#### Method `on_insert` - -```rust -TableType::on_insert( - callback: impl FnMut(&Self, Option<&ReducerEvent>) + Send + 'static, -) -> InsertCallbackId -``` - -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`](rust_#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(); - -sleep(Duration::from_secs(1)); - -// Will print a note whenever a new `User` row is inserted. -``` - -#### Method `remove_on_insert` - -```rust -TableType::remove_on_insert(id: InsertCallbackId) -``` - -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. | - -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_insert(|_, _| unreachable!()); - -User::remove_on_insert(id); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -sleep(Duration::from_secs(1)); - -// No `unreachable` panic. -``` - -#### Method `on_delete` - -```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: - -- `row: &Self`, the previously-present row which is no longer resident in the database. -- `reducer_event: Option<&ReducerEvent>`, the [`ReducerEvent`](rust_#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. - -```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)); - -// 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" }" -``` - -#### 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. -``` - -### Trait `TableWithPrimaryKey` - -```rust -spacetimedb_sdk::table::TableWithPrimaryKey -``` - -[Generated table structs](rust_#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`](rust_#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) -``` - -| 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. - -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_update(|_, _, _| unreachable!); - -User::remove_on_update(id); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -// No `unreachable` panic. -``` - -## Observe and request reducer invocations - -### Type `ReducerEvent` - -```rust -module_bindings::ReducerEvent -``` - -`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.). - -[`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. - -### Type `{REDUCER}Args` - -```rust -module_bindings::{REDUCER}Args -``` - -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`. - -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...}) -``` - -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`](rust_#type-identity.) of the client which invoked the reducer. -- `caller_address: Option
`, the [`Address`](rust_#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. - -### Function `remove_on_{REDUCER}` - -```rust -module_bindings::remove_on_{REDUCER}(id: ReducerCallbackId<{REDUCER}Args>) -``` - -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. - -### Type `Status` - -```rust -spacetimedb_sdk::reducer::Status -``` - -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. diff --git a/Writerside/topics/sdks/rust/sdks_rust_quickstart.md b/Writerside/topics/sdks/rust/sdks_rust_quickstart.md deleted file mode 100644 index f6049bf5..00000000 --- a/Writerside/topics/sdks/rust/sdks_rust_quickstart.md +++ /dev/null @@ -1,487 +0,0 @@ -# Rust Client SDK Quick Start - -In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in Rust. - -We'll implement a command-line client for the module created in our Rust or C# Module Quickstart guides. Make sure you follow one of these guides before you start on this one. - -## Project structure - -Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: - -```bash -cd quickstart-chat -``` - -Within it, create a `client` crate, our client application, which users run locally: - -```bash -cargo new client -``` - -## Depend on `spacetimedb-sdk` and `hex` - -`client/Cargo.toml` should be initialized without any dependencies. We'll need two: - -- [`spacetimedb-sdk`](https://crates.io/crates/spacetimedb-sdk), which defines client-side interfaces for interacting with a remote SpacetimeDB module. -- [`hex`](https://crates.io/crates/hex), which we'll use to print unnamed users' identities as hexadecimal strings. - -Below the `[dependencies]` line in `client/Cargo.toml`, add: - -```toml -spacetimedb-sdk = "0.7" -hex = "0.4" -``` - -Make sure you depend on the same version of `spacetimedb-sdk` as is reported by the SpacetimeDB CLI tool's `spacetime version`! - -## Clear `client/src/main.rs` - -`client/src/main.rs` should be initialized with a trivial "Hello world" program. Clear it out so we can write our chat client. - -In your `quickstart-chat` directory, run: - -```bash -rm client/src/main.rs -touch client/src/main.rs -``` - -## Generate your module types - -The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. - -In your `quickstart-chat` directory, run: - -```bash -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: - -``` -module_bindings -├── message.rs -├── mod.rs -├── send_message_reducer.rs -├── set_name_reducer.rs -└── user.rs -``` - -We need to declare the module in our client crate, and we'll want to import its definitions. - -To `client/src/main.rs`, add: - -```rust -mod module_bindings; -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. - -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}, -}; -``` - -## Define 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: - -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: - -```rust -fn main() { - register_callbacks(); - connect_to_db(); - subscribe_to_tables(); - user_input_loop(); -} -``` - -## Register callbacks - -We need to handle several sorts of events: - -1. When we connect and receive our credentials, we'll save them to a file so that the next time we connect, we can re-authenticate as the same user. -2. When a new user joins, we'll print a message introducing them. -3. When a user is updated, we'll print their new name, or declare their new online status. -4. When we receive a new message, we'll print it. -5. When we're informed of the backlog of past messages, we'll sort them and print them in order. -6. If the server rejects our attempt to set our name, we'll print an error. -7. If the server rejects a message we send, we'll print an error. -8. When our connection ends, we'll print a note, then exit the process. - -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); - - // When a new user joins, print a notification. - User::on_insert(on_user_inserted); - - // When a user's status changes, print a notification. - User::on_update(on_user_updated); - - // When a new message is received, print it. - Message::on_insert(on_message_inserted); - - // When we receive the message backlog, print it in timestamp order. - on_subscription_applied(on_sub_applied); - - // When we fail to set our name, print a warning. - 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); -} -``` - -### 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. - -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) { - eprintln!("Failed to save credentials: {:?}", e); - } -} - -const CREDS_DIR: &str = ".spacetime_chat"; -``` - -### 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`. - -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. - -`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. - -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. - -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>) { - if user.online { - println!("User {} connected.", user_name_or_identity(user)); - } -} - -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]) -} -``` - -### 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. - -`on_update` callbacks take three arguments: the old row, the new row, and an `Option<&ReducerEvent>`. - -In our module, users can be updated for three reasons: - -1. They've set their name using the `set_name` reducer. -2. They're an existing user re-connecting, so their `online` has been set to `true`. -3. They've disconnected, so their `online` has been set to `false`. - -We'll print an appropriate message in each of these cases. - -To `client/src/main.rs`, add: - -```rust -/// Our `User::on_update` callback: -/// print a notification about name and status changes. -fn on_user_updated(old: &User, new: &User, _: Option<&ReducerEvent>) { - if old.name != new.name { - println!( - "User {} renamed to {}.", - user_name_or_identity(old), - user_name_or_identity(new) - ); - } - if old.online && !new.online { - println!("User {} disconnected.", user_name_or_identity(new)); - } - if !old.online && new.online { - println!("User {} connected.", user_name_or_identity(new)); - } -} -``` - -### 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. - -To find the `User` based on the message's `sender` identity, we'll use `User::filter_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filter_by_identity` accepts an owned `Identity`, rather than a reference. We can `clone` the identity held in `message.sender`. - -We'll print the user's name or identity in the same way as we did when notifying about `User` table events, but here we have to handle the case where we don't find a matching `User` row. This can happen when the module owner sends a message using the CLI's `spacetime call`. In this case, we'll print `unknown`. - -To `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 print_message(message: &Message) { - let sender = User::filter_by_identity(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: - -```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::>(); - messages.sort_by_key(|m| m.sent); - for message in messages { - print_message(&message); - } -} -``` - -### Warn if our name was rejected - -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. - -These callbacks will be invoked in one of two cases: - -1. If the reducer was successful and altered any of our subscribed rows. -2. If we requested an invocation which failed. - -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. - -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); - } -} -``` - -### 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); - } -} -``` - -### 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 -/// The URL of the SpacetimeDB instance hosting our chat module. -const SPACETIMEDB_URI: &str = "http://localhost:3000"; - -/// The module name we chose when we published our module. -const DB_NAME: &str = ""; - -/// 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"); -} -``` - -## Subscribe to queries - -SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. - -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(); -} -``` - -## 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. - -`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`. - -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() { - 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()); - } else { - send_message(line); - } - } -} -``` - -## Run it - -Change your directory to the client app, then compile and run it. From the `quickstart-chat` directory, run: - -```bash -cd client -cargo run -``` - -You should see something like: - -``` -User d9e25c51996dea2f connected. -``` - -Now try sending a message. Type `Hello, world!` and press enter. You should see something like: - -``` -d9e25c51996dea2f: Hello, world! -``` - -Next, set your name. Type `/name `, replacing `` with your name. You should see something like: - -``` -User d9e25c51996dea2f renamed to . -``` - -Then send another message. Type `Hello after naming myself.` and press enter. You should see: - -``` -: 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: - -``` -User connected. -: Hello, world! -: Hello after naming myself. -``` - -## What's next? - -You can find the full code for this client [in the Rust SDK's examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/sdk/examples/quickstart-chat). - -Check out the [Rust SDK Reference](rust1.) 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. - -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). - -Or, you could extend the module and the client together, perhaps: - -- 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. diff --git a/Writerside/topics/sdks/sdks_index.md b/Writerside/topics/sdks/sdks_index.md deleted file mode 100644 index bcc59bfd..00000000 --- a/Writerside/topics/sdks/sdks_index.md +++ /dev/null @@ -1,74 +0,0 @@ - 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 - -- [Rust](rust1.) - [(Quickstart)](quickstart2.) -- [C#](c-sharp1.) - [(Quickstart)](quickstart3.) -- [TypeScript](typescript.) - [(Quickstart)](quickstart4.) -- [Python](python.) - [(Quickstart)](quickstart5.) - -## Key Features - -The SpacetimeDB Client SDKs offer the following key functionalities: - -### Connection Management - -The SDKs handle the process of connecting and disconnecting from the SpacetimeDB server, simplifying this process for the client applications. - -### Authentication - -The SDKs support authentication using an auth token, allowing clients to securely establish a session with the SpacetimeDB server. - -### Local Database View - -Each client can define a local view of the database via a subscription consisting of a set of queries. This local view is maintained by the server and populated into a local cache on the client side. - -### Reducer Calls - -The SDKs allow clients to call transactional functions (reducers) on the server. - -### Callback Registrations - -The SpacetimeDB Client SDKs offer powerful callback functionality that allow clients to monitor changes in their local database view. These callbacks come in two forms: - -#### Connection and Subscription Callbacks - -Clients can also register callbacks that trigger when the connection to the server is established or lost, or when a subscription is updated. This allows clients to react to changes in the connection status. - -#### Row Update Callbacks - -Clients can register callbacks that trigger when any row in their local cache is updated by the server. These callbacks contain information about the reducer that triggered the change. This feature enables clients to react to changes in data that they're interested in. - -#### Reducer Call Callbacks - -Clients can also register callbacks that fire when a reducer call modifies something in the client's local view. This allows the client to know when a transactional function it has executed has had an effect on the data it cares about. - -Additionally, when a client makes a reducer call that fails, the SDK triggers the registered reducer callback on the client that initiated the failed call with the error message that was returned from the server. This allows for appropriate error handling or user notifications. - -## Choosing a Language - -When selecting a language for your client application with SpacetimeDB, a variety of factors come into play. While the functionality of the SDKs remains consistent across different languages, the choice of language will often depend on the specific needs and context of your application. Here are a few considerations: - -### Team Expertise - -The familiarity of your development team with a particular language can greatly influence your choice. You might want to choose a language that your team is most comfortable with to increase productivity and reduce development time. - -### Application Type - -Different languages are often better suited to different types of applications. For instance, if you are developing a web-based application, you might opt for TypeScript due to its seamless integration with web technologies. On the other hand, if you're developing a desktop application, you might choose C# or Python, depending on your requirements and platform. Python is also very useful for utility scripts and tools. - -### Performance - -The performance characteristics of the different languages can also be a factor. If your application is performance-critical, you might opt for Rust, known for its speed and memory efficiency. - -### Platform Support - -The platform you're targeting can also influence your choice. For instance, if you're developing a game or a 3D application using the Unity engine, you'll want to choose the C# SDK, as Unity uses C# as its primary scripting language. - -### Ecosystem and Libraries - -Each language has its own ecosystem of libraries and tools that can help in developing your application. If there's a library in a particular language that you want to use, it may influence your choice. - -Remember, the best language to use is the one that best fits your use case and the one you and your team are most comfortable with. It's worth noting that due to the consistent functionality across different SDKs, transitioning from one language to another should you need to in the future will primarily involve syntax changes rather than changes in the application's logic. - -You may want to use multiple languages in your application. For instance, you might want to use C# in Unity for your game logic, TypeScript for a web-based administration panel, and Python for utility scripts. This is perfectly fine, as the SpacetimeDB server is completely client-agnostic. diff --git a/Writerside/topics/sdks/typescript/typescript_index.md b/Writerside/topics/sdks/typescript/typescript_index.md deleted file mode 100644 index 2316ecbb..00000000 --- a/Writerside/topics/sdks/typescript/typescript_index.md +++ /dev/null @@ -1,942 +0,0 @@ -# The SpacetimeDB Typescript client SDK - -The SpacetimeDB client SDK for TypeScript contains all the tools you need to build clients for SpacetimeDB modules using Typescript, either in the browser or with NodeJS. - -> You need a database created before use the client, so make sure to follow the Rust or C# Module Quickstart guides if need one. - -## Install the SDK - -First, create a new client project, and add the following to your `tsconfig.json` file: - -```json -{ - "compilerOptions": { - //You can use any target higher than this one - //https://www.typescriptlang.org/tsconfig#target - "target": "es2015" - } -} -``` - -Then add the SpacetimeDB SDK to your dependencies: - -```bash -cd client -npm install @clockworklabs/spacetimedb-sdk -``` - -You should have this folder layout starting from the root of your project: - -```bash -quickstart-chat -├── client -│ ├── node_modules -│ ├── public -│ └── src -└── server - └── src -``` - -### Tip for utilities/scripts - -If want to create a quick script to test your module bindings from the command line, you can use https://www.npmjs.com/package/tsx to execute TypeScript files. - -Then you create a `script.ts` file and add the imports, code and execute with: - -```bash -npx tsx src/script.ts -``` - -## Generate module bindings - -Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's `src` directory and generate the Typescript interface files using the Spacetime CLI. From your project directory, run: - -```bash -mkdir -p client/src/module_bindings -spacetime generate --lang typescript \ - --out-dir client/src/module_bindings \ - --project-path server -``` - -And now you will get the files for the `reducers` & `tables`: - -```bash -quickstart-chat -├── client -│ ├── node_modules -│ ├── public -│ └── src -| └── module_bindings -| ├── add_reducer.ts -| ├── person.ts -| └── say_hello_reducer.ts -└── server - └── src -``` - -Import the `module_bindings` in your client's _main_ file: - -```typescript -import { SpacetimeDBClient, Identity } from "@clockworklabs/spacetimedb-sdk"; - -import Person from "./module_bindings/person"; -import AddReducer from "./module_bindings/add_reducer"; -import SayHelloReducer from "./module_bindings/say_hello_reducer"; -console.log(Person, AddReducer, SayHelloReducer); -``` - -> There is a known issue where if you do not use every type in your file, it will not pull them into the published build. To fix this, we are using `console.log` to force them to get pulled in. - -## API at a glance - -### Classes - -| Class | Description | -|-------------------------------------------------|------------------------------------------------------------------------------| -| [`SpacetimeDBClient`](#class-spacetimedbclient.) | The database client connection to a SpacetimeDB server. | -| [`Identity`](typescript_#class-identity.) | The user's public identity. | -| [`Address`](typescript_#class-address.) | An opaque identifier for differentiating connections by the same `Identity`. | -| [`{Table}`](typescript_#class-table.) | `{Table}` is a placeholder for each of the generated tables. | -| [`{Reducer}`](typescript_#class-reducer.) | `{Reducer}` is a placeholder for each of the generated reducers. | - -### Class `SpacetimeDBClient` - -The database client connection to a SpacetimeDB server. - -Defined in [spacetimedb-sdk.spacetimedb](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/spacetimedb.ts): - -| Constructors | Description | -| ----------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`SpacetimeDBClient.constructor`](#spacetimedbclient-constructor.) | Creates a new `SpacetimeDBClient` database client. | -| Properties | -| [`SpacetimeDBClient.identity`](#spacetimedbclient-identity.) | The user's public identity. | -| [`SpacetimeDBClient.live`](#spacetimedbclient-live.) | Whether the client is connected. | -| [`SpacetimeDBClient.token`](#spacetimedbclient-token.) | The user's private authentication token. | -| Methods | | -| [`SpacetimeDBClient.connect`](#spacetimedbclient-connect.) | Connect to a SpacetimeDB module. | -| [`SpacetimeDBClient.disconnect`](#spacetimedbclient-disconnect.) | Close the current connection. | -| [`SpacetimeDBClient.subscribe`](#spacetimedbclient-subscribe.) | Subscribe to a set of queries. | -| Events | | -| [`SpacetimeDBClient.onConnect`](#spacetimedbclient-onconnect.) | Register a callback to be invoked upon authentication with the database. | -| [`SpacetimeDBClient.onError`](#spacetimedbclient-onerror.) | Register a callback to be invoked upon a error. | - -## Constructors - -### `SpacetimeDBClient` constructor - -Creates a new `SpacetimeDBClient` database client and set the initial parameters. - -```ts -new SpacetimeDBClient(host: string, name_or_address: string, auth_token?: string, protocol?: "binary" | "json") -``` - -#### Parameters - -| Name | Type | Description | -| :---------------- | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | -| `host` | `string` | The host of the SpacetimeDB server. | -| `name_or_address` | `string` | The name or address of the SpacetimeDB module. | -| `auth_token?` | `string` | The credentials to use to connect to authenticate with SpacetimeDB. | -| `protocol?` | `"binary"` \| `"json"` | Define how encode the messages: `"binary"` \| `"json"`. Binary is more efficient and compact, but JSON provides human-readable debug information. | - -#### Example - -```ts -const host = "ws://localhost:3000"; -const name_or_address = "database_name"; -const auth_token = undefined; -const protocol = "binary"; - -var spacetimeDBClient = new SpacetimeDBClient( - host, - name_or_address, - auth_token, - protocol -); -``` - -## Class methods - -### `SpacetimeDBClient.registerReducers` - -Registers reducer classes for use with a SpacetimeDBClient - -```ts -registerReducers(...reducerClasses: ReducerClass[]) -``` - -#### Parameters - -| Name | Type | Description | -| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | -| `reducerClasses` | `ReducerClass` | A list of classes to register | - -#### Example - -```ts -import SayHelloReducer from './types/say_hello_reducer'; -import AddReducer from './types/add_reducer'; - -SpacetimeDBClient.registerReducers(SayHelloReducer, AddReducer); -``` - ---- - -### `SpacetimeDBClient.registerTables` - -Registers table classes for use with a SpacetimeDBClient - -```ts -registerTables(...reducerClasses: TableClass[]) -``` - -#### Parameters - -| Name | Type | Description | -| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | -| `tableClasses` | `TableClass` | A list of classes to register | - -#### Example - -```ts -import User from './types/user'; -import Player from './types/player'; - -SpacetimeDBClient.registerTables(User, Player); -``` - ---- - -## Properties - -### `SpacetimeDBClient` identity - -The user's public [Identity](typescript_#class-identity.). - -``` -identity: Identity | undefined -``` - ---- - -### `SpacetimeDBClient` live - -Whether the client is connected. - -```ts -live: boolean; -``` - ---- - -### `SpacetimeDBClient` token - -The user's private authentication token. - -``` -token: string | undefined -``` - -#### Parameters - -| Name | Type | Description | -| :------------ | :----------------------------------------------------- | :------------------------------ | -| `reducerName` | `string` | The name of the reducer to call | -| `serializer` | [`Serializer`](serializer.Serializer.md) | - | - ---- - -### `SpacetimeDBClient` connect - -Connect to The SpacetimeDB Websocket For Your Module. By default, this will use a secure websocket connection. The parameters are optional, and if not provided, will use the values provided on construction of the client. - -```ts -connect(host: string?, name_or_address: string?, auth_token: string?): Promise -``` - -#### Parameters - -| Name | Type | Description | -| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | -| `host?` | `string` | The hostname of the SpacetimeDB server. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | -| `name_or_address?` | `string` | The name or address of the SpacetimeDB module. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | -| `auth_token?` | `string` | The credentials to use to authenticate with SpacetimeDB. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | - -#### Returns - -`Promise`<`void`\> - -#### Example - -```ts -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 -); -// Connect with the initial parameters -spacetimeDBClient.connect(); -//Set the `auth_token` -spacetimeDBClient.connect(undefined, undefined, NEW_TOKEN); -``` - ---- - -### `SpacetimeDBClient` disconnect - -Close the current connection. - -```ts -disconnect(): void -``` - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); - -spacetimeDBClient.disconnect(); -``` - ---- - -### `SpacetimeDBClient` subscribe - -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -> A new call to `subscribe` will remove all previous subscriptions and replace them with the new `queries`. -> If any rows matched the previous subscribed queries but do not match the new queries, -> those rows will be removed from the client cache, and [`{Table}.on_delete`](#table-ondelete.) callbacks will be invoked for them. - -```ts -subscribe(queryOrQueries: string | string[]): void -``` - -#### Parameters - -| Name | Type | Description | -| :--------------- | :--------------------- | :------------------------------- | -| `queryOrQueries` | `string` \| `string`[] | A `SQL` query or list of queries | - -#### Example - -```ts -spacetimeDBClient.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); -``` - -## Events - -### `SpacetimeDBClient` onConnect - -Register a callback to be invoked upon authentication with the database. - -```ts -onConnect(callback: (token: string, identity: Identity) => void): void -``` - -The callback will be invoked with the public user [Identity](typescript_#class-identity.), private authentication token and connection [`Address`](typescript_#class-address.) provided by the database. If credentials were supplied to [connect](#spacetimedbclient-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 will be generated by the database to identify this user. - -The credentials passed to the callback can be saved and used to authenticate the same user in future connections. - -#### Parameters - -| Name | Type | -|:-----------|:-----------------------------------------------------------------------------------------------------------------| -| `callback` | (`token`: `string`, `identity`: [`Identity`](typescript_#class-identity.), `address`: [`Address`](typescript_#class-address.)) => `void` | - -#### Example - -```ts -spacetimeDBClient.onConnect((token, identity, address) => { - console.log("Connected to SpacetimeDB"); - console.log("Token", token); - console.log("Identity", identity); - console.log("Address", address); -}); -``` - ---- - -### `SpacetimeDBClient` onError - -Register a callback to be invoked upon an error. - -```ts -onError(callback: (...args: any[]) => void): void -``` - -#### Parameters - -| Name | Type | -| :--------- | :----------------------------- | -| `callback` | (...`args`: `any`[]) => `void` | - -#### Example - -```ts -spacetimeDBClient.onError((...args: any[]) => { - console.error("ERROR", args); -}); -``` - -### Class `Identity` - -A unique public identifier for a user of a database. - -Defined in [spacetimedb-sdk.identity](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/identity.ts): - -| Constructors | Description | -| ----------------------------------------------- | -------------------------------------------- | -| [`Identity.constructor`](#identity-constructor.) | Creates a new `Identity`. | -| Methods | | -| [`Identity.isEqual`](#identity-isequal.) | Compare two identities for equality. | -| [`Identity.toHexString`](#identity-tohexstring.) | Print the identity as a hexadecimal string. | -| Static methods | | -| [`Identity.fromString`](#identity-fromstring.) | Parse an Identity from a hexadecimal string. | - -## Constructors - -### `Identity` constructor - -```ts -new Identity(data: Uint8Array) -``` - -#### Parameters - -| Name | Type | -| :----- | :----------- | -| `data` | `Uint8Array` | - -## Methods - -### `Identity` isEqual - -Compare two identities for equality. - -```ts -isEqual(other: Identity): boolean -``` - -#### Parameters - -| Name | Type | -| :------ | :---------------------------- | -| `other` | [`Identity`](typescript_#class-identity.) | - -#### Returns - -`boolean` - ---- - -### `Identity` toHexString - -Print an `Identity` as a hexadecimal string. - -```ts -toHexString(): string -``` - -#### Returns - -`string` - ---- - -### `Identity` fromString - -Static method; parse an Identity from a hexadecimal string. - -```ts -Identity.fromString(str: string): Identity -``` - -#### Parameters - -| Name | Type | -| :---- | :------- | -| `str` | `string` | - -#### Returns - -[`Identity`](typescript_#class-identity.) - -### Class `Address` - -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](typescript_#type-identity.). - -Defined in [spacetimedb-sdk.address](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/address.ts): - -| Constructors | Description | -| ----------------------------------------------- | -------------------------------------------- | -| [`Address.constructor`](#address-constructor.) | Creates a new `Address`. | -| Methods | | -| [`Address.isEqual`](#address-isequal.) | Compare two identities for equality. | -| [`Address.toHexString`](#address-tohexstring.) | Print the address as a hexadecimal string. | -| Static methods | | -| [`Address.fromString`](#address-fromstring.) | Parse an Address from a hexadecimal string. | - -## Constructors - -### `Address` constructor - -```ts -new Address(data: Uint8Array) -``` - -#### Parameters - -| Name | Type | -| :----- | :----------- | -| `data` | `Uint8Array` | - -## Methods - -### `Address` isEqual - -Compare two addresses for equality. - -```ts -isEqual(other: Address): boolean -``` - -#### Parameters - -| Name | Type | -| :------ | :---------------------------- | -| `other` | [`Address`](typescript_#class-address.) | - -#### Returns - -`boolean` - -___ - -### `Address` toHexString - -Print an `Address` as a hexadecimal string. - -```ts -toHexString(): string -``` - -#### Returns - -`string` - -___ - -### `Address` fromString - -Static method; parse an Address from a hexadecimal string. - -```ts -Address.fromString(str: string): Address -``` - -#### Parameters - -| Name | Type | -| :---- | :------- | -| `str` | `string` | - -#### Returns - -[`Address`](typescript_#class-address.) - -### Class `{Table}` - -For each table defined by a module, `spacetime generate` generates a `class` in the `module_bindings` folder whose name is that table's name converted to `PascalCase`. - -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; returned subscribed rows with a given value in a particular 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 - -### {Table} name - -• **name**: `string` - -The name of the `Class`. - ---- - -### {Table} tableName - -The name of the table in the database. - -▪ `Static` **tableName**: `string` = `"Person"` - -## Methods - -### {Table} all - -Return all the subscribed rows in the table. - -```ts -{Table}.all(): {Table}[] -``` - -#### Returns - -`{Table}[]` - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); - -spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(["SELECT * FROM Person"]); - - setTimeout(() => { - console.log(Person.all()); // Prints all the `Person` rows in the database. - }, 5000); -}); -``` - ---- - -### {Table} count - -Return the number of subscribed rows in the table, or 0 if there is no active connection. - -```ts -{Table}.count(): number -``` - -#### Returns - -`number` - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); - -spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(["SELECT * FROM Person"]); - - setTimeout(() => { - console.log(Person.count()); - }, 5000); -}); -``` - ---- - -### {Table} filterBy{COLUMN} - -For each column of a table, `spacetime generate` generates a static method on the `Class` to filter or seek subscribed rows where that column matches a requested value. - -These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `camelCase`. - -```ts -{Table}.filterBy{COLUMN}(value): {Table}[] -``` - -#### Parameters - -| Name | Type | -| :------ | :-------------------------- | -| `value` | The type of the `{COLUMN}`. | - -#### Returns - -`{Table}[]` - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); - -spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(["SELECT * FROM Person"]); - - setTimeout(() => { - console.log(Person.filterByName("John")); // prints all the `Person` rows named John. - }, 5000); -}); -``` - ---- - -### {Table} fromValue - -Deserialize an `AlgebraicType` into this `{Table}`. - -```ts - {Table}.fromValue(value: AlgebraicValue): {Table} -``` - -#### Parameters - -| Name | Type | -| :------ | :--------------- | -| `value` | `AlgebraicValue` | - -#### Returns - -`{Table}` - ---- - -### {Table} getAlgebraicType - -Serialize `this` into an `AlgebraicType`. - -#### Example - -```ts -{Table}.getAlgebraicType(): AlgebraicType -``` - -#### Returns - -`AlgebraicType` - ---- - -### {Table} onInsert - -Register an `onInsert` callback for when a subscribed row is newly inserted into the database. - -```ts -{Table}.onInsert(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | Description | -| :--------- | :---------------------------------------------------------------------------- | :----------------------------------------------------- | -| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is inserted. | - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); -spacetimeDBClient.onConnect((token, identity, address) => { - 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); - } -}); -``` - ---- - -### {Table} removeOnInsert - -Unregister a previously-registered [`onInsert`](#table-oninsert.) callback. - -```ts -{Table}.removeOnInsert(callback: (value: Person, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | -| :--------- | :---------------------------------------------------------------------------- | -| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | - ---- - -### {Table} onUpdate - -Register an `onUpdate` callback to run when an existing row is modified by primary key. - -```ts -{Table}.onUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -`onUpdate` callbacks are only meaningful for tables with a column declared as a primary key. Tables without primary keys will never fire `onUpdate` callbacks. - -#### Parameters - -| Name | Type | Description | -| :--------- | :------------------------------------------------------------------------------------------------------ | :---------------------------------------------------- | -| `callback` | (`oldValue`: `{Table}`, `newValue`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is updated. | - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); -spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(["SELECT * FROM Person"]); -}); - -Person.onUpdate((oldPerson, newPerson, reducerEvent) => { - console.log("Person updated by reducer", reducerEvent, oldPerson, newPerson); -}); -``` - ---- - -### {Table} removeOnUpdate - -Unregister a previously-registered [`onUpdate`](#table-onupdate.) callback. - -```ts -{Table}.removeOnUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | -| :--------- | :------------------------------------------------------------------------------------------------------ | -| `callback` | (`oldValue`: `{Table}`, `newValue`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | - ---- - -### {Table} onDelete - -Register an `onDelete` callback for when a subscribed row is removed from the database. - -```ts -{Table}.onDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | Description | -| :--------- | :---------------------------------------------------------------------------- | :---------------------------------------------------- | -| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is removed. | - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); -spacetimeDBClient.onConnect((token, identity, address) => { - 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 - ); - } -}); -``` - ---- - -### {Table} removeOnDelete - -Unregister a previously-registered [`onDelete`](#table-ondelete.) callback. - -```ts -{Table}.removeOnDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | -| :--------- | :---------------------------------------------------------------------------- | -| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | - -### Class `{Reducer}` - -`spacetime generate` defines an `{Reducer}` class in the `module_bindings` folder for each reducer defined by a module. - -The class's name will be the reducer's name converted to `PascalCase`. - -| Static methods | Description | -| ------------------------------- | ------------------------------------------------------------ | -| [`Reducer.call`](#reducer-call.) | Executes the reducer. | -| Events | | -| [`Reducer.on`](#reducer-on.) | Register a callback to run each time the reducer is invoked. | - -## Static methods - -### {Reducer} call - -Executes the reducer. - -```ts -{Reducer}.call(): void -``` - -#### Example - -```ts -SayHelloReducer.call(); -``` - -## Events - -### {Reducer} on - -Register a callback to run each time the reducer is invoked. - -```ts -{Reducer}.on(callback: (reducerEvent: ReducerEvent, ...reducerArgs: any[]) => void): void -``` - -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. - -#### Parameters - -| Name | Type | -| :--------- | :---------------------------------------------------------- | -| `callback` | `(reducerEvent: ReducerEvent, ...reducerArgs: any[]) => void)` | - -#### Example - -```ts -SayHelloReducer.on((reducerEvent, ...reducerArgs) => { - console.log("SayHelloReducer called", reducerEvent, reducerArgs); -}); -``` diff --git a/Writerside/topics/sdks/typescript/typescript_quickstart.md b/Writerside/topics/sdks/typescript/typescript_quickstart.md deleted file mode 100644 index 13ccd4d6..00000000 --- a/Writerside/topics/sdks/typescript/typescript_quickstart.md +++ /dev/null @@ -1,502 +0,0 @@ -# Typescript Client SDK Quick Start - -In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in Typescript. - -We'll implement a basic single page web app for the module created in our Rust or C# Module Quickstart guides. **Make sure you follow one of these guides before you start on this one.** - -## Project structure - -Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: - -```bash -cd quickstart-chat -``` - -Within it, create a `client` react app: - -```bash -npx create-react-app client --template typescript -``` - -We also need to install the `spacetime-client-sdk` package: - -```bash -cd client -npm install @clockworklabs/spacetimedb-sdk -``` - -## Basic layout - -We are going to start by creating a basic layout for our app. The page contains four sections: - -1. A profile section, where we can set our name. -2. A message section, where we can see all the messages. -3. A system section, where we can see system messages. -4. A new message section, where we can send a new message. - -The `onSubmitNewName` and `onMessageSubmit` callbacks will be called when the user clicks the submit button in the profile and new message sections, respectively. We'll hook these up later. - -Replace the entire contents of `client/src/App.tsx` with the following: - -```typescript -import React, { useEffect, useState } from "react"; -import logo from "./logo.svg"; -import "./App.css"; - -export type MessageType = { - name: string; - message: string; -}; - -function App() { - const [newName, setNewName] = useState(""); - const [settingName, setSettingName] = useState(false); - const [name, setName] = useState(""); - const [systemMessage, setSystemMessage] = useState(""); - const [messages, setMessages] = useState([]); - - const [newMessage, setNewMessage] = useState(""); - - const onSubmitNewName = (e: React.FormEvent) => { - e.preventDefault(); - setSettingName(false); - // Fill in app logic here - }; - - const onMessageSubmit = (e: React.FormEvent) => { - e.preventDefault(); - // Fill in app logic here - setNewMessage(""); - }; - - return ( -
-
-

Profile

- {!settingName ? ( - <> -

{name}

- - - ) : ( -
- setNewName(e.target.value)} - /> - - - )} -
-
-

Messages

- {messages.length < 1 &&

No messages

} -
- {messages.map((message, key) => ( -
-

- {message.name} -

-

{message.message}

-
- ))} -
-
-
-

System

-
-

{systemMessage}

-
-
-
-
-

New Message

- - - -
-
- ); -} - -export default App; -``` - -Now when you run `npm start`, you should see a basic chat app that does not yet send or receive messages. - -## Generate your module types - -The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. - -In your `quickstart-chat` directory, run: - -```bash -mkdir -p client/src/module_bindings -spacetime generate --lang typescript --out-dir client/src/module_bindings --project-path server -``` - -Take a look inside `client/src/module_bindings`. The CLI should have generated four files: - -``` -module_bindings -├── message.ts -├── send_message_reducer.ts -├── set_name_reducer.ts -└── user.ts -``` - -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 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); -``` - -## Create your SpacetimeDB client - -First, we need to create a SpacetimeDB client and connect to the module. Create your client at the top of the `App` function. - -We are going to create a stateful variable to store our client's SpacetimeDB identity when we receive it. Also, we are using `localStorage` to retrieve your auth token if this client has connected before. We will explain these later. - -Replace `` with the name you chose when publishing your module during the module quickstart. If you are using SpacetimeDB Cloud, the host will be `wss://spacetimedb.com/spacetimedb`. - -Add this before the `App` function declaration: - -```typescript -let token = localStorage.getItem("auth_token") || undefined; -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "chat", - token -); -``` - -Inside the `App` function, add a few refs: - -```typescript -let local_identity = useRef(undefined); -let initialized = useRef(false); -const client = useRef(spacetimeDBClient); -``` - -## Register callbacks and connect - -We need to handle several sorts of events: - -1. `onConnect`: When we connect and receive our credentials, we'll save them to browser local storage, so that the next time we connect, we can re-authenticate as the same user. -2. `initialStateSync`: When we're informed of the backlog of past messages, we'll sort them and update the `message` section of the page. -3. `Message.onInsert`: When we receive a new message, we'll update the `message` section of the page. -4. `User.onInsert`: When a new user joins, we'll update the `system` section of the page with an appropiate message. -5. `User.onUpdate`: When a user is updated, we'll add a message with their new name, or declare their new online status to the `system` section of the page. -6. `SetNameReducer.on`: If the server rejects our attempt to set our name, we'll update the `system` section of the page with an appropriate error message. -7. `SendMessageReducer.on`: If the server rejects a message we send, we'll update the `system` section of the page with an appropriate error message. - -We will add callbacks for each of these items in the following sections. All of these callbacks will be registered inside the `App` function after the `useRef` declarations. - -### onConnect Callback - -On connect SpacetimeDB will provide us with our client credentials. - -Each user has a set of 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. - -These 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. - -We want to store our local client identity in a stateful variable and also save our `token` to local storage for future connections. - -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. - -Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. - -To the body of `App`, add: - -```typescript -client.current.onConnect((token, identity, address) => { - console.log("Connected to SpacetimeDB"); - - local_identity.current = identity; - - localStorage.setItem("auth_token", token); - - client.current.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); -}); -``` - -### initialStateSync callback - -This callback fires when our local client cache of the database is populated. This is a good time to set the initial messages list. - -We'll define a helper function, `setAllMessagesInOrder`, to supply the `MessageType` class for our React application. It will call the autogenerated `Message.all` function to get an array of `Message` rows, then sort them and convert them to `MessageType`. - -To find the `User` based on the message's `sender` identity, we'll use `User::filterByIdentity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filterByIdentity` accepts a `UInt8Array`, rather than an `Identity`. The `sender` identity stored in the message is also a `UInt8Array`, not an `Identity`, so we can just pass it to the filter method. - -Whenever we want to display a user name, if they have set a name, we'll use that. If they haven't set a name, we'll instead use the first 8 bytes of their identity, encoded as hexadecimal. We'll define the function `userNameOrIdentity` to handle this. - -We also 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 display `unknown`. - -To the body of `App`, add: - -```typescript -function userNameOrIdentity(user: User): string { - console.log(`Name: ${user.name} `); - if (user.name !== null) { - return user.name || ""; - } else { - var identityStr = new Identity(user.identity).toHexString(); - console.log(`Name: ${identityStr} `); - return new Identity(user.identity).toHexString().substring(0, 8); - } -} - -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 sender_identity = User.filterByIdentity(message.sender); - let display_name = sender_identity - ? userNameOrIdentity(sender_identity) - : "unknown"; - - return { - name: display_name, - message: message.text, - }; - }); - - setMessages(messagesType); -} - -client.current.on("initialStateSync", () => { - setAllMessagesInOrder(); - var user = User.filterByIdentity(local_identity?.current?.toUint8Array()!); - setName(userNameOrIdentity(user!)); -}); -``` - -### Message.onInsert callback - Update messages - -When we receive a new message, we'll update the messages section of the page. Keep in mind that we only want to do this for new messages, i.e. those inserted by a `send_message` reducer invocation. When the server is initializing our cache, we'll get a callback for each existing message, but we don't want to update the page for those. To that effect, our `onInsert` callback will check if its `ReducerEvent` argument is not `undefined`, and only update the `message` section in that case. - -To the body of `App`, add: - -```typescript -Message.onInsert((message, reducerEvent) => { - if (reducerEvent !== undefined) { - setAllMessagesInOrder(); - } -}); -``` - -### User.onInsert callback - 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 `onInsert` and `onDelete` methods of the trait `TableType`, 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.onInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. - -`onInsert` and `onDelete` callbacks take two arguments: the altered row, and a `ReducerEvent | undefined`. This will be `undefined` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is a class containing information about the reducer that triggered this event. For now, we can ignore this argument. - -We are going to add a helper function called `appendToSystemMessage` that will append a line to the `systemMessage` state. We will use this to update the `system` message when a new user joins. - -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); -} - -User.onInsert((user, reducerEvent) => { - if (user.online) { - appendToSystemMessage(`${userNameOrIdentity(user)} has connected.`); - } -}); -``` - -### User.onUpdate callback - 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 `onUpdate` method which is automatically implemented by `spacetime generate` for any table with a `#[primarykey]` column. - -`onUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. - -In our module, users can be updated for three reasons: - -1. They've set their name using the `set_name` reducer. -2. They're an existing user re-connecting, so their `online` has been set to `true`. -3. They've disconnected, so their `online` has been set to `false`. - -We'll update the `system` message in each of these cases. - -To the body of `App`, add: - -```typescript -User.onUpdate((oldUser, user, reducerEvent) => { - if (oldUser.online === false && user.online === true) { - appendToSystemMessage(`${userNameOrIdentity(user)} has connected.`); - } else if (oldUser.online === true && user.online === false) { - appendToSystemMessage(`${userNameOrIdentity(user)} has disconnected.`); - } - - if (user.name !== oldUser.name) { - appendToSystemMessage( - `User ${userNameOrIdentity(oldUser)} renamed to ${userNameOrIdentity( - user - )}.` - ); - } -}); -``` - -### SetNameReducer.on callback - Handle errors and update profile name - -We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducer` method which is automatically implemented for each reducer by `spacetime generate`. - -Each reducer callback takes a number of parameters: - -1. `ReducerEvent` that contains information about the reducer that triggered this event. It contains several fields. The ones we care about are: - - - `callerIdentity`: The `Identity` of the client that called the reducer. - - `status`: The `Status` of the reducer run, one of `"Committed"`, `"Failed"` or `"OutOfEnergy"`. - - `message`: The error message, if any, that the reducer returned. - -2. The rest of the parameters are arguments passed to the reducer. - -These callbacks will be invoked in one of two cases: - -1. If the reducer was successful and altered any of our subscribed rows. -2. If we requested an invocation which failed. - -Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. - -We already handle other users' `set_name` calls using our `User.onUpdate` callback, but we need some additional behavior for setting our own name. If our name was rejected, we'll update the `system` message. If our name was accepted, we'll update our name in the app. - -We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. - -If the reducer status comes back as `committed`, we'll update the name in our app. - -To the body of `App`, add: - -```typescript -SetNameReducer.on((reducerEvent, newName) => { - if ( - local_identity.current && - reducerEvent.callerIdentity.isEqual(local_identity.current) - ) { - if (reducerEvent.status === "failed") { - appendToSystemMessage(`Error setting name: ${reducerEvent.message} `); - } else if (reducerEvent.status === "committed") { - setName(newName); - } - } -}); -``` - -### SendMessageReducer.on callback - Handle errors - -We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different. We don't need to do anything for successful SendMessage reducer runs; our Message.onInsert callback already displays them. - -To the body of `App`, add: - -```typescript -SendMessageReducer.on((reducerEvent, newMessage) => { - if ( - local_identity.current && - reducerEvent.callerIdentity.isEqual(local_identity.current) - ) { - if (reducerEvent.status === "failed") { - appendToSystemMessage(`Error sending message: ${reducerEvent.message} `); - } - } -}); -``` - -## Update the UI button callbacks - -We need to update the `onSubmitNewName` and `onMessageSubmit` callbacks to send the appropriate reducer to the module. - -`spacetime generate` defined two functions for us, `SetNameReducer.call` and `SendMessageReducer.call`, 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 `SetNameReducer.call` and `SendMessageReducer.call` take one argument, a `String`. - -Add the following to the `onSubmitNewName` callback: - -```typescript -SetNameReducer.call(newName); -``` - -Add the following to the `onMessageSubmit` callback: - -```typescript -SendMessageReducer.call(newMessage); -``` - -## Connecting to the module - -We need to connect to the module when the app loads. We'll do this by adding a `useEffect` hook to the `App` function. This hook should only run once, when the component is mounted, but we are going to use an `initialized` boolean to ensure that it only runs once. - -```typescript -useEffect(() => { - if (!initialized.current) { - client.current.connect(); - initialized.current = true; - } -}, []); -``` - -## What's next? - -When you run `npm start` you should see a chat app that can send and receive messages. If you open it in multiple private browser windows, you should see that messages are synchronized between them. - -Congratulations! You've built a simple chat app with SpacetimeDB. You can find the full source code for this app [here](https://github.com/clockworklabs/spacetimedb-typescript-sdk/tree/main/examples/quickstart) - -For a more advanced example of the SpacetimeDB TypeScript SDK, take a look at the [Spacetime MUD (multi-user dungeon)](https://github.com/clockworklabs/spacetime-mud/tree/main/react-client). - -## Troubleshooting - -If you encounter the following error: - -``` -TS2802: Type 'IterableIterator' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher. -``` - -You can fix it by changing your compiler target. Add the following to your `tsconfig.json` file: - -```json -{ - "compilerOptions": { - "target": "es2015" - } -} -``` diff --git a/Writerside/topics/sql/sql_index.md b/Writerside/topics/sql/sql_index.md deleted file mode 100644 index 96f0c223..00000000 --- a/Writerside/topics/sql/sql_index.md +++ /dev/null @@ -1,407 +0,0 @@ -# SQL Support - -SpacetimeDB supports a subset of SQL as a query language. Developers can evaluate SQL queries against a Spacetime database via the `spacetime sql` command-line tool and the [`/database/sql/:name_or_address POST` HTTP endpoint](database#databasesqlname_or_address-post.). Client developers also write SQL queries when subscribing to events in the [WebSocket API](ws#subscribe.) or via an SDK `subscribe` function. - -SpacetimeDB aims to support much of the [SQL 2016 standard](https://www.iso.org/standard/63555.html), and in particular aims to be compatible with [PostgreSQL](https://www.postgresql.org/). - -SpacetimeDB 0.6 implements a relatively small subset of SQL. Future SpacetimeDB versions will implement additional SQL features. - -## Types - -| Type | Description | -| --------------------------------------------- | -------------------------------------- | -| [Nullable types](#nullable-types.) | Types which may not hold a value. | -| [Logic types](#logic-types.) | Booleans, i.e. `true` and `false`. | -| [Integer types](#integer-types.) | Numbers without fractional components. | -| [Floating-point types](#floating-point-types.) | Numbers with fractional components. | -| [Text types](#text-types.) | UTF-8 encoded text. | - -### Definition statements - -| Statement | Description | -| ----------------------------- | ------------------------------------ | -| [CREATE TABLE](#create-table.) | Create a new table. | -| [DROP TABLE](#drop-table.) | Remove a table, discarding all rows. | - -### Query statements - -| Statement | Description | -| ----------------- | -------------------------------------------------------------------------------------------- | -| [FROM](#from.) | A source of data, like a table or a value. | -| [JOIN](#join.) | Combine several data sources. | -| [SELECT](#select.) | Select specific rows and columns from a data source, and optionally compute a derived value. | -| [DELETE](#delete.) | Delete specific rows from a table. | -| [INSERT](#insert.) | Insert rows into a table. | -| [UPDATE](#update.) | Update specific rows in a table. | - -## Data types - -SpacetimeDB is built on the Spacetime Algebraic Type System, or SATS. SATS is a richer, more expressive type system than the one included in the SQL language. - -Because SATS is a richer type system than SQL, some SATS types cannot cleanly correspond to SQL types. In particular, the SpacetimeDB SQL interface is unable to construct or compare instances of product and sum types. As such, SpacetimeDB SQL must largely restrict themselves to interacting with columns of builtin types. - -Most SATS builtin types map cleanly to SQL types. - -### Nullable types - -SpacetimeDB types, by default, do not permit `NULL` as a value. Nullable types are encoded in SATS using a sum type which corresponds to [Rust's `Option`](https://doc.rust-lang.org/stable/std/option/enum.Option.html). In SQL, such types can be written by adding the constraint `NULL`, like `INT NULL`. - -### Logic types - -| SQL | SATS | Example | -| --------- | ------ | --------------- | -| `BOOLEAN` | `Bool` | `true`, `false` | - -### Numeric types - -#### Integer types - -An integer is a number without a fractional component. - -Adding the `UNSIGNED` constraint to an integer type allows only positive values. This allows representing a larger positive range without increasing the width of the integer. - -| SQL | SATS | Example | Min | Max | -| ------------------- | ----- | ------- | ------ | ----- | -| `TINYINT` | `I8` | 1 | -(2⁷) | 2⁷-1 | -| `TINYINT UNSIGNED` | `U8` | 1 | 0 | 2⁸-1 | -| `SMALLINT` | `I16` | 1 | -(2¹⁵) | 2¹⁵-1 | -| `SMALLINT UNSIGNED` | `U16` | 1 | 0 | 2¹⁶-1 | -| `INT`, `INTEGER` | `I32` | 1 | -(2³¹) | 2³¹-1 | -| `INT UNSIGNED` | `U32` | 1 | 0 | 2³²-1 | -| `BIGINT` | `I64` | 1 | -(2⁶³) | 2⁶³-1 | -| `BIGINT UNSIGNED` | `U64` | 1 | 0 | 2⁶⁴-1 | - -#### Floating-point types - -SpacetimeDB supports single- and double-precision [binary IEEE-754 floats](https://en.wikipedia.org/wiki/IEEE_754). - -| SQL | SATS | Example | Min | Max | -| ----------------- | ----- | ------- | ------------------------ | ----------------------- | -| `REAL` | `F32` | 1.0 | -3.40282347E+38 | 3.40282347E+38 | -| `DOUBLE`, `FLOAT` | `F64` | 1.0 | -1.7976931348623157E+308 | 1.7976931348623157E+308 | - -### Text types - -SpacetimeDB supports a single string type, `String`. SpacetimeDB strings are UTF-8 encoded. - -| SQL | SATS | Example | Notes | -| ----------------------------------------------- | -------- | ------- | -------------------- | -| `CHAR`, `VARCHAR`, `NVARCHAR`, `TEXT`, `STRING` | `String` | 'hello' | Always UTF-8 encoded | - -> SpacetimeDB SQL currently does not support length contraints like `CHAR(10)`. - -## Syntax - -### Comments - -SQL line comments begin with `--`. - -```sql --- This is a comment -``` - -### Expressions - -We can express different, composable, values that are universally called `expressions`. - -An expression is one of the following: - -#### Literals - -| Example | Description | -| --------- | ----------- | -| `1` | An integer. | -| `1.0` | A float. | -| `'hello'` | A string. | -| `true` | A boolean. | - -#### Binary operators - -| Example | Description | -| ------- | ------------------- | -| `1 > 2` | Integer comparison. | -| `1 + 2` | Integer addition. | - -#### Logical expressions - -Any expression which returns a boolean, i.e. `true` or `false`, is a logical expression. - -| Example | Description | -| ---------------- | ------------------------------------------------------------ | -| `1 > 2` | Integer comparison. | -| `1 + 2 == 3` | Equality comparison between a constant and a computed value. | -| `true AND false` | Boolean and. | -| `true OR false` | Boolean or. | -| `NOT true` | Boolean inverse. | - -#### Function calls - -| Example | Description | -| --------------- | -------------------------------------------------- | -| `lower('JOHN')` | Apply the function `lower` to the string `'JOHN'`. | - -#### Table identifiers - -| Example | Description | -| ------------- | ------------------------- | -| `inventory` | Refers to a table. | -| `"inventory"` | Refers to the same table. | - -#### Column references - -| Example | Description | -| -------------------------- | ------------------------------------------------------- | -| `inventory_id` | Refers to a column. | -| `"inventory_id"` | Refers to the same column. | -| `"inventory.inventory_id"` | Refers to the same column, explicitly naming its table. | - -#### Wildcards - -Special "star" expressions which select all the columns of a table. - -| Example | Description | -| ------------- | ------------------------------------------------------- | -| `*` | Refers to all columns of a table identified by context. | -| `inventory.*` | Refers to all columns of the `inventory` table. | - -#### Parenthesized expressions - -Sub-expressions can be enclosed in parentheses for grouping and to override operator precedence. - -| Example | Description | -| ------------- | ----------------------- | -| `1 + (2 / 3)` | One plus a fraction. | -| `(1 + 2) / 3` | A sum divided by three. | - -### `CREATE TABLE` - -A `CREATE TABLE` statement creates a new, initially empty table in the database. - -The syntax of the `CREATE TABLE` statement is: - -> **CREATE TABLE** _table_name_ (_column_name_ _data_type_, ...); - -![create-table](create_table.svg) - -#### Examples - -Create a table `inventory` with two columns, an integer `inventory_id` and a string `name`: - -```sql -CREATE TABLE inventory (inventory_id INTEGER, name TEXT); -``` - -Create a table `player` with two integer columns, an `entity_id` and an `inventory_id`: - -```sql -CREATE TABLE player (entity_id INTEGER, inventory_id INTEGER); -``` - -Create a table `location` with three columns, an integer `entity_id` and floats `x` and `z`: - -```sql -CREATE TABLE location (entity_id INTEGER, x REAL, z REAL); -``` - -### `DROP TABLE` - -A `DROP TABLE` statement removes a table from the database, deleting all its associated rows, indexes, constraints and sequences. - -To empty a table of rows without destroying the table, use [`DELETE`](#delete.). - -The syntax of the `DROP TABLE` statement is: - -> **DROP TABLE** _table_name_; - -![drop-table](drop_table.svg) - -Examples: - -```sql -DROP TABLE inventory; -``` - -## Queries - -### `FROM` - -A `FROM` clause derives a data source from a table name. - -The syntax of the `FROM` clause is: - -> **FROM** _table_name_ _join_clause_?; - -![from](from.svg) - -#### Examples - -Select all rows from the `inventory` table: - -```sql -SELECT * FROM inventory; -``` - -### `JOIN` - -A `JOIN` clause combines two data sources into a new data source. - -Currently, SpacetimeDB SQL supports only inner joins, which return rows from two data sources where the values of two columns match. - -The syntax of the `JOIN` clause is: - -> **JOIN** _table_name_ **ON** _expr_ = _expr_; - -![join](join.svg) - -### Examples - -Select all players rows who have a corresponding location: - -```sql -SELECT player.* FROM player - JOIN location - ON location.entity_id = player.entity_id; -``` - -Select all inventories which have a corresponding player, and where that player has a corresponding location: - -```sql -SELECT inventory.* FROM inventory - JOIN player - ON inventory.inventory_id = player.inventory_id - JOIN location - ON player.entity_id = location.entity_id; -``` - -### `SELECT` - -A `SELECT` statement returns values of particular columns from a data source, optionally filtering the data source to include only rows which satisfy a `WHERE` predicate. - -The syntax of the `SELECT` command is: - -> **SELECT** _column_expr_ > **FROM** _from_expr_ -> {**WHERE** _expr_}? - -![sql-select](select.svg) - -#### Examples - -Select all columns of all rows from the `inventory` table: - -```sql -SELECT * FROM inventory; -SELECT inventory.* FROM inventory; -``` - -Select only the `inventory_id` column of all rows from the `inventory` table: - -```sql -SELECT inventory_id FROM inventory; -SELECT inventory.inventory_id FROM inventory; -``` - -An optional `WHERE` clause can be added to filter the data source using a [logical expression](#logical-expressions.). The `SELECT` will return only the rows from the data source for which the expression returns `true`. - -#### Examples - -Select all columns of all rows from the `inventory` table, with a filter that is always true: - -```sql -SELECT * FROM inventory WHERE 1 = 1; -``` - -Select all columns of all rows from the `inventory` table with the `inventory_id` 1: - -```sql -SELECT * FROM inventory WHERE inventory_id = 1; -``` - -Select only the `name` column of all rows from the `inventory` table with the `inventory_id` 1: - -```sql -SELECT name FROM inventory WHERE inventory_id = 1; -``` - -Select all columns of all rows from the `inventory` table where the `inventory_id` is 2 or greater: - -```sql -SELECT * FROM inventory WHERE inventory_id > 1; -``` - -### `INSERT` - -An `INSERT INTO` statement inserts new rows into a table. - -One can insert one or more rows specified by value expressions. - -The syntax of the `INSERT INTO` statement is: - -> **INSERT INTO** _table_name_ (_column_name_, ...) **VALUES** (_expr_, ...), ...; - -![sql-insert](insert.svg) - -#### Examples - -Insert a single row: - -```sql -INSERT INTO inventory (inventory_id, name) VALUES (1, 'health1'); -``` - -Insert two rows: - -```sql -INSERT INTO inventory (inventory_id, name) VALUES (1, 'health1'), (2, 'health2'); -``` - -### UPDATE - -An `UPDATE` statement changes the values of a set of specified columns in all rows of a table, optionally filtering the table to update only rows which satisfy a `WHERE` predicate. - -Columns not explicitly modified with the `SET` clause retain their previous values. - -If the `WHERE` clause is absent, the effect is to update all rows in the table. - -The syntax of the `UPDATE` statement is - -> **UPDATE** _table_name_ **SET** > _column_name_ = _expr_, ... -> {_WHERE expr_}?; - -![sql-update](update.svg) - -#### Examples - -Set the `name` column of all rows from the `inventory` table with the `inventory_id` 1 to `'new name'`: - -```sql -UPDATE inventory - SET name = 'new name' - WHERE inventory_id = 1; -``` - -### DELETE - -A `DELETE` statement deletes rows that satisfy the `WHERE` clause from the specified table. - -If the `WHERE` clause is absent, the effect is to delete all rows in the table. In that case, the result is a valid empty table. - -The syntax of the `DELETE` statement is - -> **DELETE** _table_name_ -> {**WHERE** _expr_}?; - -![sql-delete](delete.svg) - -#### Examples - -Delete all the rows from the `inventory` table with the `inventory_id` 1: - -```sql -DELETE FROM inventory WHERE inventory_id = 1; -``` - -Delete all rows from the `inventory` table, leaving it empty: - -```sql -DELETE FROM inventory; -``` diff --git a/Writerside/topics/unity/homeless.md b/Writerside/topics/unity/homeless.md deleted file mode 100644 index 121fe538..00000000 --- a/Writerside/topics/unity/homeless.md +++ /dev/null @@ -1,355 +0,0 @@ -### Create the Module - -1. It is important that you already have the SpacetimeDB CLI tool [installed](install.). - -2. Run SpacetimeDB locally using the installed CLI. In a **new** terminal or command window, run the following command: - -```bash -spacetime start -``` - -💡 Standalone mode will run in the foreground. -💡 Below examples Rust language, [but you may also use C#](c-sharp_index.md). - -## Create a Server Module - -Run the following command to initialize the SpacetimeDB server module project with Rust as the language: - -```bash -spacetime init --lang=rust server -``` - -This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. - -### SpacetimeDB Tables - -In this section we'll be making some edits to the file `server/src/lib.cs`. We recommend you open up this file in an IDE like VSCode or RustRover. - -**Important: Open the `server/src/lib.cs` file and delete its contents. We will be writing it from scratch here.** - -First we need to add some imports at the top of the file. - -**Copy and paste into lib.cs:** - -```csharp -// using SpacetimeDB; // Uncomment to omit `SpacetimeDB` attribute prefixes -using SpacetimeDB.Module; -using static SpacetimeDB.Runtime; -``` - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](c-sharp.). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.cs:** - -```csharp -/// We're using this table as a singleton, -/// so there should typically only be one element where the version is 0. -[SpacetimeDB.Table] -public partial class Config -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public Identity Version; - public string? MessageOfTheDay; -} -``` - -Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. - -**Append to the bottom of lib.cs:** - -```csharp -/// This allows us to store 3D points in tables. -[SpacetimeDB.Type] -public partial class StdbVector3 -{ - public float X; - public float Y; - public float Z; -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```csharp -/// This stores information related to all entities in our game. In this tutorial -/// all entities must at least have an entity_id, a position, a direction and they -/// must specify whether or not they are moving. -[SpacetimeDB.Table] -public partial class EntityComponent -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - public ulong EntityId; - public StdbVector3 Position; - public float Direction; - public bool Moving; -} -``` - -Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. - -**Append to the bottom of lib.cs:** - -```csharp -/// All players have this component and it associates an entity with the user's -/// Identity. It also stores their username and whether or not they're logged in. -[SpacetimeDB.Table] -public partial class PlayerComponent -{ - // An EntityId that matches an EntityId in the `EntityComponent` table. - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public ulong EntityId; - - // The user's identity, which is unique to each player - [SpacetimeDB.Column(ColumnAttrs.Unique)] - public Identity Identity; - public string? Username; - public bool LoggedIn; -} -``` - -Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.cs:** - -```csharp -/// This reducer is called when the user logs in for the first time and -/// enters a username. -[SpacetimeDB.Reducer] -public static void CreatePlayer(DbEventArgs dbEvent, string username) -{ - // Get the Identity of the client who called this reducer - Identity sender = dbEvent.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"); - } - - // Create a new entity for this player - try - { - new EntityComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, - Direction = 0, - Moving = false, - }.Insert(); - } - catch - { - Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); - Throw; - } - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - try - { - new PlayerComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Identity = dbEvent.Sender, - Username = username, - LoggedIn = true, - }.Insert(); - } - catch - { - Log("Error: Failed to insert PlayerComponent", LogLevel.Error); - throw; - } - Log($"Player created: {username}"); -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. -- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the module is initially published -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void OnInit() -{ - try - { - new Config - { - Version = 0, - MessageOfTheDay = "Hello, World!", - }.Insert(); - } - catch - { - Log("Error: Failed to insert Config", LogLevel.Error); - throw; - } -} -``` - -We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the client connects, we update the LoggedIn state to true -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void ClientConnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:true); -``` -```csharp -/// Called when the client disconnects, we update the logged_in state to false -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void ClientDisonnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:false); -``` -```csharp -/// This helper function gets the PlayerComponent, sets the LoggedIn -/// variable and updates the PlayerComponent table row. -private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) -{ - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - player.LoggedIn = loggedIn; - PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); -} -``` - -Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. - -Using the `EntityId` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. - -**Append to the bottom of lib.cs:** - -```csharp -/// Updates the position of a player. This is also called when the player stops moving. -[SpacetimeDB.Reducer] -private static void UpdatePlayerPosition( - DbEventArgs dbEvent, - StdbVector3 position, - float direction, - bool moving) -{ - // First, look up the player using the sender identity - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - // Use the Player's EntityId to retrieve and update the EntityComponent - ulong playerEntityId = player.EntityId; - EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); - if (entity is null) - { - throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); - } - - entity.Position = position; - entity.Direction = direction; - entity.Moving = moving; - EntityComponent.UpdateByEntityId(playerEntityId, entity); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -### Finally, Add Chat Support - -The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -[SpacetimeDB.Table] -public partial class ChatMessage -{ - // The primary key for this table will be auto-incremented - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - - // The entity id of the player that sent the message - public ulong SenderId; - - // Message contents - public string? Text; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -/// Adds a chat entry to the ChatMessage table -[SpacetimeDB.Reducer] -public static void SendChatMessage(DbEventArgs dbEvent, string text) -{ - // Get the player's entity id - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - - // Insert the chat message - new ChatMessage - { - SenderId = player.EntityId, - Text = text, - }.Insert(); -} -``` - -## Wrapping Up - -💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) - -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside/topics/unity/part-1.md b/Writerside/topics/unity/part-1.md deleted file mode 100644 index bfad0644..00000000 --- a/Writerside/topics/unity/part-1.md +++ /dev/null @@ -1,57 +0,0 @@ -# Unity Multiplayer Tutorial - -## Part 1 of 3: Setup - -This tutorial will guide you through setting up a multiplayer game project using Unity and SpacetimeDB. We will start by cloning the project, connecting it to SpacetimeDB and running the project. - -💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! - -> [!IMPORTANT] -> TODO: This draft may link to WIP repos or docs - be sure to replace with final links after prerequisite PRs are approved (that are not yet approved upon writing this) - -## 1. Clone the Project - -Let's name it `SpacetimeDBUnityTutorial` for reference: -```bash -git clone https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade SpacetimeDBUnityTutorial -``` - -This project repo is separated into two sub-projects: - -1. [Server](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp) (SpacetimeDB Module) -1. [Client](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Client) (Unity project) - -> [!TIP] -> You may optionally _update_ the [SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk) via the Package Manager in Unity - -## 2. Publishing the Project - -From Unity, you don't need CLI commands for common functionality: - -1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) -1. Create an identity -> Select `testnet` for the server -1. Browse to your repo root `Server-Csharp` dir -> **Publish** -> **Generate** Unity files - -💡For the next section, we'll use the selected `Server` and publish result `Host` - -![Unity Publisher Tool](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-publisher.jpg) - -## 3. Connecting the Project - -1. Open `Scenes/Main` in Unity -> select the `GameManager` GameObject in the inspector. -1. Matching the earlier Publish setup: - 1. For the GameManager `Db Name or Address`, input `testnet` - 1. For the GameManager `Host`, input `https://testnet.spacetimedb.com -1. Save your scene - -## 4. Running the Project - -With the same `Main` scene open, press play! - -![Gameplay Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-action.jpg) - -![UI Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-ui.jpg) - -You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. - -Congratulations! You have successfully set up your multiplayer game project. In the next section, we will break down how Server Modules work and analyze the demo code. diff --git a/Writerside/topics/unity/part-2.md b/Writerside/topics/unity/part-2.md deleted file mode 100644 index 348a4e7f..00000000 --- a/Writerside/topics/unity/part-2.md +++ /dev/null @@ -1,483 +0,0 @@ -# Unity Multiplayer Tutorial - -## Part 2 of 3: Analyzing the C# Server Module - -This progressive tutorial is continued from [Part 1](part-11.md). - -In this part of the tutorial, we will create a SpacetimeDB (SpacetimeDB) server module using C# for the Unity multiplayer game. The server module will handle the game logic and data management for the game. - -💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! - -## The Entity Component Systems (ECS) - -Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). - -We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. - -## C# Module Limitations & Nuances - -Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), there are some limitations and nuances to be aware of when developing your server module. Sometimes errors may occur instantly, but others may not reflect until you build or used at runtime. - -In other words, it's best to be aware _before_ you start: - -1. No DateTime-like types in Types or Tables: - - Use `string` for timestamps (exampled at at [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs), or `long` for Unix Epoch time. - -1. No Timers or async/await, such as those to create repeating loops: - - For repeating invokers, instead **re**schedule it from within a fired [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) function. - -1. Using `Debug` advanced option in the `Publisher` Unity editor tool will add callstack symbols for easier debugging: - - However, avoid using `Debug` mode when publishing outside of a `localhost` server: - - Due to WASM buffer size limitations, publishing outside `localhost` may fail. - -1. If you `throw` a new `Exception`, no error logs will appear. Instead, use either: - 1. Use `Log(message, LogLevel.Error);` before you throw. - 2. Use the demo's static [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs) class to `Utils.Throw()` to wrap the error log before throwing. - -1. `[AutoIncrement]` or `[PrimaryKeyAuto]` will never equal 0: - - Inserting a new row with an Auto key equaling 0 will always return a unique, non-0 value. - - -1. Enums cannot declare values out of the default order: - - For example, `{ Foo = 0, Bar = 3 }` will fail to compile. - -## Namespaces - -Common `using` statements include: - -```csharp -using SpacetimeDB; // Contains class|func|struct attributes like [Table], [Type], [Reducer] -using static SpacetimeDB.Runtime; // Contains Identity DbEventArgs, Log() -using SpacetimeDB.Module; // Contains prop attributes like [Column] -using Module.Utils; // Helper to workaround the `throw` and `DateTime` limitations noted above -``` - -- You will mostly see `SpacetimeDB.Module` in [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs) for schema definitions -- `SpacetimeDB` and `SpacetimeDB.Runtime` can be found in most all SpacetimeDB scripts -- `Module.Utils` parse DateTimeOffset into a timestamp string and wraps `throw` with error logs - -## Partial Classes & Structs - -- Throughout the demo, you will notice most classes or structs with a SpacetimeDB [Attribute] such as `[Table]` or `[Reducer]` will be defined with the `partial` keyword. - -- This allows the _Roslyn Compiler_ to [incrementally generate](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) additions to the SpacetimeDB SDK, such as adding helper functions and utilities. This means SpacetimeDB takes care of all the low-level tooling for you, such as inserting, updating or querying the DB. - - This further allows you to separate your models from logic within the same class. - -* Notice that the module class, itself, is also a `static partial class`. - -## Types & Tables - -`[Table]` attributes are database columns, while `[Type]` attributes are define a schema. - -### Types - -`[Type]` attributes attach to properties containing `[Table]` attributes when you want to use a custom Type that's not [SpacetimeDB natively-supported](c-sharp#supported-types.). These are generally defined as a `partial struct` or `partial class` - -Let's inspect a real example `Type`; open [Server-cs/Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): - -In Unity, you are likely familiar with the `Vector2` type. In SpacetimeDB, let's inspect the `StdbVector2` type to store 2D positions in the database: - -```csharp -/// A spacetime type which can be used in tables & reducers to represent a 2D position (such as movement) -[Type] -public partial class StdbVector2 -{ - public float X; - public float Z; -} -``` - -Since `Types` are used in `Tables`, we can now use a custom SpacetimeDB `StdbVector3` `Type` in a `[Table]`. - -### Tables - -`[Table] attributes use `[Type]`s - either custom (like `StdbVector2` above) or [SpacetimeDB natively-supported types](../modules/c-sharp#supported-types). These are generally defined as a `struct` or `class` - -Let's inspect a real example `Table`, looking again at [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): - -```csharp -/// Represents chat messages within the game, including the sender and message content -[Table] -public partial class ChatMessage -{ - /// Primary key, automatically incremented - [Column(ColumnAttrs.PrimaryKeyAuto)] - public ulong ChatEntityId; - - /// The entity id of the player (or NPC) that sent the message - public ulong SourceEntityId; - - /// Message contents - public string? ChatText; - - /// - /// Stringified ISO 8601 format (Unix Epoch Time) - /// - /// DateTime.ToUniversalTime().ToString("o"); - /// - public static string GetTimestamp(DateTimeOffset dateTimeOffset) => - dateTimeOffset.ToUniversalTime().ToString("o"); -} -``` - -**Let's break this down:** -In addition, this allows for static helper utilities like `GetTimestamp.GetTimestamp()` above. Let - - -```csharp -/// This component will be created for all world objects that can move smoothly throughout the world, keeping track -/// of position, the last time the component was updated & the direction the mobile object is currently moving. -[Table] -public partial class MobileEntityComponent -{ - /// Primary key for the mobile entity - [Column(ColumnAttrs.PrimaryKey)] - public ulong EntityId; - - /// The last known location of this entity - public StdbVector2? Location; - - /// Movement direction, {0,0} if not moving at all. - public StdbVector2? Direction; - - /// Timestamp when movement started. Timestamp::UNIX_EPOCH if not moving. - public string? MoveStartTimestamp; -} -``` - -**Let's break this down:** - -- `EntityId` is the unique identifier for the table, declared as a `ulong` -- Location and Direction are both `StdbVector2` types discussed above -- `MoveStartTimestamp` is a string of epoch time, as you cannot use `DateTime`-like types within Tables. - - See the [Limitations](#limitations.) section below - -## Reducers - -Reducers are cloud functions that run on the server and can be called from the client, always returning `void`. - -Looking at the most straight-forward example, open [Chat.cs]( - - - - - - - -```csharp - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](/docs/modules/c-sharp). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.cs:** - -```csharp -/// We're using this table as a singleton, -/// so there should typically only be one element where the version is 0. -[SpacetimeDB.Table] -public partial class Config -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public Identity Version; - public string? MessageOfTheDay; -} -``` - -Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. - -**Append to the bottom of lib.cs:** - -```csharp -/// This allows us to store 3D points in tables. -[SpacetimeDB.Type] -public partial class StdbVector3 -{ - public float X; - public float Y; - public float Z; -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```csharp -/// This stores information related to all entities in our game. In this tutorial -/// all entities must at least have an entity_id, a position, a direction and they -/// must specify whether or not they are moving. -[SpacetimeDB.Table] -public partial class EntityComponent -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - public ulong EntityId; - public StdbVector3 Position; - public float Direction; - public bool Moving; -} -``` - -Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. - -**Append to the bottom of lib.cs:** - -```csharp -/// All players have this component and it associates an entity with the user's -/// Identity. It also stores their username and whether or not they're logged in. -[SpacetimeDB.Table] -public partial class PlayerComponent -{ - // An EntityId that matches an EntityId in the `EntityComponent` table. - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public ulong EntityId; - - // The user's identity, which is unique to each player - [SpacetimeDB.Column(ColumnAttrs.Unique)] - public Identity Identity; - public string? Username; - public bool LoggedIn; -} -``` - -Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.cs:** - -```csharp -/// This reducer is called when the user logs in for the first time and -/// enters a username. -[SpacetimeDB.Reducer] -public static void CreatePlayer(DbEventArgs dbEvent, string username) -{ - // Get the Identity of the client who called this reducer - Identity sender = dbEvent.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"); - } - - // Create a new entity for this player - try - { - new EntityComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, - Direction = 0, - Moving = false, - }.Insert(); - } - catch - { - Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); - Throw; - } - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - try - { - new PlayerComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Identity = dbEvent.Sender, - Username = username, - LoggedIn = true, - }.Insert(); - } - catch - { - Log("Error: Failed to insert PlayerComponent", LogLevel.Error); - throw; - } - Log($"Player created: {username}"); -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. -- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the module is initially published -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void OnInit() -{ - try - { - new Config - { - Version = 0, - MessageOfTheDay = "Hello, World!", - }.Insert(); - } - catch - { - Log("Error: Failed to insert Config", LogLevel.Error); - throw; - } -} -``` - -We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the client connects, we update the LoggedIn state to true -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void ClientConnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:true); -``` -```csharp -/// Called when the client disconnects, we update the logged_in state to false -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void ClientDisonnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:false); -``` -```csharp -/// This helper function gets the PlayerComponent, sets the LoggedIn -/// variable and updates the PlayerComponent table row. -private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) -{ - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - player.LoggedIn = loggedIn; - PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); -} -``` - -Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. - -Using the `EntityId` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. - -**Append to the bottom of lib.cs:** - -```csharp -/// Updates the position of a player. This is also called when the player stops moving. -[SpacetimeDB.Reducer] -private static void UpdatePlayerPosition( - DbEventArgs dbEvent, - StdbVector3 position, - float direction, - bool moving) -{ - // First, look up the player using the sender identity - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - // Use the Player's EntityId to retrieve and update the EntityComponent - ulong playerEntityId = player.EntityId; - EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); - if (entity is null) - { - throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); - } - - entity.Position = position; - entity.Direction = direction; - entity.Moving = moving; - EntityComponent.UpdateByEntityId(playerEntityId, entity); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -### Finally, Add Chat Support - -The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -[SpacetimeDB.Table] -public partial class ChatMessage -{ - // The primary key for this table will be auto-incremented - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - - // The entity id of the player that sent the message - public ulong SenderId; - - // Message contents - public string? Text; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -/// Adds a chat entry to the ChatMessage table -[SpacetimeDB.Reducer] -public static void SendChatMessage(DbEventArgs dbEvent, string text) -{ - // Get the player's entity id - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - - // Insert the chat message - new ChatMessage - { - SenderId = player.EntityId, - Text = text, - }.Insert(); -} -``` - -## Wrapping Up - -💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) - -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside/topics/unity/part-2a-rust.md b/Writerside/topics/unity/part-2a-rust.md deleted file mode 100644 index 1271b345..00000000 --- a/Writerside/topics/unity/part-2a-rust.md +++ /dev/null @@ -1,316 +0,0 @@ -# Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (Rust) - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 1 Tutorial](part-11.md) - -## Create a Server Module - -Run the following command to initialize the SpacetimeDB server module project with Rust as the language: - -```bash -spacetime init --lang=rust server -``` - -This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. - -### SpacetimeDB Tables - -In this section we'll be making some edits to the file `server/src/lib.rs`. We recommend you open up this file in an IDE like VSCode or RustRover. - -**Important: Open the `server/src/lib.rs` file and delete its contents. We will be writing it from scratch here.** - -First we need to add some imports at the top of the file. - -**Copy and paste into lib.rs:** - -```rust -use spacetimedb::{spacetimedb, Identity, SpacetimeType, ReducerContext}; -use log; -``` - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [Rust module reference](rust.). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.rs:** - -```rust -// We're using this table as a singleton, so there should typically only be one element where the version is 0. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct Config { - #[primarykey] - pub version: u32, - pub message_of_the_day: String, -} -``` - -Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `#[derive(SpacetimeType)]` and a `#[spacetimedb(table)]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. - -**Append to the bottom of lib.rs:** - -```rust -// This allows us to store 3D points in tables. -#[derive(SpacetimeType, Clone)] -pub struct StdbVector3 { - pub x: f32, - pub y: f32, - pub z: f32, -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```rust -// This stores information related to all entities in our game. In this tutorial -// all entities must at least have an entity_id, a position, a direction and they -// must specify whether or not they are moving. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct EntityComponent { - #[primarykey] - // The autoinc macro here just means every time we insert into this table - // we will receive a new row where this value will be increased by one. This - // allows us to easily get rows where `entity_id` is unique. - #[autoinc] - pub entity_id: u64, - pub position: StdbVector3, - pub direction: f32, - pub moving: bool, -} -``` - -Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `entity_id`. You'll see how this works later in the `create_player` reducer. - -**Append to the bottom of lib.rs:** - -```rust -// All players have this component and it associates an entity with the user's -// Identity. It also stores their username and whether or not they're logged in. -#[derive(Clone)] -#[spacetimedb(table)] -pub struct PlayerComponent { - // An entity_id that matches an entity_id in the `EntityComponent` table. - #[primarykey] - pub entity_id: u64, - - // The user's identity, which is unique to each player - #[unique] - pub owner_id: Identity, - pub username: String, - pub logged_in: bool, -} -``` - -Next, we write our very first reducer, `create_player`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.rs:** - -```rust -// This reducer is called when the user logs in for the first time and -// enters a username -#[spacetimedb(reducer)] -pub fn create_player(ctx: ReducerContext, username: String) -> Result<(), String> { - // Get the Identity of the client who called this reducer - let owner_id = ctx.sender; - - // Make sure we don't already have a player with this identity - if PlayerComponent::filter_by_owner_id(&owner_id).is_some() { - log::info!("Player already exists"); - return Err("Player already exists".to_string()); - } - - // Create a new entity for this player and get a unique `entity_id`. - let entity_id = EntityComponent::insert(EntityComponent - { - entity_id: 0, - position: StdbVector3 { x: 0.0, y: 0.0, z: 0.0 }, - direction: 0.0, - moving: false, - }).expect("Failed to create a unique PlayerComponent.").entity_id; - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - PlayerComponent::insert(PlayerComponent { - entity_id, - owner_id, - username: username.clone(), - logged_in: true, - }).expect("Failed to insert player component."); - - log::info!("Player created: {}({})", username, entity_id); - - Ok(()) -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `sender` value of the `ReducerContext`. -- `disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. - -**Append to the bottom of lib.rs:** - -```rust -// Called when the module is initially published -#[spacetimedb(init)] -pub fn init() { - Config::insert(Config { - version: 0, - message_of_the_day: "Hello, World!".to_string(), - }).expect("Failed to insert config."); -} -``` - -We use the `connect` and `disconnect` reducers to update the logged in state of the player. The `update_player_login_state` helper function looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `logged_in` variable and calls the auto-generated `update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.rs:** - -```rust -// Called when the client connects, we update the logged_in state to true -#[spacetimedb(connect)] -pub fn client_connected(ctx: ReducerContext) { - update_player_login_state(ctx, true); -} -``` -```rust -// Called when the client disconnects, we update the logged_in state to false -#[spacetimedb(disconnect)] -pub fn client_disconnected(ctx: ReducerContext) { - update_player_login_state(ctx, false); -} -``` -```rust -// This helper function gets the PlayerComponent, sets the logged -// in variable and updates the PlayerComponent table row. -pub fn update_player_login_state(ctx: ReducerContext, logged_in: bool) { - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - // We clone the PlayerComponent so we can edit it and pass it back. - let mut player = player.clone(); - player.logged_in = logged_in; - PlayerComponent::update_by_entity_id(&player.entity_id.clone(), player); - } -} -``` - -Our final reducer handles player movement. In `update_player_position` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `create_player` first. - -Using the `entity_id` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `update` function. - -**Append to the bottom of lib.rs:** - -```rust -// Updates the position of a player. This is also called when the player stops moving. -#[spacetimedb(reducer)] -pub fn update_player_position( - ctx: ReducerContext, - position: StdbVector3, - direction: f32, - moving: bool, -) -> Result<(), String> { - // First, look up the player using the sender identity, then use that - // entity_id to retrieve and update the EntityComponent - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - if let Some(mut entity) = EntityComponent::filter_by_entity_id(&player.entity_id) { - entity.position = position; - entity.direction = direction; - entity.moving = moving; - EntityComponent::update_by_entity_id(&player.entity_id, entity); - return Ok(()); - } - } - - // If we can not find the PlayerComponent or EntityComponent for - // this player then something went wrong. - return Err("Player not found".to_string()); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -### Finally, Add Chat Support - -The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.rs``. - -**Append to the bottom of server/src/lib.rs:** - -```rust -#[spacetimedb(table)] -pub struct ChatMessage { - // The primary key for this table will be auto-incremented - #[primarykey] - #[autoinc] - pub message_id: u64, - - // The entity id of the player that sent the message - pub sender_id: u64, - // Message contents - pub text: String, -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.rs:** - -```rust -// Adds a chat entry to the ChatMessage table -#[spacetimedb(reducer)] -pub fn send_chat_message(ctx: ReducerContext, text: String) -> Result<(), String> { - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - // Now that we have the player we can insert the chat message using the player entity id. - ChatMessage::insert(ChatMessage { - // this column auto-increments so we can set it to 0 - message_id: 0, - sender_id: player.entity_id, - text, - }) - .unwrap(); - - return Ok(()); - } - - Err("Player not found".into()) -} -``` - -## Wrapping Up - -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside/topics/unity/part-3.md b/Writerside/topics/unity/part-3.md deleted file mode 100644 index 12e85ef3..00000000 --- a/Writerside/topics/unity/part-3.md +++ /dev/null @@ -1,479 +0,0 @@ -# Unity Tutorial - Basic Multiplayer - Part 3 - Client - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from one of the Part 2 tutorials: -- [Rust Server Module](part-2a-rust1.md) -- [C# Server Module](part-2.) - -## Updating our Unity Project Client to use SpacetimeDB - -Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. - -### Import the SDK and Generate Module Files - -1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. - -```bash -https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git -``` - -![Unity-PackageManager](Unity-PackageManager.JPG) - -3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. - -```bash -mkdir -p ../client/Assets/module_bindings -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -``` - -### Connect to Your SpacetimeDB Module - -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. - -![Unity-AddNetworkManager](Unity-AddNetworkManager.JPG) - -Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: - -**Append to the top of TutorialGameManager.cs** - -```csharp -using SpacetimeDB; -using SpacetimeDB.Types; -using System.Linq; -``` - -At the top of the class definition add the following members: - -**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** - -```csharp -// These are connection variables that are exposed on the GameManager -// inspector. -[SerializeField] private string moduleAddress = "unity-tutorial"; -[SerializeField] private string hostName = "localhost:3000"; - -// This is the identity for this player that is automatically generated -// the first time you log in. We set this variable when the -// onIdentityReceived callback is triggered by the SDK after connecting -private Identity local_identity; -``` - -The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. - -Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. - -**REPLACE the Start() function in TutorialGameManager.cs** - -```csharp -// Start is called before the first frame update -void Start() -{ - instance = this; - - SpacetimeDBClient.instance.onConnect += () => - { - Debug.Log("Connected."); - - // Request all tables - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM *", - }); - }; - - // Called when we have an error connecting to SpacetimeDB - SpacetimeDBClient.instance.onConnectError += (error, message) => - { - Debug.LogError($"Connection error: " + message); - }; - - // Called when we are disconnected from SpacetimeDB - SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => - { - Debug.Log("Disconnected."); - }; - - // Called when we receive the client identity from SpacetimeDB - SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { - AuthToken.SaveToken(token); - local_identity = identity; - }; - - // Called after our local cache is populated from a Subscribe call - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - - // Now that we’ve registered all our callbacks, lets connect to spacetimedb - SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); -} -``` - -In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. - -Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. - ---- - -**Local Client Cache** - -The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. - ---- - -Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. - -**Append after the Start() function in TutorialGameManager.cs** - -```csharp -void OnSubscriptionApplied() -{ - // If we don't have any data for our player, then we are creating a - // new one. Let's show the username dialog, which will then call the - // create player reducer - var player = PlayerComponent.FilterByOwnerId(local_identity); - if (player == null) - { - // Show username selection - UIUsernameChooser.instance.Show(); - } - - // Show the Message of the Day in our Config table of the Client Cache - UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); - - // Now that we've done this work we can unregister this callback - SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; -} -``` - -### Adding the Multiplayer Functionality - -Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. - -**Append to the top of UIUsernameChooser.cs** - -```csharp -using SpacetimeDB.Types; -``` - -Then we're doing a modification to the `ButtonPressed()` function: - -**Modify the ButtonPressed function in UIUsernameChooser.cs** - -```csharp -public void ButtonPressed() -{ - CameraController.RemoveDisabler(GetHashCode()); - _panel.SetActive(false); - - // Call the SpacetimeDB CreatePlayer reducer - Reducer.CreatePlayer(_usernameField.text); -} -``` - -We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. - -First append this using to the top of `RemotePlayer.cs` - -**Create file RemotePlayer.cs, then replace its contents:** - -```csharp -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using SpacetimeDB.Types; -using TMPro; - -public class RemotePlayer : MonoBehaviour -{ - public ulong EntityId; - - public TMP_Text UsernameElement; - - public string Username { set { UsernameElement.text = value; } } - - void Start() - { - // Initialize overhead name - UsernameElement = GetComponentInChildren(); - var canvas = GetComponentInChildren(); - canvas.worldCamera = Camera.main; - - // Get the username from the PlayerComponent for this object and set it in the UI - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - if (playerComp is null) - { - string inputUsername = UsernameElement.Text; - Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); - Reducer.CreatePlayer(inputUsername); - - // Try again, optimistically assuming success for simplicity - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - } - - Username = playerComp.Username; - - // Get the last location for this player and set the initial position - EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); - transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - - // Register for a callback that is called when the client gets an - // update for a row in the EntityComponent table - EntityComponent.OnUpdate += EntityComponent_OnUpdate; - } -} -``` - -We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. - -**Append to bottom of RemotePlayer class in RemotePlayer.cs:** - -```csharp -private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) -{ - // If the update was made to this object - if(obj.EntityId == EntityId) - { - var movementController = GetComponent(); - - // Update target position, rotation, etc. - movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); - movementController.RemoteTargetRotation = obj.Direction; - movementController.SetMoving(obj.Moving); - } -} -``` - -Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. - -**Append to bottom of Start() function in TutorialGameManager.cs:** - -```csharp -PlayerComponent.OnInsert += PlayerComponent_OnInsert; -``` - -Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. - -**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** - -```csharp -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } -} -``` - -Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. - -**Append to the top of LocalPlayer.cs** - -```csharp -using SpacetimeDB.Types; -using SpacetimeDB; -``` - -**Append to the bottom of LocalPlayer class in LocalPlayer.cs** - -```csharp -private float? lastUpdateTime; -private void FixedUpdate() -{ - float? deltaTime = Time.time - lastUpdateTime; - bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; - bool isConnected = SpacetimeDBClient.instance.IsConnected(); - - if (hasUpdatedRecently || !isConnected) - { - return; - } - - lastUpdateTime = Time.time; - var p = PlayerMovementController.Local.GetModelPosition(); - - Reducer.UpdatePlayerPosition(new StdbVector3 - { - X = p.x, - Y = p.y, - Z = p.z, - }, - PlayerMovementController.Local.GetModelRotation(), - PlayerMovementController.Local.IsMoving()); -} -``` - -Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. - -![GameManager-Inspector2](GameManager-Inspector2.JPG) - -### Play the Game! - -Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. - -![Unity-AddOpenScenes](Unity-AddOpenScenes.JPG) - -When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. - -### Implement Player Logout - -So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. - -**Append to the bottom of Start() function in TutorialGameManager.cs** -```csharp -PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; -``` - -We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. - -Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. - -**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** -```csharp -private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(newValue); -} - -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(obj); -} - -private void OnPlayerComponentChanged(PlayerComponent obj) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it - var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); - if (obj.LoggedIn) - { - // Only spawn remote players who aren't already spawned - if (existingPlayer == null) - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } - } - else - { - if (existingPlayer != null) - { - Destroy(existingPlayer.gameObject); - } - } - } -} -``` - -Now you when you play the game you should see remote players disappear when they log out. - -Before updating the client, let's generate the client files and update publish our module. - -**Execute commands in the server/ directory** -```bash -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -spacetime publish -c unity-tutorial -``` - -On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. - -**Append to the top of UIChatController.cs:** -```csharp -using SpacetimeDB.Types; -``` - -**REPLACE the OnChatButtonPress function in UIChatController.cs:** - -```csharp -public void OnChatButtonPress() -{ - Reducer.SendChatMessage(_chatInput.text); - _chatInput.text = ""; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: - -**Append to the bottom of the Start() function in TutorialGameManager.cs:** -```csharp -Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; -``` - -Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. - -**Append after the Start() function in TutorialGameManager.cs** -```csharp -private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) -{ - var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); - if (player != null) - { - UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); - } -} -``` - -Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. - -## Conclusion - -This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: - -**Next Unity Tutorial:** [Resources & Scheduling](part-41.md) - ---- - -### Troubleshooting - -- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` - -- If you get this exception when running the project: - -``` -NullReferenceException: Object reference not set to an instance of an object -TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) -``` - -Check to see if your GameManager object in the Scene has the NetworkManager component attached. - -- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. - -``` -Connection error: Unable to connect to the remote server -``` diff --git a/Writerside/topics/unity/part-4.md b/Writerside/topics/unity/part-4.md deleted file mode 100644 index f17ac2b0..00000000 --- a/Writerside/topics/unity/part-4.md +++ /dev/null @@ -1,261 +0,0 @@ -# Unity Tutorial - Advanced - Part 4 - Resources and Scheduling - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 3](part-31.md) Tutorial. - -**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** - -In this second part of the lesson, we'll add resource nodes to our project and learn about scheduled reducers. Then we will spawn the nodes on the client so they are visible to the player. - -## Add Resource Node Spawner - -In this section we will add functionality to our server to spawn the resource nodes. - -### Step 1: Add the SpacetimeDB Tables for Resource Nodes - -1. Before we start adding code to the server, we need to add the ability to use the rand crate in our SpacetimeDB module so we can generate random numbers. Open the `Cargo.toml` file in the `Server` directory and add the following line to the `[dependencies]` section. - -```toml -rand = "0.8.5" -``` - -We also need to add the `getrandom` feature to our SpacetimeDB crate. Update the `spacetimedb` line to: - -```toml -spacetimedb = { "0.5", features = ["getrandom"] } -``` - -2. The first entity component we are adding, `ResourceNodeComponent`, stores the resource type. We'll define an enum to describe a `ResourceNodeComponent`'s type. For now, we'll just have one resource type: Iron. In the future, though, we'll add more resources by adding variants to the `ResourceNodeType` enum. Since we are using a custom enum, we need to mark it with the `SpacetimeType` attribute. Add the following code to lib.rs. - -```rust -#[derive(SpacetimeType, Clone)] -pub enum ResourceNodeType { - Iron, -} - -#[spacetimedb(table)] -#[derive(Clone)] -pub struct ResourceNodeComponent { - #[primarykey] - pub entity_id: u64, - - // Resource type of this resource node - pub resource_type: ResourceNodeType, -} -``` - -Because resource nodes never move, the `MobileEntityComponent` is overkill. Instead, we will add a new entity component named `StaticLocationComponent` that only stores the position and rotation. - -```rust -#[spacetimedb(table)] -#[derive(Clone)] -pub struct StaticLocationComponent { - #[primarykey] - pub entity_id: u64, - - pub location: StdbVector2, - pub rotation: f32, -} -``` - -3. We are also going to add a couple of additional column to our Config table. `map_extents` let's our spawner know where it can spawn the nodes. `num_resource_nodes` is the maximum number of nodes to spawn on the map. Update the config table in lib.rs. - -```rust -#[spacetimedb(table)] -pub struct Config { - // Config is a global table with a single row. This table will be used to - // store configuration or global variables - - #[primarykey] - // always 0 - // having a table with a primarykey field which is always zero is a way to store singleton global state - pub version: u32, - - pub message_of_the_day: String, - - // new variables for resource node spawner - // X and Z range of the map (-map_extents to map_extents) - pub map_extents: u32, - // maximum number of resource nodes to spawn on the map - pub num_resource_nodes: u32, -} -``` - -4. In the `init` reducer, we need to set the initial values of our two new variables. Update the following code: - -```rust - Config::insert(Config { - version: 0, - message_of_the_day: "Hello, World!".to_string(), - - // new variables for resource node spawner - map_extents: 25, - num_resource_nodes: 10, - }) - .expect("Failed to insert config."); -``` - -### Step 2: Write our Resource Spawner Repeating Reducer - -1. Add the following code to lib.rs. We are using a special attribute argument called repeat which will automatically schedule the reducer to run every 1000ms. - -```rust -#[spacetimedb(reducer, repeat = 1000ms)] -pub fn resource_spawner_agent(_ctx: ReducerContext, _prev_time: Timestamp) -> Result<(), String> { - let config = Config::filter_by_version(&0).unwrap(); - - // Retrieve the maximum number of nodes we want to spawn from the Config table - let num_resource_nodes = config.num_resource_nodes as usize; - - // Count the number of nodes currently spawned and exit if we have reached num_resource_nodes - let num_resource_nodes_spawned = ResourceNodeComponent::iter().count(); - if num_resource_nodes_spawned >= num_resource_nodes { - log::info!("All resource nodes spawned. Skipping."); - return Ok(()); - } - - // Pick a random X and Z based off the map_extents - let mut rng = rand::thread_rng(); - let map_extents = config.map_extents as f32; - let location = StdbVector2 { - x: rng.gen_range(-map_extents..map_extents), - z: rng.gen_range(-map_extents..map_extents), - }; - // Pick a random Y rotation in degrees - let rotation = rng.gen_range(0.0..360.0); - - // Insert our SpawnableEntityComponent which assigns us our entity_id - let entity_id = SpawnableEntityComponent::insert(SpawnableEntityComponent { entity_id: 0 }) - .expect("Failed to create resource spawnable entity component.") - .entity_id; - - // Insert our static location with the random position and rotation we selected - StaticLocationComponent::insert(StaticLocationComponent { - entity_id, - location: location.clone(), - rotation, - }) - .expect("Failed to insert resource static location component."); - - // Insert our resource node component, so far we only have iron - ResourceNodeComponent::insert(ResourceNodeComponent { - entity_id, - resource_type: ResourceNodeType::Iron, - }) - .expect("Failed to insert resource node component."); - - // Log that we spawned a node with the entity_id and location - log::info!( - "Resource node spawned: {} at ({}, {})", - entity_id, - location.x, - location.z, - ); - - Ok(()) -} -``` - -2. Since this reducer uses `rand::Rng` we need add include it. Add this `use` statement to the top of lib.rs. - -```rust -use rand::Rng; -``` - -3. Even though our reducer is set to repeat, we still need to schedule it the first time. Add the following code to the end of the `init` reducer. You can use this `schedule!` macro to schedule any reducer to run in the future after a certain amount of time. - -```rust - // Start our resource spawner repeating reducer - spacetimedb::schedule!("1000ms", resource_spawner_agent(_, Timestamp::now())); -``` - -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: - -```bash -spacetime generate --out-dir ../Assets/autogen --lang=csharp - -spacetime publish -c yourname/bitcraftmini -``` - -Your resource node spawner will start as soon as you publish since we scheduled it to run in our init reducer. You can watch the log output by using the `--follow` flag on the logs CLI command. - -```bash -spacetime logs -f yourname/bitcraftmini -``` - -### Step 3: Spawn the Resource Nodes on the Client - -1. First we need to update the `GameResource` component in Unity to work for multiplayer. Open GameResource.cs and add `using SpacetimeDB.Types;` to the top of the file. Then change the variable `Type` to be of type `ResourceNodeType` instead of `int`. Also add a new variable called `EntityId` of type `ulong`. - -```csharp - public ulong EntityId; - - public ResourceNodeType Type = ResourceNodeType.Iron; -``` - -2. Now that we've changed the `Type` variable, we need to update the code in the `PlayerAnimator` component that references it. Open PlayerAnimator.cs and update the following section of code. We need to add `using SpacetimeDB.Types;` to this file as well. This fixes the compile errors that result from changing the type of the `Type` variable to our new server generated enum. - -```csharp - var resourceType = res?.Type ?? ResourceNodeType.Iron; - switch (resourceType) - { - case ResourceNodeType.Iron: - _animator.SetTrigger("Mine"); - Interacting = true; - break; - default: - Interacting = false; - break; - } - for (int i = 0; i < _tools.Length; i++) - { - _tools[i].SetActive(((int)resourceType) == i); - } - _target = res; -``` - -3. Now that our `GameResource` is ready to be spawned, lets update the `BitcraftMiniGameManager` component to actually create them. First, we need to add the new tables to our SpacetimeDB subscription. Open BitcraftMiniGameManager.cs and update the following code: - -```csharp - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM Config", - "SELECT * FROM SpawnableEntityComponent", - "SELECT * FROM PlayerComponent", - "SELECT * FROM MobileEntityComponent", - // Our new tables for part 2 of the tutorial - "SELECT * FROM ResourceNodeComponent", - "SELECT * FROM StaticLocationComponent" - }); -``` - -4. Next let's add an `OnInsert` handler for the `ResourceNodeComponent`. Add the following line to the `Start` function. - -```csharp - ResourceNodeComponent.OnInsert += ResourceNodeComponent_OnInsert; -``` - -5. Finally we add the new function to handle the insert event. This function will be called whenever a new `ResourceNodeComponent` is inserted into our local client cache. We can use this to spawn the resource node in the world. Add the following code to the `BitcraftMiniGameManager` class. - -To get the position and the rotation of the node, we look up the `StaticLocationComponent` for this entity by using the EntityId. - -```csharp - private void ResourceNodeComponent_OnInsert(ResourceNodeComponent insertedValue, ReducerEvent callInfo) - { - switch(insertedValue.ResourceType) - { - case ResourceNodeType.Iron: - var iron = Instantiate(IronPrefab); - StaticLocationComponent loc = StaticLocationComponent.FilterByEntityId(insertedValue.EntityId); - Vector3 nodePos = new Vector3(loc.Location.X, 0.0f, loc.Location.Z); - iron.transform.position = new Vector3(nodePos.x, MathUtil.GetTerrainHeight(nodePos), nodePos.z); - iron.transform.rotation = Quaternion.Euler(0.0f, loc.Rotation, 0.0f); - break; - } - } -``` - -### Step 4: Play the Game! - -6. Hit Play in the Unity Editor and you should now see your resource nodes spawning in the world! diff --git a/Writerside/topics/unity/part-5.md b/Writerside/topics/unity/part-5.md deleted file mode 100644 index d4274636..00000000 --- a/Writerside/topics/unity/part-5.md +++ /dev/null @@ -1,108 +0,0 @@ -# Unity Tutorial - Advanced - Part 5 - BitCraft Mini - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 4](part-31.md) Tutorial. - -**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** - -BitCraft Mini is a game that we developed which extends the code you've already developed in this tutorial. It is inspired by our game [BitCraft](https://bitcraftonline.com) and illustrates how you could build a more complex game from just the components we've discussed. Right now you can walk around, mine ore, and manage your inventory. - -## 1. Download - -You can git-clone BitCraftMini from here: - -```plaintext -git clone ssh://git@github.com/clockworklabs/BitCraftMini -``` - -Once you have downloaded BitCraftMini, you will need to compile the spacetime module. - -## 2. Compile the Spacetime Module - -In order to compile the BitCraftMini module, you will need to install cargo. You can install cargo from here: - -> https://www.rust-lang.org/tools/install - -Once you have cargo installed, you can compile and publish the module with these commands: - -```bash -cd BitCraftMini/Server -spacetime publish -``` - -`spacetime publish` will output an address where your module has been deployed to. You will want to copy/save this address because you will need it in step 3. Here is an example of what it should look like: - -```plaintext -$ spacetime publish -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - Finished release [optimized] target(s) in 0.03s -Publish finished successfully. -Created new database with address: c91c17ecdcea8a05302be2bad9dd59b3 -``` - -Optionally, you can specify a name when you publish the module: - -```bash -spacetime publish "unique-module-name" -``` - -Currently, all the named modules exist in the same namespace so if you get a message saying that database is not owned by you, it means that someone else has already published a module with that name. You can either choose a different name or you can use the address instead. If you specify a name when you publish, you can use that name in place of the autogenerated address in both the CLI and in the Unity client. - -In the BitCraftMini module we have a function called `initialize()`. This function should be called immediately after publishing the module to spacetimedb. This function is in charge of generating some initial settings that are required for the server to operate. You can call this function like so: - -```bash -spacetime call "" "initialize" "[]" -``` - -Here we are telling spacetime to invoke the `initialize()` function on our module "bitcraftmini". If the function had some arguments, we would json encode them and put them into the "[]". Since `initialize()` requires no parameters, we just leave it empty. - -After you have called `initialize()` on the spacetime module you shouldgenerate the client files: - -```bash -spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs -``` - -Here is some sample output: - -```plaintext -$ spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang cs -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - Finished release [optimized] target(s) in 0.03s -compilation took 234.613518ms -Generate finished successfully. -``` - -If you've gotten this message then everything should be working properly so far. - -## 3. Replace address in BitCraftMiniGameManager - -The following settings are exposed in the `BitCraftMiniGameManager` inspector: Module Address, Host Name, and SSL Enabled. - -Open the Main scene in Unity and click on the `GameManager` object in the heirarchy. The inspector window will look like this: - -![GameManager-Inspector](GameManager-Inspector.JPG) - -Update the module address with the address you got from the `spacetime publish` command. If you are using SpacetimeDB Cloud `testnet`, the host name should be `testnet.spacetimedb.com` and SSL Enabled should be checked. If you are running SpacetimeDB Standalone locally, the host name should be `localhost:3000` and SSL Enabled should be unchecked. For instructions on how to deploy to these environments, see the [Deployment Section](testnet.) - -## 4. Play Mode - -You should now be able to enter play mode and walk around! You can mine some rocks, cut down some trees and if you connect more clients you can trade with other players. - -## 5. Editing the Module - -If you want to make further updates to the module, make sure to use this publish command instead: - -```bash -spacetime publish -``` - -Where `` is your own address. If you do this instead then you won't have to change the address inside of `BitCraftMiniGameManager.cs` - -When you change the server module you should also regenerate the client files as well: - -```bash -spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs -``` - -You may want to consider putting these 2 commands into a simple shell script to make the process a bit cleaner. diff --git a/Writerside/topics/unity/unity_index.md b/Writerside/topics/unity/unity_index.md deleted file mode 100644 index a16870f0..00000000 --- a/Writerside/topics/unity/unity_index.md +++ /dev/null @@ -1,24 +0,0 @@ -# Unity Tutorial Overview - -💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! - -The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, Git, using a commandline terminal and coding. - -We'll give you some CLI commands to execute. If you are using Windows, we recommend using Git Bash or PowerShell. For Mac, we recommend Terminal. - -Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll be opening .cs files in an IDE like _Visual Studio_ or _Rider_. - -## Unity Tutorial - Basic Multiplayer -Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](rust.) or [C#](c-sharp.): - -- [Part 1 - Setup](part-11.md) -- [Part 2 - Server (C#)](part-21.md) ☼ -- [Part 3 - Client (Unity)](part-31.md) - -☼ While the tutorial uses C#, the repo cloned in [Part 1](part-11.md) does include a legacy Rust example to optionally use, instead. - -## Unity Tutorial - Advanced -By this point, you should already have a basic understanding of SpacetimeDB client, server and CLI: - -- [Part 4 - Resources & Scheduling](part-41.md) -- [Part 5 - BitCraft Mini](part-51.md) diff --git a/Writerside/topics/webassembly-abi/webassembly-abi_index.md b/Writerside/topics/webassembly-abi/webassembly-abi_index.md deleted file mode 100644 index ceccfbd1..00000000 --- a/Writerside/topics/webassembly-abi/webassembly-abi_index.md +++ /dev/null @@ -1,499 +0,0 @@ -# Module ABI Reference - -This document specifies the _low level details_ of module-host interactions (_"Module ABI"_). _**Most users**_ looking to interact with the host will want to use derived and higher level functionality like [`bindings`], `#[spacetimedb(table)]`, and `#[derive(SpacetimeType)]` rather than this low level ABI. For more on those, read the [Rust module quick start][module_quick_start] guide and the [Rust module reference][module_ref]. - -The Module ABI is defined in [`bindings_sys::raw`] and is used by modules to interact with their host and perform various operations like: - -- logging, -- transporting data, -- scheduling reducers, -- altering tables, -- inserting and deleting rows, -- querying tables. - -In the next few sections, we'll define the functions that make up the ABI and what these functions do. - -## General notes - -The functions in this ABI all use the [`C` ABI on the `wasm32` platform][wasm_c_abi]. They are specified in a Rust `extern "C" { .. }` block. For those more familiar with the `C` notation, an [appendix][c_header] is provided with equivalent definitions as would occur in a `.h` file. - -Many functions in the ABI take in- or out-pointers, e.g. `*const u8` and `*mut u8`. The WASM host itself does not have undefined behavior. However, what WASM does not consider a memory access violation could be one according to some other language's abstract machine. For example, running the following on a WASM host would violate Rust's rules around writing across allocations: - -```rust -fn main() { - let mut bytes = [0u8; 12]; - let other_bytes = [0u8; 4]; - unsafe { ffi_func_with_out_ptr_and_len(&mut bytes as *mut u8, 16); } - assert_eq!(other_bytes, [0u8; 4]); -} -``` - -When we note in this reference that traps occur or errors are returned on memory access violations, we only mean those that WASM can directly detected, and not cases like the one above. - -Should memory access violations occur, such as a buffer overrun, undefined behavior will never result, as it does not exist in WASM. However, in many cases, an error code will result. - -Some functions will treat UTF-8 strings _lossily_. That is, if the slice identified by a `(ptr, len)` contains non-UTF-8 bytes, these bytes will be replaced with `�` in the read string. - -Most functions return a `u16` value. This is how these functions indicate an error where a `0` value means that there were no errors. Such functions will instead return any data they need to through out pointers. - -## Logging - -```rust -/// The error log level. -const LOG_LEVEL_ERROR: u8 = 0; -/// The warn log level. -const LOG_LEVEL_WARN: u8 = 1; -/// The info log level. -const LOG_LEVEL_INFO: u8 = 2; -/// The debug log level. -const LOG_LEVEL_DEBUG: u8 = 3; -/// The trace log level. -const LOG_LEVEL_TRACE: u8 = 4; -/// The panic log level. -/// -/// A panic level is emitted just before -/// a fatal error causes the WASM module to trap. -const LOG_LEVEL_PANIC: u8 = 101; - -/// Log at `level` a `text` message occuring in `filename:line_number` -/// with `target` being the module path at the `log!` invocation site. -/// -/// These various pointers are interpreted lossily as UTF-8 strings. -/// The data pointed to are copied. Ownership does not transfer. -/// -/// See https://docs.rs/log/latest/log/struct.Record.html#method.target -/// for more info on `target`. -/// -/// Calls to the function cannot fail -/// irrespective of memory access violations. -/// If they occur, no message is logged. -fn _console_log( - // The level we're logging at. - // One of the `LOG_*` constants above. - level: u8, - // The module path, if any, associated with the message - // or to "blame" for the reason we're logging. - // - // This is a pointer to a buffer holding an UTF-8 encoded string. - // When the pointer is `NULL`, `target` is ignored. - target: *const u8, - // The length of the buffer pointed to by `text`. - // Unused when `target` is `NULL`. - target_len: usize, - // The file name, if any, associated with the message - // or to "blame" for the reason we're logging. - // - // This is a pointer to a buffer holding an UTF-8 encoded string. - // When the pointer is `NULL`, `filename` is ignored. - filename: *const u8, - // The length of the buffer pointed to by `text`. - // Unused when `filename` is `NULL`. - filename_len: usize, - // The line number associated with the message - // or to "blame" for the reason we're logging. - line_number: u32, - // A pointer to a buffer holding an UTF-8 encoded message to log. - text: *const u8, - // The length of the buffer pointed to by `text`. - text_len: usize, -); -``` - -## Buffer handling - -```rust -/// Returns the length of buffer `bufh` without -/// transferring ownership of the data into the function. -/// -/// The `bufh` must have previously been allocating using `_buffer_alloc`. -/// -/// Traps if the buffer does not exist. -fn _buffer_len( - // The buffer previously allocated using `_buffer_alloc`. - // Ownership of the buffer is not taken. - bufh: ManuallyDrop -) -> usize; - -/// Consumes the buffer `bufh`, -/// moving its contents to the WASM byte slice `(ptr, len)`. -/// -/// Returns an error if the buffer does not exist -/// or on any memory access violations associated with `(ptr, len)`. -fn _buffer_consume( - // The buffer to consume and move into `(ptr, len)`. - // Ownership of the buffer and its contents are taken. - // That is, `bufh` won't be usable after this call. - bufh: Buffer, - // A WASM out pointer to write the contents of `bufh` to. - ptr: *mut u8, - // The size of the buffer pointed to by `ptr`. - // This size must match that of `bufh` or a trap will occur. - len: usize -); - -/// Creates a buffer of size `data_len` in the host environment. -/// -/// The contents of the byte slice lasting `data_len` bytes -/// at the `data` WASM pointer are read -/// and written into the newly initialized buffer. -/// -/// Traps on any memory access violations. -fn _buffer_alloc(data: *const u8, data_len: usize) -> Buffer; -``` - -## Reducer scheduling - -```rust -/// Schedules a reducer to be called asynchronously at `time`. -/// -/// The reducer is named as the valid UTF-8 slice `(name, name_len)`, -/// and is passed the slice `(args, args_len)` as its argument. -/// -/// A generated schedule id is assigned to the reducer. -/// This id is written to the pointer `out`. -/// -/// Errors on any memory access violations, -/// if `(name, name_len)` does not point to valid UTF-8, -/// or if the `time` delay exceeds `64^6 - 1` milliseconds from now. -fn _schedule_reducer( - // A pointer to a buffer - // with a valid UTF-8 string of `name_len` many bytes. - name: *const u8, - // The number of bytes in the `name` buffer. - name_len: usize, - // A pointer to a byte buffer of `args_len` many bytes. - args: *const u8, - // The number of bytes in the `args` buffer. - args_len: usize, - // When to call the reducer. - time: u64, - // The schedule ID is written to this out pointer on a successful call. - out: *mut u64, -); - -/// Unschedules a reducer -/// using the same `id` generated as when it was scheduled. -/// -/// This assumes that the reducer hasn't already been executed. -fn _cancel_reducer(id: u64); -``` - -## Altering tables - -```rust -/// Creates an index with the name `index_name` and type `index_type`, -/// on a product of the given columns in `col_ids` -/// in the table identified by `table_id`. -/// -/// Here `index_name` points to a UTF-8 slice in WASM memory -/// and `col_ids` points to a byte slice in WASM memory -/// with each element being a column. -/// -/// Currently only single-column-indices are supported -/// and they may only be of the btree index type. -/// In the former case, the function will panic, -/// and in latter, an error is returned. -/// -/// Returns an error on any memory access violations, -/// if `(index_name, index_name_len)` is not valid UTF-8, -/// or when a table with the provided `table_id` doesn't exist. -/// -/// Traps if `index_type /= 0` or if `col_len /= 1`. -fn _create_index( - // A pointer to a buffer holding an UTF-8 encoded index name. - index_name: *const u8, - // The length of the buffer pointed to by `index_name`. - index_name_len: usize, - // The ID of the table to create the index for. - table_id: u32, - // The type of the index. - // Must be `0` currently, that is, a btree-index. - index_type: u8, - // A pointer to a buffer holding a byte slice - // where each element is the position - // of a column to include in the index. - col_ids: *const u8, - // The length of the byte slice in `col_ids`. Must be `1`. - col_len: usize, -) -> u16; -``` - -## Inserting and deleting rows - -```rust -/// Inserts a row into the table identified by `table_id`, -/// where the row is read from the byte slice `row_ptr` in WASM memory, -/// lasting `row_len` bytes. -/// -/// Errors if there were unique constraint violations, -/// if there were any memory access violations in associated with `row`, -/// if the `table_id` doesn't identify a table, -/// or if `(row, row_len)` doesn't decode from BSATN to a `ProductValue` -/// according to the `ProductType` that the table's schema specifies. -fn _insert( - // The table to insert the row into. - // The interpretation of `(row, row_len)` depends on this ID - // as it's table schema determines how to decode the raw bytes. - table_id: u32, - // An in/out pointer to a byte buffer - // holding the BSATN-encoded `ProductValue` row data to insert. - // - // The pointer is written to with the inserted row re-encoded. - // This is due to auto-incrementing columns. - row: *mut u8, - // The length of the buffer pointed to by `row`. - row_len: usize -) -> u16; - -/// Deletes all rows in the table identified by `table_id` -/// where the column identified by `col_id` matches the byte string, -/// in WASM memory, pointed to by `value`. -/// -/// Matching is defined by decoding of `value` to an `AlgebraicValue` -/// according to the column's schema and then `Ord for AlgebraicValue`. -/// -/// The number of rows deleted is written to the WASM pointer `out`. -/// -/// Errors if there were memory access violations -/// associated with `value` or `out`, -/// if no columns were deleted, -/// or if the column wasn't found. -fn _delete_by_col_eq( - // The table to delete rows from. - table_id: u32, - // The position of the column to match `(value, value_len)` against. - col_id: u32, - // A pointer to a byte buffer holding a BSATN-encoded `AlgebraicValue` - // of the `AlgebraicType` that the table's schema specifies - // for the column identified by `col_id`. - value: *const u8, - // The length of the buffer pointed to by `value`. - value_len: usize, - // An out pointer that the number of rows deleted is written to. - out: *mut u32 -) -> u16; -``` - -## Querying tables - -```rust -/// Queries the `table_id` associated with the given (table) `name` -/// where `name` points to a UTF-8 slice -/// in WASM memory of `name_len` bytes. -/// -/// The table id is written into the `out` pointer. -/// -/// Errors on memory access violations associated with `name` -/// or if the table does not exist. -fn _get_table_id( - // A pointer to a buffer holding the name of the table - // as a valid UTF-8 encoded string. - name: *const u8, - // The length of the buffer pointed to by `name`. - name_len: usize, - // An out pointer to write the table ID to. - out: *mut u32 -) -> u16; - -/// Finds all rows in the table identified by `table_id`, -/// where the row has a column, identified by `col_id`, -/// with data matching the byte string, -/// in WASM memory, pointed to at by `val`. -/// -/// Matching is defined by decoding of `value` -/// to an `AlgebraicValue` according to the column's schema -/// and then `Ord for AlgebraicValue`. -/// -/// The rows found are BSATN encoded and then concatenated. -/// The resulting byte string from the concatenation -/// is written to a fresh buffer -/// with the buffer's identifier written to the WASM pointer `out`. -/// -/// Errors if no table with `table_id` exists, -/// if `col_id` does not identify a column of the table, -/// if `(value, value_len)` cannot be decoded to an `AlgebraicValue` -/// typed at the `AlgebraicType` of the column, -/// or if memory access violations occurred associated with `value` or `out`. -fn _iter_by_col_eq( - // Identifies the table to find rows in. - table_id: u32, - // The position of the column in the table - // to match `(value, value_len)` against. - col_id: u32, - // A pointer to a byte buffer holding a BSATN encoded - // value typed at the `AlgebraicType` of the column. - value: *const u8, - // The length of the buffer pointed to by `value`. - value_len: usize, - // An out pointer to which the new buffer's id is written to. - out: *mut Buffer -) -> u16; - -/// Starts iteration on each row, as bytes, -/// of a table identified by `table_id`. -/// -/// The iterator is registered in the host environment -/// under an assigned index which is written to the `out` pointer provided. -/// -/// Errors if the table doesn't exist -/// or if memory access violations occurred in association with `out`. -fn _iter_start( - // The ID of the table to start row iteration on. - table_id: u32, - // An out pointer to which an identifier - // to the newly created buffer is written. - out: *mut BufferIter -) -> u16; - -/// Like [`_iter_start`], starts iteration on each row, -/// as bytes, of a table identified by `table_id`. -/// -/// The rows are filtered through `filter`, which is read from WASM memory -/// and is encoded in the embedded language defined by `spacetimedb_lib::filter::Expr`. -/// -/// The iterator is registered in the host environment -/// under an assigned index which is written to the `out` pointer provided. -/// -/// Errors if `table_id` doesn't identify a table, -/// if `(filter, filter_len)` doesn't decode to a filter expression, -/// or if there were memory access violations -/// in association with `filter` or `out`. -fn _iter_start_filtered( - // The ID of the table to start row iteration on. - table_id: u32, - // A pointer to a buffer holding an encoded filter expression. - filter: *const u8, - // The length of the buffer pointed to by `filter`. - filter_len: usize, - // An out pointer to which an identifier - // to the newly created buffer is written. - out: *mut BufferIter -) -> u16; - -/// Advances the registered iterator with the index given by `iter_key`. -/// -/// On success, the next element (the row as bytes) is written to a buffer. -/// The buffer's index is returned and written to the `out` pointer. -/// If there are no elements left, an invalid buffer index is written to `out`. -/// On failure however, the error is returned. -/// -/// Errors if `iter` does not identify a registered `BufferIter`, -/// or if there were memory access violations in association with `out`. -fn _iter_next( - // An identifier for the iterator buffer to advance. - // Ownership of the buffer nor the identifier is moved into the function. - iter: ManuallyDrop, - // An out pointer to write the newly created buffer's identifier to. - out: *mut Buffer -) -> u16; - -/// Drops the entire registered iterator with the index given by `iter_key`. -/// The iterator is effectively de-registered. -/// -/// Returns an error if the iterator does not exist. -fn _iter_drop( - // An identifier for the iterator buffer to unregister / drop. - iter: ManuallyDrop -) -> u16; -``` - -## Appendix, `bindings.h` - -```c -#include -#include -#include -#include -#include - -typedef uint32_t Buffer; -typedef uint32_t BufferIter; - -void _console_log( - uint8_t level, - const uint8_t *target, - size_t target_len, - const uint8_t *filename, - size_t filename_len, - uint32_t line_number, - const uint8_t *text, - size_t text_len -); - - -Buffer _buffer_alloc( - const uint8_t *data, - size_t data_len -); -void _buffer_consume( - Buffer bufh, - uint8_t *into, - size_t len -); -size_t _buffer_len(Buffer bufh); - - -void _schedule_reducer( - const uint8_t *name, - size_t name_len, - const uint8_t *args, - size_t args_len, - uint64_t time, - uint64_t *out -); -void _cancel_reducer(uint64_t id); - - -uint16_t _create_index( - const uint8_t *index_name, - size_t index_name_len, - uint32_t table_id, - uint8_t index_type, - const uint8_t *col_ids, - size_t col_len -); - - -uint16_t _insert( - uint32_t table_id, - uint8_t *row, - size_t row_len -); -uint16_t _delete_by_col_eq( - uint32_t table_id, - uint32_t col_id, - const uint8_t *value, - size_t value_len, - uint32_t *out -); - - -uint16_t _get_table_id( - const uint8_t *name, - size_t name_len, - uint32_t *out -); -uint16_t _iter_by_col_eq( - uint32_t table_id, - uint32_t col_id, - const uint8_t *value, - size_t value_len, - Buffer *out -); -uint16_t _iter_drop(BufferIter iter); -uint16_t _iter_next(BufferIter iter, Buffer *out); -uint16_t _iter_start(uint32_t table_id, BufferIter *out); -uint16_t _iter_start_filtered( - uint32_t table_id, - const uint8_t *filter, - size_t filter_len, - BufferIter *out -); -``` - -[`bindings_sys::raw`]: https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/bindings-sys/src/lib.rs#L44-L215 -[`bindings`]: https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/bindings/src/lib.rs -[module_ref]: /docs/languages/rust/rust-module-reference -[module_quick_start]: /docs/languages/rust/rust-module-quick-start -[wasm_c_abi]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md -[c_header]: #appendix-bindingsh diff --git a/Writerside/topics/ws/ws_index.md b/Writerside/topics/ws/ws_index.md deleted file mode 100644 index 4814bb45..00000000 --- a/Writerside/topics/ws/ws_index.md +++ /dev/null @@ -1,318 +0,0 @@ -# The SpacetimeDB WebSocket API - -As an extension of the [HTTP API](http-api-reference.), SpacetimeDB offers a WebSocket API. Clients can subscribe to a database via a WebSocket connection to receive streaming updates as the database changes, and send requests to invoke reducers. Messages received from the server over a WebSocket will follow the same total ordering of transactions as are committed to the database. - -The SpacetimeDB SDKs comminicate with their corresponding database using the WebSocket API. - -## Connecting - -To initiate a WebSocket connection, send a `GET` request to the [`/database/subscribe/:name_or_address` endpoint](database#databasesubscribename_or_address-get.) with headers appropriate to upgrade to a WebSocket connection as per [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). - -To re-connect with an existing identity, include its token in a [SpacetimeDB Authorization header](http.). Otherwise, a new identity and token will be generated for the client. - -## Protocols - -Clients connecting via WebSocket can choose between two protocols, [`v1.bin.spacetimedb`](#binary-protocol.) and [`v1.text.spacetimedb`](#text-protocol.). Clients should include one of these protocols in the `Sec-WebSocket-Protocol` header of their request. - -| `Sec-WebSocket-Protocol` header value | Selected protocol | -| ------------------------------------- | -------------------------- | -| `v1.bin.spacetimedb` | [Binary](#binary-protocol.) | -| `v1.text.spacetimedb` | [Text](#text-protocol.) | - -### Binary Protocol - -The SpacetimeDB binary WebSocket protocol, `v1.bin.spacetimedb`, encodes messages using [ProtoBuf 3](https://protobuf.dev), and reducer and row data using [BSATN](bsatn.). - -The binary protocol's messages are defined in [`client_api.proto`](https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/client-api-messages/protobuf/client_api.proto). - -### Text Protocol - -The SpacetimeDB text WebSocket protocol, `v1.text.spacetimedb`, encodes messages, reducer and row data as JSON. Reducer arguments and table rows are JSON-encoded according to the [SATN JSON format](satn.). - -## Messages - -### Client to server - -| Message | Description | -| ------------------------------- | --------------------------------------------------------------------------- | -| [`FunctionCall`](#functioncall.) | Invoke a reducer. | -| [`Subscribe`](#subscribe.) | Register queries to receive streaming updates for a subset of the database. | - -#### `FunctionCall` - -Clients send a `FunctionCall` message to request that the database run a reducer. The message includes the reducer's name and a SATS `ProductValue` of arguments. - -##### Binary: ProtoBuf definition - -```protobuf -message FunctionCall { - string reducer = 1; - bytes argBytes = 2; -} -``` - -| Field | Value | -| ---------- | -------------------------------------------------------- | -| `reducer` | The name of the reducer to invoke. | -| `argBytes` | The reducer arguments encoded as a BSATN `ProductValue`. | - -##### Text: JSON encoding - -```typescript -{ - "call": { - "fn": string, - "args": array, - } -} -``` - -| Field | Value | -| ------ | ---------------------------------------------- | -| `fn` | The name of the reducer to invoke. | -| `args` | The reducer arguments encoded as a JSON array. | - -#### `Subscribe` - -Clients send a `Subscribe` message to register SQL queries in order to receive streaming updates. - -The client will only receive [`TransactionUpdate`s](#transactionupdate.) for rows to which it is subscribed, and for reducer runs which alter at least one subscribed row. As a special exception, the client is always notified when a reducer run it requests via a [`FunctionCall` message](#functioncall.) fails. - -SpacetimeDB responds to each `Subscribe` message with a [`SubscriptionUpdate` message](#subscriptionupdate.) containing all matching rows at the time the subscription is applied. - -Each `Subscribe` message establishes a new set of subscriptions, replacing all previous subscriptions. Clients which want to add a query to an existing subscription must send a `Subscribe` message containing all the previous queries in addition to the new query. In this case, the returned [`SubscriptionUpdate`](#subscriptionupdate.) will contain all previously-subscribed rows in addition to the newly-subscribed rows. - -Each query must be a SQL `SELECT * FROM` statement on a single table with an optional `WHERE` clause. See the [SQL Reference](sql.) for the subset of SQL supported by SpacetimeDB. - -##### Binary: ProtoBuf definition - -```protobuf -message Subscribe { - repeated string query_strings = 1; -} -``` - -| Field | Value | -| --------------- | ----------------------------------------------------------------- | -| `query_strings` | A sequence of strings, each of which contains a single SQL query. | - -##### Text: JSON encoding - -```typescript -{ - "subscribe": { - "query_strings": array - } -} -``` - -| Field | Value | -| --------------- | --------------------------------------------------------------- | -| `query_strings` | An array of strings, each of which contains a single SQL query. | - -### Server to client - -| Message | Description | -| ------------------------------------------- | -------------------------------------------------------------------------- | -| [`IdentityToken`](#identitytoken.) | Sent once upon successful connection with the client's identity and token. | -| [`SubscriptionUpdate`](#subscriptionupdate.) | Initial message in response to a [`Subscribe` message](#subscribe.). | -| [`TransactionUpdate`](#transactionupdate.) | Streaming update after a reducer runs containing altered rows. | - -#### `IdentityToken` - -Upon establishing a WebSocket connection, the server will send an `IdentityToken` message containing the client's identity and token. If the client included a [SpacetimeDB Authorization header](http.) in their connection request, the `IdentityToken` message will contain the same token used to connect, and its corresponding identity. If the client connected anonymously, SpacetimeDB will generate a new identity and token for the client. - -##### Binary: ProtoBuf definition - -```protobuf -message IdentityToken { - bytes identity = 1; - string token = 2; -} -``` - -| Field | Value | -| ---------- | --------------------------------------- | -| `identity` | The client's public Spacetime identity. | -| `token` | The client's private access token. | - -##### Text: JSON encoding - -```typescript -{ - "IdentityToken": { - "identity": array, - "token": string - } -} -``` - -| Field | Value | -| ---------- | --------------------------------------- | -| `identity` | The client's public Spacetime identity. | -| `token` | The client's private access token. | - -#### `SubscriptionUpdate` - -In response to a [`Subscribe` message](#subscribe.), the database sends a `SubscriptionUpdate` containing all of the matching rows which are resident in the database at the time the `Subscribe` was received. - -##### Binary: ProtoBuf definition - -```protobuf -message SubscriptionUpdate { - repeated TableUpdate tableUpdates = 1; -} - -message TableUpdate { - uint32 tableId = 1; - string tableName = 2; - repeated TableRowOperation tableRowOperations = 3; -} - -message TableRowOperation { - enum OperationType { - DELETE = 0; - INSERT = 1; - } - OperationType op = 1; - bytes row = 3; -} -``` - -Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribed rows. Each `TableUpdate` contains a `TableRowOperation` for each subscribed row. `SubscriptionUpdate`, `TableUpdate` and `TableRowOperation` are also used by the [`TransactionUpdate` message](#transactionupdate.) to encode rows altered by a reducer, so `TableRowOperation` includes an `OperationType` which identifies the row alteration as either an insert or a delete. When a client receives a `SubscriptionUpdate` message in response to a [`Subscribe` message](#subscribe.), all of the `TableRowOperation`s will have `op` of `INSERT`. - -| `TableUpdate` field | Value | -| -------------------- | ------------------------------------------------------------------------------------------------------------- | -| `tableId` | An integer identifier for the table. A table's `tableId` is not stable, so clients should not depend on it. | -| `tableName` | The string name of the table. Clients should use this field to identify the table, rather than the `tableId`. | -| `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`. | - -##### Text: JSON encoding - -```typescript -// SubscriptionUpdate: -{ - "SubscriptionUpdate": { - "table_updates": array - } -} - -// TableUpdate: -{ - "table_id": number, - "table_name": string, - "table_row_operations": array -} - -// TableRowOperation: -{ - "op": "insert" | "delete", - "row": array -} -``` - -Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribed rows. Each `TableUpdate` contains a `TableRowOperation` for each subscribed row. `SubscriptionUpdate`, `TableUpdate` and `TableRowOperation` are also used by the [`TransactionUpdate` message](#transactionupdate.) to encode rows altered by a reducer, so `TableRowOperation` includes an `"op"` field which identifies the row alteration as either an insert or a delete. When a client receives a `SubscriptionUpdate` message in response to a [`Subscribe` message](#subscribe.), all of the `TableRowOperation`s will have `"op"` of `"insert"`. - -| `TableUpdate` field | Value | -| ---------------------- | -------------------------------------------------------------------------------------------------------------- | -| `table_id` | An integer identifier for the table. A table's `table_id` is not stable, so clients should not depend on it. | -| `table_name` | The string name of the table. Clients should use this field to identify the table, rather than the `table_id`. | -| `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. | - -#### `TransactionUpdate` - -Upon a reducer run, a client will receive a `TransactionUpdate` containing information about the reducer which ran and the subscribed rows which it altered. Clients will only receive a `TransactionUpdate` for a reducer invocation if either of two criteria is met: - -1. The reducer ran successfully and altered at least one row to which the client subscribes. -2. The reducer was invoked by the client, and either failed or was terminated due to insufficient energy. - -Each `TransactionUpdate` contains a [`SubscriptionUpdate`](#subscriptionupdate.) with all rows altered by the reducer, including inserts and deletes; and an `Event` with information about the reducer itself, including a [`FunctionCall`](#functioncall.) containing the reducer's name and arguments. - -##### Binary: ProtoBuf definition - -```protobuf -message TransactionUpdate { - Event event = 1; - SubscriptionUpdate subscriptionUpdate = 2; -} - -message Event { - enum Status { - committed = 0; - failed = 1; - out_of_energy = 2; - } - uint64 timestamp = 1; - bytes callerIdentity = 2; - FunctionCall functionCall = 3; - Status status = 4; - string message = 5; - int64 energy_quanta_used = 6; - uint64 host_execution_duration_micros = 7; -} -``` - -| Field | Value | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| `event` | An `Event` containing information about the reducer run. | -| `subscriptionUpdate` | A [`SubscriptionUpdate`](#subscriptionupdate.) containing all the row insertions and deletions committed by the transaction. | - -| `Event` field | Value | -| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `timestamp` | The time when the reducer started, as microseconds since the Unix epoch. | -| `callerIdentity` | The identity of the client which requested the reducer invocation. For event-driven and scheduled reducers, this is the identity of the database owner. | -| `functionCall` | A [`FunctionCall`](#functioncall.) containing the name of the reducer and the arguments passed to it. | -| `status` | `committed` if the reducer ran successfully and its changes were committed to the database; `failed` if the reducer signaled an error; `out_of_energy` if the reducer was canceled due to insufficient energy. | -| `message` | The error message with which the reducer failed if `status` is `failed`, or the empty string otherwise. | -| `energy_quanta_used` | The amount of energy consumed by running the reducer. | -| `host_execution_duration_micros` | The duration of the reducer's execution, in microseconds. | - -##### Text: JSON encoding - -```typescript -// TransactionUpdate: -{ - "TransactionUpdate": { - "event": Event, - "subscription_update": SubscriptionUpdate - } -} - -// Event: -{ - "timestamp": number, - "status": "committed" | "failed" | "out_of_energy", - "caller_identity": string, - "function_call": { - "reducer": string, - "args": array, - }, - "energy_quanta_used": number, - "message": string -} -``` - -| Field | Value | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| `event` | An `Event` containing information about the reducer run. | -| `subscription_update` | A [`SubscriptionUpdate`](#subscriptionupdate.) containing all the row insertions and deletions committed by the transaction. | - -| `Event` field | Value | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `timestamp` | The time when the reducer started, as microseconds since the Unix epoch. | -| `status` | `committed` if the reducer ran successfully and its changes were committed to the database; `failed` if the reducer signaled an error; `out_of_energy` if the reducer was canceled due to insufficient energy. | -| `caller_identity` | The identity of the client which requested the reducer invocation. For event-driven and scheduled reducers, this is the identity of the database owner. | -| `function_call.reducer` | The name of the reducer. | -| `function_call.args` | The reducer arguments encoded as a JSON array. | -| `energy_quanta_used` | The amount of energy consumed by running the reducer. | -| `message` | The error message with which the reducer failed if `status` is `failed`, or the empty string otherwise. | diff --git a/Writerside/v.list b/Writerside/v.list deleted file mode 100644 index 2d12cb39..00000000 --- a/Writerside/v.list +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Writerside/writerside.cfg b/Writerside/writerside.cfg deleted file mode 100644 index 9b301c2a..00000000 --- a/Writerside/writerside.cfg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Writerside2/c.list b/Writerside2/c.list deleted file mode 100644 index c4c77a29..00000000 --- a/Writerside2/c.list +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Writerside2/cfg/buildprofiles.xml b/Writerside2/cfg/buildprofiles.xml deleted file mode 100644 index a72eec8e..00000000 --- a/Writerside2/cfg/buildprofiles.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - true - false - - - - diff --git a/Writerside2/s.tree b/Writerside2/s.tree deleted file mode 100644 index ebb0279d..00000000 --- a/Writerside2/s.tree +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Writerside2/topics/bsatn.md b/Writerside2/topics/bsatn.md deleted file mode 100644 index f8aeca7f..00000000 --- a/Writerside2/topics/bsatn.md +++ /dev/null @@ -1,115 +0,0 @@ -# SATN Binary Format (BSATN) - -The Spacetime Algebraic Type Notation binary (BSATN) format defines -how Spacetime `AlgebraicValue`s and friends are encoded as byte strings. - -Algebraic values and product values are BSATN-encoded for e.g., -module-host communication and for storing row data in the database. - -## Notes on notation - -In this reference, we give a formal definition of the format. -To do this, we use inductive definitions, and define the following notation: - -- `bsatn(x)` denotes a function converting some value `x` to a list of bytes. -- `a: B` means that `a` is of type `B`. -- `Foo(x)` denotes extracting `x` out of some variant or type `Foo`. -- `a ++ b` denotes concatenating two byte lists `a` and `b`. -- `bsatn(A) = bsatn(B) | ... | bsatn(Z)` where `B` to `Z` are variants of `A` - means that `bsatn(A)` is defined as e.g., - `bsatn(B)`, `bsatn(C)`, .., `bsatn(Z)` depending on what variant of `A` it was. -- `[]` denotes the empty list of bytes. - -## Values - -### At a glance - -| Type | Description | -| ---------------- | ---------------------------------------------------------------- | -| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype.). | -| `SumValue` | A value whose type is a [`SumType`](#sumtype.). | -| `ProductValue` | A value whose type is a [`ProductType`](#producttype.). | -| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype.). | - -### `AlgebraicValue` - -The BSATN encoding of an `AlgebraicValue` defers to the encoding of each variant: - -```fsharp -bsatn(AlgebraicValue) = bsatn(SumValue) | bsatn(ProductValue) | bsatn(BuiltinValue) -``` - -### `SumValue` - -An instance of a [`SumType`](#sumtype.). -`SumValue`s are binary-encoded as `bsatn(tag) ++ bsatn(variant_data)` -where `tag: u8` is an index into the [`SumType.variants`](#sumtype.) -array of the value's [`SumType`](#sumtype.), -and where `variant_data` is the data of the variant. -For variants holding no data, i.e., of some zero sized type, -`bsatn(variant_data) = []`. - -### `ProductValue` - -An instance of a [`ProductType`](#producttype.). -`ProductValue`s are binary encoded as: - -```fsharp -bsatn(elems) = bsatn(elem_0) ++ .. ++ bsatn(elem_n) -``` - -Field names are not encoded. - -### `BuiltinValue` - -An instance of a [`BuiltinType`](#builtintype.). -The BSATN encoding of `BuiltinValue`s defers to the encoding of each variant: - -```fsharp -bsatn(BuiltinValue) - = bsatn(Bool) - | bsatn(U8) | bsatn(U16) | bsatn(U32) | bsatn(U64) | bsatn(U128) - | bsatn(I8) | bsatn(I16) | bsatn(I32) | bsatn(I64) | bsatn(I128) - | bsatn(F32) | bsatn(F64) - | bsatn(String) - | bsatn(Array) - | bsatn(Map) - -bsatn(Bool(b)) = bsatn(b as u8) -bsatn(U8(x)) = [x] -bsatn(U16(x: u16)) = to_little_endian_bytes(x) -bsatn(U32(x: u32)) = to_little_endian_bytes(x) -bsatn(U64(x: u64)) = to_little_endian_bytes(x) -bsatn(U128(x: u128)) = to_little_endian_bytes(x) -bsatn(I8(x: i8)) = to_little_endian_bytes(x) -bsatn(I16(x: i16)) = to_little_endian_bytes(x) -bsatn(I32(x: i32)) = to_little_endian_bytes(x) -bsatn(I64(x: i64)) = to_little_endian_bytes(x) -bsatn(I128(x: i128)) = to_little_endian_bytes(x) -bsatn(F32(x: f32)) = bsatn(f32_to_raw_bits(x)) // lossless conversion -bsatn(F64(x: f64)) = bsatn(f64_to_raw_bits(x)) // lossless conversion -bsatn(String(s)) = bsatn(len(s) as u32) ++ bsatn(bytes(s)) -bsatn(Array(a)) = bsatn(len(a) as u32) - ++ bsatn(normalize(a)_0) ++ .. ++ bsatn(normalize(a)_n) -bsatn(Map(map)) = bsatn(len(m) as u32) - ++ bsatn(key(map_0)) ++ bsatn(value(map_0)) - .. - ++ bsatn(key(map_n)) ++ bsatn(value(map_n)) -``` - -Where - -- `f32_to_raw_bits(x)` is the raw transmute of `x: f32` to `u32` -- `f64_to_raw_bits(x)` is the raw transmute of `x: f64` to `u64` -- `normalize(a)` for `a: ArrayValue` converts `a` to a list of `AlgebraicValue`s -- `key(map_i)` extracts the key of the `i`th entry of `map` -- `value(map_i)` extracts the value of the `i`th entry of `map` - -## Types - -All SATS types are BSATN-encoded by converting them to an `AlgebraicValue`, -then BSATN-encoding that meta-value. - -See [the SATN JSON Format](satn-reference-json-format.) -for more details of the conversion to meta values. -Note that these meta values are converted to BSATN and _not JSON_. diff --git a/Writerside2/topics/deploying/deploying_index.md b/Writerside2/topics/deploying/deploying_index.md deleted file mode 100644 index 658df48d..00000000 --- a/Writerside2/topics/deploying/deploying_index.md +++ /dev/null @@ -1,48 +0,0 @@ -# Deploying Overview - -SpacetimeDB supports both hosted and self-hosted publishing in multiple ways. Below, we will: - -1. Generally introduce Identities. -1. Generally introduce Servers. -1Choose to proceed with either a [Hosted](hosted1.md) or [Self-Hosted](self-hosted1.md) deployment. - -💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. - -## About Identities - -An `Identity` is a hash attached to a `Nickname` and `Email`, allowing you to manage your app (such as `Publishing` your app). - -Each `Identity` is bound to one, single `Server`: Unlike GitHub, for example, you would require one identity per server. - -By default, there are no identities created. While the next tutorial will go more in-depth, you may create a new one via CLI: -```bash -spacetime identity new --name {Nickname} --email {Email} -``` - -See the verbose [overview identity explanation](https://spacetimedb.com/docs#identities), [API reference](identity1.md) or CLI help (below) for further managing `Identities`: -```bash -spacetime identity --help -``` - -## About Servers - -You `publish` your app to a target `Server` database: While we recommend to **host** your SpacetimeDB app with us for simplicity and scalability, you may also **self-host** (such as locally). - -By default, there are already two default servers added ([testnet](hosted1.md) and [local](self-hosted1.md)). While the next tutorial will go more in-depth, you may list your servers via CLI: -```bash -spacetime server list -``` - -See the [API reference](database1.md) or CLI help (below) for further managing `Servers`: -```bash -spacetime server --help -``` - ---- - -## Deploying via CLI - -Choose a server for your hosting tutorial path to set a server as default, create an identity, and deploy (`publish`) your app: - -1. [testnet](hosted1.md) (hosted) -2. [local](self-hosted1.md) (self-hosted) diff --git a/Writerside2/topics/deploying/hosted.md b/Writerside2/topics/deploying/hosted.md deleted file mode 100644 index 187eec4c..00000000 --- a/Writerside2/topics/deploying/hosted.md +++ /dev/null @@ -1,74 +0,0 @@ -# Deploying - Hosted - -This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. Via CLI, we will then: - -1. Ensure our hosted server named `testnet` exists as the default. -1. Create an `Identity`. -1. `Publish` your app. - -💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI and that you already have `testnet` server added (exists by default). If you accidentally removed `testnet`, add it back via CLI: - -```bash -spacetime server add "https://testnet.spacetimedb.com" testnet -``` - -## SpacetimeDB Cloud (Hosted) Deployment - -Currently, for hosted deployment, only the `testnet` server is available for SpacetimeDB cloud, which is subject to wipes. - -📢 Stay tuned (such as [via Discord](https://discord.com/invite/SpacetimeDB)) for `mainnet` coming soon! - -## Set the Server Default - -To make CLI commands easier so that we don't need to keep specifying `testnet` as the target server, let's set it as default: - -```bash -spacetime server set-default testnet -``` - -## Creating an Identity - -By default, there are no identities created. Let's create a new one via CLI: -```bash -spacetime identity new --name {Nickname} --email {Email} -``` - -💡If you already created an identity but forgot to attach an email, add it via CLI: -```bash -spacetime identity set-email {Email} -``` - -## Create and Publish a Module - -Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: - -```bash -cd ~ -spacetime init --lang rust HelloSpacetimeDB -cd HelloSpacetimeDB -spacetime publish HelloSpacetimeDB -``` - -## Hosted Web Dashboard - -By earlier associating an email with your CLI identity, you can now view your published modules on the web dashboard. For multiple identities, first list them and copy the hash you want to use: - -```bash -spacetime identity list -``` - -1. Open the SpacetimeDB [login page](https://spacetimedb.com/login) using the same email above. -1. Choose your identity from the dropdown menu. - - \[For multiple identities\] `CTRL+F` to highlight the correct identity you copied earlier. -1. Check your email for a validation link. - -You should now be able to see your published modules on the web dashboard! - ---- - -## Summary - -- We ensured the hosted `testnet` server existed, then set it as the default. -- We added an `identity` to bind with our hosted `testnet` server, ensuring it contained both a Nickname and Email. -- We then logged in the web dashboard via an email `one-time password (OTP)` and were then able to view our published apps. -- With SpacetimeDB Cloud, you benefit from automatic scaling, robust security, and the convenience of not having to manage the hosting environment. diff --git a/Writerside2/topics/deploying/self-hosted.md b/Writerside2/topics/deploying/self-hosted.md deleted file mode 100644 index 9c47282f..00000000 --- a/Writerside2/topics/deploying/self-hosted.md +++ /dev/null @@ -1,60 +0,0 @@ -# Deploying - Self-Hosted - -This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI. Via CLI, we will then: - -1. Ensure our localhost server named `local` exists as the default. -1. Start our localhost server in a separate terminal window. -1. Create an `Identity` with at least a Nickname. -1. `Publish` your app. - -💡 This tutorial assumes that you have already [installed](install.) the SpacetimeDB CLI and that you already have `local` server added (exists by default). If you accidentally removed `local`, add it back via CLI with the `--no-fingerprint` flag (since our server is not yet running): - -```bash -spacetime server add "http://127.0.0.1:3000" local --no-fingerprint -``` - -## Set the Server Default - -To make CLI commands easier so that we don't need to keep specifying `local` as the target server, let's set it as default: - -```bash -spacetime server set-default local -``` - -## Start the Local Server - -In a **separate** terminal window, start the local listen server in the foreground: -```bash -spacetime start -``` - -## Creating an Identity - -By default, there are no identities created. Let's create a new one via CLI: -```bash -spacetime identity new --name {Nickname} -``` - -💡We could optionally add `--email {Email}` to the above command, but is currently unnecessary for local deployment since there's no web dashboard. If you already created an identity but forgot to attach a Nickname, add it via CLI to easier identify your modules: -```bash -spacetime identity set-name {Nickname} -``` - -## Create and Publish a Module - -Let's create a vanilla Rust module called `HelloSpacetimeBD` from our home dir, then publish it "as-is". For Windows users, use `PowerShell`: - -```bash -cd ~ -spacetime init --lang rust HelloSpacetimeDB -cd HelloSpacetimeDB -spacetime publish HelloSpacetimeDB -``` - ---- - -## Summary - -- We ensured the self-hosted `local` server existed, then set it as the default. -- We then opened a separate terminal to run the self-hosted `local` server in the foreground. -- We added an `identity` to bind with our self-hosted `local` server set to default, ensuring it contained a Nickname. diff --git a/Writerside2/topics/getting-started.md b/Writerside2/topics/getting-started.md deleted file mode 100644 index 31e2fc90..00000000 --- a/Writerside2/topics/getting-started.md +++ /dev/null @@ -1,34 +0,0 @@ -# Getting Started - -To develop SpacetimeDB applications locally, you will need to run the Standalone version of the server. - -1. [Install](install.) the SpacetimeDB CLI (Command Line Interface) -2. Run the start command: - -```bash -spacetime start -``` - -The server listens on port `3000` by default, customized via `--listen-addr`. - -💡 Standalone mode will run in the foreground. -⚠️ SSL is not supported in standalone mode. - -## What's Next? - -You are ready to start developing SpacetimeDB modules. See below for a quickstart guide for both client and server (module) languages/frameworks. - -### Server (Module) - -- [Rust](quickstart.) -- [C#](quickstart1.) - -⚡**Note:** Rust is [roughly 2x faster](https://faun.dev/c/links/faun/c-vs-rust-vs-go-a-performance-benchmarking-in-kubernetes/) than C# - -### Client - -- [Rust](quickstart2.) -- [C# (Standalone)](quickstart3.) -- [C# (Unity)](part-1.) -- [Typescript](quickstart4.) -- [Python](quickstart5.) \ No newline at end of file diff --git a/Writerside2/topics/http/database.md b/Writerside2/topics/http/database.md deleted file mode 100644 index 0e7fbe89..00000000 --- a/Writerside2/topics/http/database.md +++ /dev/null @@ -1,589 +0,0 @@ -# `/database` HTTP API - -The HTTP endpoints in `/database` allow clients to interact with Spacetime databases in a variety of ways, including retrieving information, creating and deleting databases, invoking reducers and evaluating SQL queries. - -## At a glance - -| Route | Description | -| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -| [`/database/dns/:name GET`](#databasednsname-get.) | Look up a database's address by its name. | -| [`/database/reverse_dns/:address GET`](#databasereverse_dnsaddress-get.) | Look up a database's name by its address. | -| [`/database/set_name GET`](#databaseset_name-get.) | Set a database's name, given its address. | -| [`/database/ping GET`](#databaseping-get.) | No-op. Used to determine whether a client can connect. | -| [`/database/register_tld GET`](#databaseregister_tld-get.) | Register a top-level domain. | -| [`/database/request_recovery_code GET`](#databaserequest_recovery_code-get.) | Request a recovery code to the email associated with an identity. | -| [`/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](ws.). | -| [`/database/call/:name_or_address/:reducer POST`](#databasecallname_or_addressreducer-post.) | Invoke a reducer in a database. | -| [`/database/schema/:name_or_address GET`](#databaseschemaname_or_address-get.) | Get the schema for a database. | -| [`/database/schema/:name_or_address/:entity_type/:entity GET`](#databaseschemaname_or_addressentity_typeentity-get.) | Get a schema for a particular table or reducer. | -| [`/database/info/:name_or_address GET`](#databaseinfoname_or_address-get.) | Get a JSON description of a database. | -| [`/database/logs/:name_or_address GET`](#databaselogsname_or_address-get.) | Retrieve logs from a database. | -| [`/database/sql/:name_or_address POST`](#databasesqlname_or_address-post.) | Run a SQL query against a database. | - -## `/database/dns/:name GET` - -Look up a database's address by its name. - -Accessible through the CLI as `spacetime dns lookup `. - -#### Parameters - -| Name | Value | -| ------- | ------------------------- | -| `:name` | The name of the database. | - -#### Returns - -If a database with that name exists, returns JSON in the form: - -```typescript -{ "Success": { - "domain": string, - "address": string -} } -``` - -If no database with that name exists, returns JSON in the form: - -```typescript -{ "Failure": { - "domain": string -} } -``` - -## `/database/reverse_dns/:address GET` - -Look up a database's name by its address. - -Accessible through the CLI as `spacetime dns reverse-lookup
`. - -#### Parameters - -| Name | Value | -| ---------- | ---------------------------- | -| `:address` | The address of the database. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ "names": array } -``` - -where `` is a JSON array of strings, each of which is a name which refers to the database. - -## `/database/set_name GET` - -Set the name associated with a database. - -Accessible through the CLI as `spacetime dns set-name
`. - -#### Query Parameters - -| Name | Value | -| -------------- | ------------------------------------------------------------------------- | -| `address` | The address of the database to be named. | -| `domain` | The name to register. | -| `register_tld` | A boolean; whether to register the name as a TLD. Should usually be true. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -If the name was successfully set, returns JSON in the form: - -```typescript -{ "Success": { - "domain": string, - "address": string -} } -``` - -If the top-level domain is not registered, and `register_tld` was not specified, returns JSON in the form: - -```typescript -{ "TldNotRegistered": { - "domain": string -} } -``` - -If the top-level domain is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form: - -```typescript -{ "PermissionDenied": { - "domain": string -} } -``` - -> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. - -## `/database/ping GET` - -Does nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB. - -## `/database/register_tld GET` - -Register a new Spacetime top-level domain. A TLD is the part of a database name before the first `/`. For example, in the name `tyler/bitcraft`, the TLD is `tyler`. Each top-level domain is owned by at most one identity, and only the owner can publish databases with that TLD. - -> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. - -Accessible through the CLI as `spacetime dns register-tld `. - -#### Query Parameters - -| Name | Value | -| ----- | -------------------------------------- | -| `tld` | New top-level domain name to register. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -If the domain is successfully registered, returns JSON in the form: - -```typescript -{ "Success": { - "domain": string -} } -``` - -If the domain is already registered to the caller, returns JSON in the form: - -```typescript -{ "AlreadyRegistered": { - "domain": string -} } -``` - -If the domain is already registered to another identity, returns JSON in the form: - -```typescript -{ "Unauthorized": { - "domain": string -} } -``` - -## `/database/request_recovery_code GET` - -Request a recovery code or link via email, in order to recover the token associated with an identity. - -Accessible through the CLI as `spacetime identity recover `. - -#### Query Parameters - -| 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`](identity#identity-post.) or afterwards via [`/identity/:identity/set-email`](identity#identityidentityset_email-post.). | -| `link` | A boolean; whether to send a clickable link rather than a recovery code. | - -## `/database/confirm_recovery_code GET` - -Confirm a recovery code received via email following a [`/database/request_recovery_code GET`](#-database-request_recovery_code-get.) request, and retrieve the identity's token. - -Accessible through the CLI as `spacetime identity recover `. - -#### Query Parameters - -| Name | Value | -| ---------- | --------------------------------------------- | -| `identity` | The identity whose token should be recovered. | -| `email` | The email which received the recovery code. | -| `code` | The recovery code received via email. | - -On success, returns JSON in the form: - -```typescript -{ - "identity": string, - "token": string -} -``` - -## `/database/publish POST` - -Publish a database. - -Accessible through the CLI as `spacetime publish`. - -#### Query Parameters - -| Name | Value | -| ----------------- | ------------------------------------------------------------------------------------------------ | -| `host_type` | Optional; a SpacetimeDB module host type. Currently, only `"wasmer"` is supported. | -| `clear` | A boolean; whether to clear any existing data when updating an existing database. | -| `name_or_address` | The name of the database to publish or update, or the address of an existing database to update. | -| `register_tld` | A boolean; whether to register the database's top-level domain. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Data - -A WebAssembly module in the [binary format](https://webassembly.github.io/spec/core/binary/index.html). - -#### Returns - -If the database was successfully published, returns JSON in the form: - -```typescript -{ "Success": { - "domain": null | string, - "address": string, - "op": "created" | "updated" -} } -``` - -If the top-level domain for the requested name is not registered, returns JSON in the form: - -```typescript -{ "TldNotRegistered": { - "domain": string -} } -``` - -If the top-level domain for the requested name is registered, but the identity provided in the `Authorization` header does not have permission to insert into it, returns JSON in the form: - -```typescript -{ "PermissionDenied": { - "domain": string -} } -``` - -> Spacetime top-level domains are an upcoming feature, and are not fully implemented in SpacetimeDB 0.6. For now, database names should not contain slashes. - -## `/database/delete/:address POST` - -Delete a database. - -Accessible through the CLI as `spacetime delete
`. - -#### Parameters - -| Name | Address | -| ---------- | ---------------------------- | -| `:address` | The address of the database. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -## `/database/subscribe/:name_or_address GET` - -Begin a [WebSocket connection](ws.) with a database. - -#### Parameters - -| Name | Value | -| ------------------ | ---------------------------- | -| `:name_or_address` | The address of the database. | - -#### Required Headers - -For more information about WebSocket headers, see [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). - -| Name | Value | -| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `Sec-WebSocket-Protocol` | [`v1.bin.spacetimedb`](ws#binary-protocol.) or [`v1.text.spacetimedb`](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. | - -#### Optional Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -## `/database/call/:name_or_address/:reducer POST` - -Invoke a reducer in a database. - -#### Parameters - -| Name | Value | -| ------------------ | ------------------------------------ | -| `:name_or_address` | The name or address of the database. | -| `:reducer` | The name of the reducer. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Data - -A JSON array of arguments to the reducer. - -## `/database/schema/:name_or_address GET` - -Get a schema for a database. - -Accessible through the CLI as `spacetime describe `. - -#### Parameters - -| Name | Value | -| ------------------ | ------------------------------------ | -| `:name_or_address` | The name or address of the database. | - -#### Query Parameters - -| Name | Value | -| -------- | ----------------------------------------------------------- | -| `expand` | A boolean; whether to include full schemas for each entity. | - -#### Returns - -Returns a JSON object with two properties, `"entities"` and `"typespace"`. For example, on the default module generated by `spacetime init` with `expand=true`, returns: - -```typescript -{ - "entities": { - "Person": { - "arity": 1, - "schema": { - "elements": [ - { - "algebraic_type": { - "Builtin": { - "String": [] - } - }, - "name": { - "some": "name" - } - } - ] - }, - "type": "table" - }, - "__init__": { - "arity": 0, - "schema": { - "elements": [], - "name": "__init__" - }, - "type": "reducer" - }, - "add": { - "arity": 1, - "schema": { - "elements": [ - { - "algebraic_type": { - "Builtin": { - "String": [] - } - }, - "name": { - "some": "name" - } - } - ], - "name": "add" - }, - "type": "reducer" - }, - "say_hello": { - "arity": 0, - "schema": { - "elements": [], - "name": "say_hello" - }, - "type": "reducer" - } - }, - "typespace": [ - { - "Product": { - "elements": [ - { - "algebraic_type": { - "Builtin": { - "String": [] - } - }, - "name": { - "some": "name" - } - } - ] - } - } - ] -} -``` - -The `"entities"` will be an object whose keys are table and reducer names, and whose values are objects of the form: - -```typescript -{ - "arity": number, - "type": "table" | "reducer", - "schema"?: ProductType -} -``` - -| Entity field | Value | -| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `arity` | For tables, the number of colums; for reducers, the number of arguments. | -| `type` | For tables, `"table"`; for reducers, `"reducer"`. | -| `schema` | A [JSON-encoded `ProductType`](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](satn.) referenced by the module. This can be used to resolve `Ref` types within the schema; the type `{ "Ref": n }` refers to `response["typespace"][n]`. - -## `/database/schema/:name_or_address/:entity_type/:entity GET` - -Get a schema for a particular table or reducer in a database. - -Accessible through the CLI as `spacetime describe `. - -#### Parameters - -| Name | Value | -| ------------------ | ---------------------------------------------------------------- | -| `:name_or_address` | The name or address of the database. | -| `:entity_type` | `reducer` to describe a reducer, or `table` to describe a table. | -| `:entity` | The name of the reducer or table. | - -#### Query Parameters - -| Name | Value | -| -------- | ------------------------------------------------------------- | -| `expand` | A boolean; whether to include the full schema for the entity. | - -#### Returns - -Returns a single entity in the same format as in the `"entities"` returned by [the `/database/schema/:name_or_address GET` endpoint](#databaseschemaname_or_address-get.): - -```typescript -{ - "arity": number, - "type": "table" | "reducer", - "schema"?: ProductType, -} -``` - -| Field | Value | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `arity` | For tables, the number of colums; for reducers, the number of arguments. | -| `type` | For tables, `"table"`; for reducers, `"reducer"`. | -| `schema` | A [JSON-encoded `ProductType`](satn.); for tables, the table schema; for reducers, the argument schema. Only present if `expand` is supplied and true. | - -## `/database/info/:name_or_address GET` - -Get a database's address, owner identity, host type, number of replicas and a hash of its WASM module. - -#### Parameters - -| Name | Value | -| ------------------ | ------------------------------------ | -| `:name_or_address` | The name or address of the database. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "address": string, - "identity": string, - "host_type": "wasmer", - "num_replicas": number, - "program_bytes_address": string -} -``` - -| Field | Type | Meaning | -| ------------------------- | ------ | ----------------------------------------------------------- | -| `"address"` | String | The address of the database. | -| `"identity"` | String | The Spacetime identity of the database's owner. | -| `"host_type"` | String | The module host type; currently always `"wasmer"`. | -| `"num_replicas"` | Number | The number of replicas of the database. Currently always 1. | -| `"program_bytes_address"` | String | Hash of the WASM module for the database. | - -## `/database/logs/:name_or_address GET` - -Retrieve logs from a database. - -Accessible through the CLI as `spacetime logs `. - -#### Parameters - -| Name | Value | -| ------------------ | ------------------------------------ | -| `:name_or_address` | The name or address of the database. | - -#### Query Parameters - -| Name | Value | -| ----------- | --------------------------------------------------------------- | -| `num_lines` | Number of most-recent log lines to retrieve. | -| `follow` | A boolean; whether to continue receiving new logs via a stream. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -Text, or streaming text if `follow` is supplied, containing log lines. - -## `/database/sql/:name_or_address POST` - -Run a SQL query against a database. - -Accessible through the CLI as `spacetime sql `. - -#### Parameters - -| Name | Value | -| ------------------ | --------------------------------------------- | -| `:name_or_address` | The name or address of the database to query. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Data - -SQL queries, separated by `;`. - -#### Returns - -Returns a JSON array of statement results, each of which takes the form: - -```typescript -{ - "schema": ProductType, - "rows": array -} -``` - -The `schema` will be a [JSON-encoded `ProductType`](satn.) describing the type of the returned rows. - -The `rows` will be an array of [JSON-encoded `ProductValue`s](satn.), each of which conforms to the `schema`. diff --git a/Writerside2/topics/http/energy.md b/Writerside2/topics/http/energy.md deleted file mode 100644 index fabecc30..00000000 --- a/Writerside2/topics/http/energy.md +++ /dev/null @@ -1,76 +0,0 @@ -# `/energy` HTTP API - -The HTTP endpoints in `/energy` allow clients to query identities' energy balances. Spacetime databases expend energy from their owners' balances while executing reducers. - -## At a glance - -| Route | Description | -| ------------------------------------------------ | --------------------------------------------------------- | -| [`/energy/:identity GET`](#energyidentity-get.) | Get the remaining energy balance for the user `identity`. | -| [`/energy/:identity POST`](#energyidentity-post.) | Set the energy balance for the user `identity`. | - -## `/energy/:identity GET` - -Get the energy balance of an identity. - -Accessible through the CLI as `spacetime energy status `. - -#### Parameters - -| Name | Value | -| ----------- | ----------------------- | -| `:identity` | The Spacetime identity. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "balance": string -} -``` - -| Field | Value | -| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `balance` | The identity's energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. | - -## `/energy/:identity POST` - -Set the energy balance for an identity. - -Note that in the SpacetimeDB 0.6 Testnet, this endpoint always returns code 401, `UNAUTHORIZED`. Testnet energy balances cannot be refilled. - -Accessible through the CLI as `spacetime energy set-balance `. - -#### Parameters - -| Name | Value | -| ----------- | ----------------------- | -| `:identity` | The Spacetime identity. | - -#### Query Parameters - -| Name | Value | -| --------- | ------------------------------------------ | -| `balance` | A decimal integer; the new balance to set. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "balance": number -} -``` - -| Field | Value | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `balance` | The identity's new energy balance, as a decimal integer. Note that energy balances may be negative, and will frequently be too large to store in a 64-bit integer. | diff --git a/Writerside2/topics/http/http_index.md b/Writerside2/topics/http/http_index.md deleted file mode 100644 index e9a3d21e..00000000 --- a/Writerside2/topics/http/http_index.md +++ /dev/null @@ -1,51 +0,0 @@ -# SpacetimeDB HTTP Authorization - -Rather than a password, each Spacetime identity is associated with a private token. These tokens are generated by SpacetimeDB when the corresponding identity is created, and cannot be changed. - -> Do not share your SpacetimeDB token with anyone, ever. - -### Generating identities and tokens - -Clients can request a new identity and token via [the `/identity POST` HTTP endpoint](identity#identity-post.). - -Alternately, a new identity and token will be generated during an anonymous connection via the [WebSocket API](ws.), and passed to the client as [an `IdentityToken` message](ws#identitytoken.). - -### Encoding `Authorization` headers - -Many SpacetimeDB HTTP endpoints either require or optionally accept a token in the `Authorization` header. SpacetimeDB authorization headers use `Basic` authorization with the username `token` and the token as the password. Because Spacetime tokens are not passwords, and SpacetimeDB Cloud uses TLS, usual security concerns about HTTP `Basic` authorization do not apply. - -To construct an appropriate `Authorization` header value for a `token`: - -1. Prepend the string `token:`. -2. Base64-encode. -3. Prepend the string `Basic `. - -#### Python - -```python -def auth_header_value(token): - username_and_password = f"token:{token}".encode("utf-8") - base64_encoded = base64.b64encode(username_and_password).decode("utf-8") - return f"Basic {base64_encoded}" -``` - -#### Rust - -```rust -fn auth_header_value(token: &str) -> String { - let username_and_password = format!("token:{}", token); - let base64_encoded = base64::prelude::BASE64_STANDARD.encode(username_and_password); - format!("Basic {}", encoded) -} -``` - -#### C# - -```csharp -public string AuthHeaderValue(string token) -{ - var username_and_password = Encoding.UTF8.GetBytes($"token:{auth}"); - var base64_encoded = Convert.ToBase64String(username_and_password); - return "Basic " + base64_encoded; -} -``` diff --git a/Writerside2/topics/http/identity.md b/Writerside2/topics/http/identity.md deleted file mode 100644 index 544d5d11..00000000 --- a/Writerside2/topics/http/identity.md +++ /dev/null @@ -1,160 +0,0 @@ -# `/identity` HTTP API - -The HTTP endpoints in `/identity` allow clients to generate and manage Spacetime public identities and private tokens. - -## At a glance - -| Route | Description | -| ----------------------------------------------------------------------- | ------------------------------------------------------------------ | -| [`/identity GET`](#identity-get.) | Look up an identity by email. | -| [`/identity POST`](#identity-post.) | Generate a new identity and token. | -| [`/identity/websocket_token POST`](#identitywebsocket_token-post.) | Generate a short-lived access token for use in untrusted contexts. | -| [`/identity/:identity/set-email POST`](#identityidentityset-email-post.) | Set the email for an identity. | -| [`/identity/:identity/databases GET`](#identityidentitydatabases-get.) | List databases owned by an identity. | -| [`/identity/:identity/verify GET`](#identityidentityverify-get.) | Verify an identity and token. | - -## `/identity GET` - -Look up Spacetime identities associated with an email. - -Accessible through the CLI as `spacetime identity find `. - -#### Query Parameters - -| Name | Value | -| ------- | ------------------------------- | -| `email` | An email address to search for. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "identities": [ - { - "identity": string, - "email": string - } - ] -} -``` - -The `identities` value is an array of zero or more objects, each of which has an `identity` and an `email`. Each `email` will be the same as the email passed as a query parameter. - -## `/identity POST` - -Create a new identity. - -Accessible through the CLI as `spacetime identity new`. - -#### Query Parameters - -| Name | Value | -| ------- | ----------------------------------------------------------------------------------------------------------------------- | -| `email` | An email address to associate with the new identity. If unsupplied, the new identity will not have an associated email. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "identity": string, - "token": string -} -``` - -## `/identity/websocket_token POST` - -Generate a short-lived access token which can be used in untrusted contexts, e.g. embedded in URLs. - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "token": string -} -``` - -The `token` value is a short-lived [JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519). - -## `/identity/:identity/set-email POST` - -Associate an email with a Spacetime identity. - -Accessible through the CLI as `spacetime identity set-email `. - -#### Parameters - -| Name | Value | -| ----------- | ----------------------------------------- | -| `:identity` | The identity to associate with the email. | - -#### Query Parameters - -| Name | Value | -| ------- | ----------------- | -| `email` | An email address. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -## `/identity/:identity/databases GET` - -List all databases owned by an identity. - -#### Parameters - -| Name | Value | -| ----------- | --------------------- | -| `:identity` | A Spacetime identity. | - -#### Returns - -Returns JSON in the form: - -```typescript -{ - "addresses": array -} -``` - -The `addresses` value is an array of zero or more strings, each of which is the address of a database owned by the identity passed as a parameter. - -## `/identity/:identity/verify GET` - -Verify the validity of an identity/token pair. - -#### Parameters - -| Name | Value | -| ----------- | ----------------------- | -| `:identity` | The identity to verify. | - -#### Required Headers - -| Name | Value | -| --------------- | ------------------------------------------------------------------------------------------- | -| `Authorization` | A Spacetime token [encoded as Basic authorization](http.). | - -#### Returns - -Returns no data. - -If the token is valid and matches the identity, returns `204 No Content`. - -If the token is valid but does not match the identity, returns `400 Bad Request`. - -If the token is invalid, or no `Authorization` header is included in the request, returns `401 Unauthorized`. diff --git a/Writerside2/topics/index.md b/Writerside2/topics/index.md deleted file mode 100644 index 8426e256..00000000 --- a/Writerside2/topics/index.md +++ /dev/null @@ -1,120 +0,0 @@ -# SpacetimeDB Documentation - -## Installation - -You can run SpacetimeDB as a standalone database server via the `spacetime` CLI tool. - -You can find the instructions to install the CLI tool for your platform [here](install.). - - - -To get started running your own standalone instance of SpacetimeDB check out our [Getting Started Guide](getting-started.). - - - -## What is SpacetimeDB? - -You can think of SpacetimeDB as a database that is also a server. - -It is a relational database system that lets you upload your application logic directly into the database by way of very fancy stored procedures called "modules". - -Instead of deploying a web or game server that sits in between your clients and your database, your clients connect directly to the database and execute your application logic inside the database itself. You can write all of your permission and authorization logic right inside your module just as you would in a normal server. - -This means that you can write your entire application in a single language, Rust, and deploy it as a single binary. No more microservices, no more containers, no more Kubernetes, no more Docker, no more VMs, no more DevOps, no more infrastructure, no more ops, no more servers. - -
- SpacetimeDB Architecture -
- SpacetimeDB application architecture - (elements in white are provided by SpacetimeDB) -
-
- -It's actually similar to the idea of smart contracts, except that SpacetimeDB is a database, has nothing to do with blockchain, and it's a lot faster than any smart contract system. - -So fast, in fact, that the entire backend our MMORPG [BitCraft Online](https://bitcraftonline.com) is just a SpacetimeDB module. We don't have any other servers or services running, which means that everything in the game, all of the chat messages, items, resources, terrain, and even the locations of the players are stored and processed by the database before being synchronized out to all of the clients in real-time. - -SpacetimeDB is optimized for maximum speed and minimum latency rather than batch processing or OLAP workloads. It is designed to be used for real-time applications like games, chat, and collaboration tools. - -This speed and latency is achieved by holding all of application state in memory, while persisting the data in a write-ahead-log (WAL) which is used to recover application state. - -## State Synchronization - -SpacetimeDB syncs client and server state for you so that you can just write your application as though you're accessing the database locally. No more messing with sockets for a week before actually writing your game. - -## Identities - -A SpacetimeDB `Identity` is a unique identifier that is used to authenticate and authorize access to the database. Importantly, while it represents who someone is, does NOT represent what they can do. Your application's logic will determine what a given identity is able to do by allowing or disallowing a transaction based on the caller's `Identity` along with any module-defined data and logic. - -SpacetimeDB associates each user with a 256-bit (32-byte) integer `Identity`. These identities are usually formatted as 64-digit hexadecimal strings. Identities are public information, and applications can use them to identify users. Identities are a global resource, so a user can use the same identity with multiple applications, so long as they're hosted by the same SpacetimeDB instance. - -Each identity has a corresponding authentication token. The authentication token is private, and should never be shared with anyone. Specifically, authentication tokens are [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519) signed by a secret unique to the SpacetimeDB instance. - -Additionally, each database has an owner `Identity`. Many database maintenance operations, like publishing a new version or evaluating arbitrary SQL queries, are restricted to only authenticated connections by the owner. - -SpacetimeDB provides tools in the CLI and the [client SDKs](sdks.) for managing credentials. - -## Addresses - -A SpacetimeDB `Address` is an opaque identifier for a database or a client connection. An `Address` is a 128-bit integer, usually formatted as a 32-character (16-byte) hexadecimal string. - -Each SpacetimeDB database has an `Address`, generated by the SpacetimeDB host, which can be used to connect to the database or to request information about it. Databases may also have human-readable names, which are mapped to addresses internally. - -Each client connection has an `Address`. These addresses are opaque, and do not correspond to any metadata about the client. They are notably not IP addresses or device identifiers. A client connection can be uniquely identified by its `(Identity, Address)` pair, but client addresses may not be globally unique; it is possible for multiple connections with the same `Address` but different identities to co-exist. SpacetimeDB modules should treat `Identity` as differentiating users, and `Address` as differentiating connections by the same user. - -## Language Support - -### Server-side Libraries - -Currently, Rust is the best-supported language for writing SpacetimeDB modules. Support for lots of other languages is in the works! - -- [Rust](rust.) - [(Quickstart)](quickstart.) -- [C#](c-sharp.) - [(Quickstart)](quickstart1.) -- Python (Coming soon) -- C# (Coming soon) -- Typescript (Coming soon) -- C++ (Planned) -- Lua (Planned) - -### Client-side SDKs - -- [Rust](rust1.) - [(Quickstart)](quickstart2.) -- [C#](c-sharp1.) - [(Quickstart)](quickstart3.) -- [TypeScript](typescript.) - [(Quickstart)](quickstart4.) -- [Python](python.) - [(Quickstart)](quickstart5.) -- C++ (Planned) -- Lua (Planned) - -### Unity - -SpacetimeDB was designed first and foremost as the backend for multiplayer Unity games. To learn more about using SpacetimeDB with Unity, jump on over to the [SpacetimeDB Unity Tutorial](part-1.). - -## FAQ - -1. What is SpacetimeDB? - It's a whole cloud platform within a database that's fast enough to run real-time games. - -1. How do I use SpacetimeDB? - Install the `spacetime` command line tool, choose your favorite language, import the SpacetimeDB library, write your application, compile it to WebAssembly, and upload it to the SpacetimeDB cloud platform. Once it's uploaded you can call functions directly on your application and subscribe to changes in application state. - -1. How do I get/install SpacetimeDB? - Just install our command line tool and then upload your application to the cloud. - -4. How do I create a new database with SpacetimeDB? - Follow our [Quick Start](getting-started.) guide! - -TL;DR in an empty directory, init and publish a barebones app named HelloWorld. - -```bash -spacetime init --lang=rust -spacetime publish HelloWorld -``` - -5. How do I create a Unity game with SpacetimeDB? - Follow our [Unity Project](unity-project.) guide! - -TL;DR after already initializing and publishing (see FAQ #5), generate the SDK: - -```bash -spacetime generate --out-dir --lang=csharp -``` diff --git a/Writerside2/topics/modules/c-sharp/c-sharp_index.md b/Writerside2/topics/modules/c-sharp/c-sharp_index.md deleted file mode 100644 index 31ebd1d4..00000000 --- a/Writerside2/topics/modules/c-sharp/c-sharp_index.md +++ /dev/null @@ -1,307 +0,0 @@ -# SpacetimeDB C# Modules - -You can use the [C# SpacetimeDB library](https://github.com/clockworklabs/SpacetimeDBLibCSharp) to write modules in C# which interact with the SpacetimeDB database. - -It uses [Roslyn incremental generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) to add extra static methods to types, tables and reducers marked with special attributes and registers them with the database runtime. - -## Example - -Let's start with a heavily commented version of the default example from the landing page: - -```csharp -// These imports bring into the scope common APIs you'll need to expose items from your module and to interact with the database runtime. -using SpacetimeDB.Module; -using static SpacetimeDB.Runtime; - -// Roslyn generators are statically generating extra code as-if they were part of the source tree, so, -// in order to inject new methods, types they operate on as well as their parents have to be marked as `partial`. -// -// We start with the top-level `Module` class for the module itself. -static partial class Module -{ - // `[SpacetimeDB.Table]` registers a struct or a class as a SpacetimeDB table. - // - // It generates methods to insert, filter, update, and delete rows of the given type in the table. - [SpacetimeDB.Table] - public partial struct Person - { - // `[SpacetimeDB.Column]` allows to specify column attributes / constraints such as - // "this field should be unique" or "this field should get automatically assigned auto-incremented value". - [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)] - public int Id; - public string Name; - public int Age; - } - - // `[SpacetimeDB.Reducer]` marks a static method as a SpacetimeDB reducer. - // - // Reducers are functions that can be invoked from the database runtime. - // They can't return values, but can throw errors that will be caught and reported back to the runtime. - [SpacetimeDB.Reducer] - public static void Add(string name, int age) - { - // We can skip (or explicitly set to zero) auto-incremented fields when creating new rows. - var person = new Person { Name = name, Age = age }; - - // `Insert()` method is auto-generated and will insert the given row into the table. - person.Insert(); - // After insertion, the auto-incremented fields will be populated with their actual values. - // - // `Log()` function is provided by the runtime and will print the message to the database log. - // It should be used instead of `Console.WriteLine()` or similar functions. - Log($"Inserted {person.Name} under #{person.Id}"); - } - - [SpacetimeDB.Reducer] - public static void SayHello() - { - // Each table type gets a static Iter() method that can be used to iterate over the entire table. - foreach (var person in Person.Iter()) - { - Log($"Hello, {person.Name}!"); - } - Log("Hello, World!"); - } -} -``` - -## API reference - -Now we'll get into details on all the APIs SpacetimeDB provides for writing modules in C#. - -### Logging - -First of all, logging as we're likely going to use it a lot for debugging and reporting errors. - -`SpacetimeDB.Runtime` provides a `Log` function that will print the given message to the database log, along with the source location and a log level it was provided. - -Supported log levels are provided by the `LogLevel` enum: - -```csharp -public enum LogLevel -{ - Error, - Warn, - Info, - Debug, - Trace, - Panic -} -``` - -If omitted, the log level will default to `Info`, so these two forms are equivalent: - -```csharp -Log("Hello, World!"); -Log("Hello, World!", LogLevel.Info); -``` - -### Supported types - -#### Built-in types - -The following types are supported out of the box and can be stored in the database tables directly or as part of more complex types: - -- `bool` -- `byte`, `sbyte` -- `short`, `ushort` -- `int`, `uint` -- `long`, `ulong` -- `float`, `double` -- `string` -- [`Int128`](https://learn.microsoft.com/en-us/dotnet/api/system.int128), [`UInt128`](https://learn.microsoft.com/en-us/dotnet/api/system.uint128) -- `T[]` - arrays of supported values. -- [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1) -- [`Dictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2) - -And a couple of special custom types: - -- `SpacetimeDB.SATS.Unit` - semantically equivalent to an empty struct, sometimes useful in generic contexts where C# doesn't permit `void`. -- `Identity` (`SpacetimeDB.Runtime.Identity`) - a unique identifier for each user; internally a byte blob but can be printed, hashed and compared for equality. -- `Address` (`SpacetimeDB.Runtime.Address`) - an identifier which disamgibuates connections by the same `Identity`; internally a byte blob but can be printed, hashed and compared for equality. - - -#### 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. - -Any `struct` or `class` marked with this attribute, as well as their respective parents, must be `partial`, as the code generator will add methods to them. - -```csharp -[SpacetimeDB.Type] -public partial struct Point -{ - public int x; - public int y; -} -``` - -`enum`s marked with this attribute must not use custom discriminants, as the runtime expects them to be always consecutive starting from zero. Unlike structs and classes, they don't use `partial` as C# doesn't allow to add methods to `enum`s. - -```csharp -[SpacetimeDB.Type] -public enum Color -{ - Red, - Green, - Blue, -} -``` - -#### Tagged enums - -SpacetimeDB has support for tagged enums which can be found in languages like Rust, but not C#. - -To bridge the gap, a special marker interface `SpacetimeDB.TaggedEnum` can be used on any `SpacetimeDB.Type`-marked `struct` or `class` to mark it as a SpacetimeDB tagged enum. It accepts a tuple of 2 or more named items and will generate methods to check which variant is currently active, as well as accessors for each variant. - -It is expected that you will use the `Is*` methods to check which variant is active before accessing the corresponding field, as the accessor will throw an exception on a state mismatch. - -```csharp -// Example declaration: -[SpacetimeDB.Type] -partial struct Option : SpacetimeDB.TaggedEnum<(T Some, Unit None)> { } - -// Usage: -var option = new Option { Some = 42 }; -if (option.IsSome) -{ - Log($"Value: {option.Some}"); -} -``` - -### Tables - -`[SpacetimeDB.Table]` attribute can be used on any `struct` or `class` to mark it as a SpacetimeDB table. It will register a table in the database with the given name and fields as well as will generate C# methods to insert, filter, update, and delete rows of the given type. - -It implies `[SpacetimeDB.Type]`, so you must not specify both attributes on the same type. - -```csharp -[SpacetimeDB.Table] -public partial struct Person -{ - [SpacetimeDB.Column(ColumnAttrs.Unique | ColumnAttrs.AutoInc)] - public int Id; - public string Name; - public int Age; -} -``` - -The example above will generate the following extra methods: - -```csharp -public partial struct Person -{ - // Inserts current instance as a new row into the table. - public void Insert(); - - // Returns an iterator over all rows in the table, e.g.: - // `for (var person in Person.Iter()) { ... }` - public static IEnumerable Iter(); - - // Returns an iterator over all rows in the table that match the given filter, e.g.: - // `for (var person in Person.Query(p => p.Age >= 18)) { ... }` - public static IEnumerable Query(Expression> filter); - - // Generated for each column: - - // Returns an iterator over all rows in the table that have the given value in the `Name` column. - public static IEnumerable FilterByName(string name); - public static IEnumerable FilterByAge(int age); - - // Generated for each unique column: - - // 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); -} -``` - -#### Column attributes - -Attribute `[SpacetimeDB.Column]` can be used on any field of a `SpacetimeDB.Table`-marked `struct` or `class` to customize column attributes as seen above. - -The supported column attributes are: - -- `ColumnAttrs.AutoInc` - this column should be auto-incremented. -- `ColumnAttrs.Unique` - this column should be unique. -- `ColumnAttrs.PrimaryKey` - this column should be a primary key, it implies `ColumnAttrs.Unique` but also allows clients to subscribe to updates via `OnUpdate` which will use this field to match the old and the new version of the row with each other. - -These attributes are bitflags and can be combined together, but you can also use some predefined shortcut aliases: - -- `ColumnAttrs.Identity` - same as `ColumnAttrs.Unique | ColumnAttrs.AutoInc`. -- `ColumnAttrs.PrimaryKeyAuto` - same as `ColumnAttrs.PrimaryKey | ColumnAttrs.AutoInc`. - -### Reducers - -Attribute `[SpacetimeDB.Reducer]` can be used on any `static void` method to register it as a SpacetimeDB reducer. The method must accept only supported types as arguments. If it throws an exception, those will be caught and reported back to the database runtime. - -```csharp -[SpacetimeDB.Reducer] -public static void Add(string name, int age) -{ - var person = new Person { Name = name, Age = age }; - person.Insert(); - Log($"Inserted {person.Name} under #{person.Id}"); -} -``` - -If a reducer has an argument with a type `DbEventArgs` (`SpacetimeDB.Runtime.DbEventArgs`), it will be provided with event details such as the sender identity (`SpacetimeDB.Runtime.Identity`), sender address (`SpacetimeDB.Runtime.Address?`) and the time (`DateTimeOffset`) of the invocation: - -```csharp -[SpacetimeDB.Reducer] -public static void PrintInfo(DbEventArgs e) -{ - Log($"Sender identity: {e.Sender}"); - Log($"Sender address: {e.Address}"); - Log($"Time: {e.Time}"); -} -``` - -`[SpacetimeDB.Reducer]` also generates a function to schedule the given reducer in the future. - -Since it's not possible to generate extension methods on existing methods, the codegen will instead add a `Schedule`-prefixed method colocated in the same namespace as the original method instead. The generated method will accept `DateTimeOffset` argument for the time when the reducer should be invoked, followed by all the arguments of the reducer itself, except those that have type `DbEventArgs`. - -```csharp -// Example reducer: -[SpacetimeDB.Reducer] -public static void Add(string name, int age) { ... } - -// Auto-generated by the codegen: -public static void ScheduleAdd(DateTimeOffset time, string name, int age) { ... } - -// Usage from another reducer: -[SpacetimeDB.Reducer] -public static void AddIn5Minutes(DbEventArgs e, string name, int age) -{ - // Note that we're using `e.Time` instead of `DateTimeOffset.Now` which is not allowed in modules. - var scheduleToken = ScheduleAdd(e.Time.AddMinutes(5), name, age); - - // We can cancel the scheduled reducer by calling `Cancel()` on the returned token. - scheduleToken.Cancel(); -} -``` - -#### Special reducers - -These are two special kinds of reducers that can be used to respond to module lifecycle events. They're stored in the `SpacetimeDB.Module.ReducerKind` class and can be used as an argument to the `[SpacetimeDB.Reducer]` attribute: - -- `ReducerKind.Init` - this reducer will be invoked when the module is first published. -- `ReducerKind.Update` - this reducer will be invoked when the module is updated. -- `ReducerKind.Connect` - this reducer will be invoked when a client connects to the database. -- `ReducerKind.Disconnect` - this reducer will be invoked when a client disconnects from the database. - - -Example: - -```csharp -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void Init() -{ - Log("...and we're live!"); -} -``` diff --git a/Writerside2/topics/modules/c-sharp/quickstart.md b/Writerside2/topics/modules/c-sharp/quickstart.md deleted file mode 100644 index fedd7851..00000000 --- a/Writerside2/topics/modules/c-sharp/quickstart.md +++ /dev/null @@ -1,312 +0,0 @@ -# C# Module Quickstart - -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. - -Each SpacetimeDB module defines a set of tables and a set of reducers. - -Each table is defined as a C# `class` annotated with `[SpacetimeDB.Table]`, where an instance represents a row, and each field represents a column. - -A reducer is a function which traverses and updates the database. Each reducer call runs in its own transaction, and its updates to the database are only committed if the reducer returns successfully. In C#, reducers are defined as functions annotated with `[SpacetimeDB.Reducer]`. If an exception is thrown, the reducer call fails, the database is not updated, and a failed message is reported to the client. - -## Install SpacetimeDB - -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. - -## Install .NET 8 - -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 -``` - -.NET 8.0 is the earliest to have the `wasi-experimental` workload that we rely on, but requires manual activation: - -```bash -dotnet workload install wasi-experimental -``` - -## Project structure - -Create and enter a directory `quickstart-chat`: - -```bash -mkdir quickstart-chat -cd quickstart-chat -``` - -Now create `server`, our module, which runs in the database: - -```bash -spacetime init --lang csharp server -``` - -## Declare imports - -`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. -3. Clear it out, so we can write a new module that's still pretty simple: a bare-bones chat server. - -To the top of `server/Lib.cs`, add some imports we'll be using: - -```csharp -using System.Runtime.CompilerServices; -using SpacetimeDB.Module; -using static SpacetimeDB.Runtime; -``` - -- `System.Runtime.CompilerServices` -- `SpacetimeDB.Module` - - Contains the special attributes we'll use to define our module. - - Allows us to use the `ModuleInitializer` attribute, which we'll use to register our `OnConnect` and `OnDisconnect` callbacks. -- `SpacetimeDB.Runtime` contains the raw API bindings SpacetimeDB uses to communicate with the database. - -We also need to create our static module class which all of the module code will live in. In `server/Lib.cs`, add: - -```csharp -static partial class Module -{ -} -``` - -## Define tables - -To get our chat server running, we'll need to store two kinds of data: information about each user, and records of all the messages that have been sent. - -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 -[SpacetimeDB.Table] -public partial class User -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public Identity Identity; - public string? Name; - public bool Online; -} -``` - -For each `Message`, we'll store the `Identity` of the user who sent it, the `Timestamp` when it was sent, and the text of the message. - -In `server/Lib.cs`, add the definition of the table `Message` to the `Module` class: - -```csharp -[SpacetimeDB.Table] -public partial class Message -{ - public Identity Sender; - public long Sent; - public string Text = ""; -} -``` - -## Set users' names - -We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `SetName` which clients can invoke to set their `User.Name`. It will validate the caller's chosen name, using a function `ValidateName` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail. - -Each reducer may accept as its first argument a `DbEventArgs`, 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`, `dbEvent.Sender`. - -It's also possible to call `SetName` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though. - -In `server/Lib.cs`, add to the `Module` class: - -```csharp -[SpacetimeDB.Reducer] -public static void SetName(DbEventArgs dbEvent, string name) -{ - name = ValidateName(name); - - var user = User.FindByIdentity(dbEvent.Sender); - if (user is not null) - { - user.Name = name; - User.UpdateByIdentity(dbEvent.Sender, user); - } -} -``` - -For now, we'll just do a bare minimum of validation, rejecting the empty name. You could extend this in various ways, like: - -- Comparing against a blacklist for moderation purposes. -- Unicode-normalizing names. -- Rejecting names that contain non-printable characters, or removing characters or replacing them with a placeholder. -- Rejecting or truncating long names. -- Rejecting duplicate names. - -In `server/Lib.cs`, add to the `Module` class: - -```csharp -/// Takes a name and checks if it's acceptable as a user's name. -public static string ValidateName(string name) -{ - if (string.IsNullOrEmpty(name)) - { - throw new Exception("Names must not be empty"); - } - return name; -} -``` - -## Send messages - -We define a reducer `SendMessage`, 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 `Time` timestamp taken from the `DbEventArgs`. - -In `server/Lib.cs`, add to the `Module` class: - -```csharp -[SpacetimeDB.Reducer] -public static void SendMessage(DbEventArgs dbEvent, string text) -{ - text = ValidateMessage(text); - Log(text); - new Message - { - Sender = dbEvent.Sender, - Text = text, - Sent = dbEvent.Time.ToUnixTimeMilliseconds(), - }.Insert(); -} -``` - -We'll want to validate messages' texts in much the same way we validate users' chosen names. As above, we'll do the bare minimum, rejecting only empty messages. - -In `server/Lib.cs`, add to the `Module` class: - -```csharp -/// Takes a message's text and checks if it's acceptable to send. -public static string ValidateMessage(string text) -{ - if (string.IsNullOrEmpty(text)) - { - throw new ArgumentException("Messages must not be empty"); - } - return text; -} -``` - -You could extend the validation in `ValidateMessage` in similar ways to `ValidateName`, or add additional checks to `SendMessage`, like: - -- Rejecting messages from senders who haven't set their names. -- Rate-limiting users so they can't send new messages too quickly. - -## Set users' online status - -In C# modules, you can register for `Connect` and `Disconnect` events by using a special `ReducerKind`. We'll use the `Connect` event to create a `User` record for the client if it doesn't yet exist, and to set its online status. - -We'll use `User.FindByIdentity` to look up a `User` row for `dbEvent.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. - -In `server/Lib.cs`, add the definition of the connect reducer to the `Module` class: - -```csharp -[SpacetimeDB.Reducer(ReducerKind.Connect)] -public static void OnConnect(DbEventArgs dbEventArgs) -{ - Log($"Connect {dbEventArgs.Sender}"); - var user = User.FindByIdentity(dbEventArgs.Sender); - - if (user is not null) - { - // If this is a returning user, i.e., we already have a `User` with this `Identity`, - // set `Online: true`, but leave `Name` and `Identity` unchanged. - user.Online = true; - User.UpdateByIdentity(dbEventArgs.Sender, user); - } - else - { - // If this is a new user, create a `User` object for the `Identity`, - // which is online, but hasn't set a name. - new User - { - Name = null, - Identity = dbEventArgs.Sender, - Online = true, - }.Insert(); - } -} -``` - -Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.Disconnect`. We'll use it to un-set the `Online` status of the `User` for the disconnected client. - -Add the following code after the `OnConnect` lambda: - -```csharp -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void OnDisconnect(DbEventArgs dbEventArgs) -{ - var user = User.FindByIdentity(dbEventArgs.Sender); - - if (user is not null) - { - // This user should exist, so set `Online: false`. - user.Online = false; - User.UpdateByIdentity(dbEventArgs.Sender, user); - } - else - { - // User does not exist, log warning - Log("Warning: No user found for disconnected client."); - } -} -``` - -## Publish the module - -And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more pleasant. Come up with a unique name, and fill it in where we've written ``. - -From the `quickstart-chat` directory, run: - -```bash -spacetime publish --project-path server -``` - -```bash -npm i wasm-opt -g -``` - -## Call Reducers - -You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. - -```bash -spacetime call send_message "Hello, World!" -``` - -Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. - -```bash -spacetime logs -``` - -You should now see the output that your module printed in the database. - -```bash -info: Hello, World! -``` - -## SQL Queries - -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" -``` - -```bash - text ---------- - "Hello, World!" -``` - -## What's next? - -You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](rust-sdk-quickstart-guide.), [C#](csharp-sdk-quickstart-guide.), [TypeScript](typescript-sdk-quickstart-guide.) or [Python](python-sdk-quickstart-guide.). - -If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](part-1.) or check out our example game, [BitcraftMini](part-3.). diff --git a/Writerside2/topics/modules/modules_index.md b/Writerside2/topics/modules/modules_index.md deleted file mode 100644 index fd1a7e62..00000000 --- a/Writerside2/topics/modules/modules_index.md +++ /dev/null @@ -1,30 +0,0 @@ -# Server Module Overview - -Server modules are the core of a SpacetimeDB application. They define the structure of the database and the server-side logic that processes and handles client requests. These functions are called reducers and are transactional, meaning they ensure data consistency and integrity. Reducers can perform operations such as inserting, updating, and deleting data in the database. - -In the following sections, we'll cover the basics of server modules and how to create and deploy them. - -## Supported Languages - -### Rust - -As of SpacetimeDB 0.6, Rust is the only fully supported language for server modules. Rust is a great option for server modules because it is fast, safe, and has a small runtime. - -- [Rust Module Reference](rust.) -- [Rust Module Quickstart Guide](quickstart.) - -### C# - -We have C# support available in experimental status. C# can be a good choice for developers who are already using Unity or .net for their client applications. - -- [C# Module Reference](c-sharp.) -- [C# Module Quickstart Guide](quickstart1.) - -### Coming Soon - -We have plans to support additional languages in the future. - -- Python -- Typescript -- C++ -- Lua diff --git a/Writerside2/topics/modules/rust/rust_index.md b/Writerside2/topics/modules/rust/rust_index.md deleted file mode 100644 index 05d62bdc..00000000 --- a/Writerside2/topics/modules/rust/rust_index.md +++ /dev/null @@ -1,454 +0,0 @@ -# SpacetimeDB Rust Modules - -Rust clients of SpacetimeDB use the [Rust SpacetimeDB module library][module library] to write modules which interact with the SpacetimeDB database. - -First, the `spacetimedb` library provides a number of macros for creating tables and Rust `struct`s corresponding to rows in those tables. - -Then the client API allows interacting with the database inside special functions called reducers. - -This guide assumes you are familiar with some basics of Rust. At the very least, you should be familiar with the idea of using attribute macros. An extremely common example is `derive` macros. - -Derive macros look at the type they are attached to and generate some related code. In this example, `#[derive(Debug)]` generates the formatting code needed to print out a `Location` for debugging purposes. - -```rust -#[derive(Debug)] -struct Location { - x: u32, - y: u32, -} -``` - -## SpacetimeDB Macro basics - -Let's start with a highly commented example, straight from the [demo]. This Rust package defines a SpacetimeDB module, with types we can operate on and functions we can run. - -```rust -// In this small example, we have two rust imports: -// |spacetimedb::spacetimedb| is the most important attribute we'll be using. -// |spacetimedb::println| is like regular old |println|, but outputting to the module's logs. -use spacetimedb::{spacetimedb, println}; - -// This macro lets us interact with a SpacetimeDB table of Person rows. -// We can insert and delete into, and query, this table by the collection -// of functions generated by the macro. -#[spacetimedb(table)] -pub struct Person { - name: String, -} - -// This is the other key macro we will be using. A reducer is a -// stored procedure that lives in the database, and which can -// be invoked remotely. -#[spacetimedb(reducer)] -pub fn add(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) -} - -// 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 -// functions, not methods. -#[spacetimedb(reducer)] -pub fn say_hello() { - // 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() { - // Reducers run in a very constrained and sandboxed environment, - // and in particular, can't do most I/O from the Rust standard library. - // We provide an alternative |spacetimedb::println| which is just like - // the std version, excepted it is redirected out to the module's logs. - println!("Hello, {}!", person.name); - } - println!("Hello, World!"); -} - -// Reducers can't return values, but can return errors. To do so, -// the reducer must have a return type of `Result<(), T>`, for any `T` that -// implements `Debug`. Such errors returned from reducers will be formatted and -// printed out to logs. -#[spacetimedb(reducer)] -pub fn add_person(name: String) -> Result<(), String> { - if name.is_empty() { - return Err("Name cannot be empty"); - } - - Person::insert(Person { name }) -} -``` - -## Macro API - -Now we'll get into details on all the macro APIs SpacetimeDB provides, starting with all the variants of the `spacetimedb` attribute. - -### Defining tables - -`#[spacetimedb(table)]` takes no further arguments, and is applied to a Rust struct with named fields: - -```rust -#[spacetimedb(table)] -struct Table { - field1: String, - field2: u32, -} -``` - -This attribute is applied to Rust structs in order to create corresponding tables in SpacetimeDB. Fields of the Rust struct correspond to columns of the database table. - -The fields of the struct have to be types that spacetimedb knows how to encode into the database. This is captured in Rust by the `SpacetimeType` trait. - -This is automatically defined for built in numeric types: - -- `bool` -- `u8`, `u16`, `u32`, `u64`, `u128` -- `i8`, `i16`, `i32`, `i64`, `i128` -- `f32`, `f64` - -And common data structures: - -- `String` and `&str`, utf-8 string data -- `()`, the unit type -- `Option where T: SpacetimeType` -- `Vec where T: SpacetimeType` - -All `#[spacetimedb(table)]` types are `SpacetimeType`s, and accordingly, all of their fields have to be. - -```rust -#[spacetimedb(table)] -struct AnotherTable { - // Fine, some builtin types. - id: u64, - name: Option, - - // Fine, another table type. - table: Table, - - // Fine, another type we explicitly make serializable. - serial: Serial, -} -``` - -If you want to have a field that is not one of the above primitive types, and not a table of its own, you can derive the `SpacetimeType` attribute on it. - -We can derive `SpacetimeType` on `struct`s and `enum`s with members that are themselves `SpacetimeType`s. - -```rust -#[derive(SpacetimeType)] -enum Serial { - Builtin(f64), - Compound { - s: String, - bs: Vec, - } -} -``` - -Once the table is created via the macro, other attributes described below can control more aspects of the table. For instance, a particular column can be indexed, or take on values of an automatically incremented counter. These are described in detail below. - -```rust -#[spacetimedb(table)] -struct Person { - #[unique] - id: u64, - - name: String, - address: String, -} -``` - -### Defining reducers - -`#[spacetimedb(reducer)]` optionally takes a single argument, which is a frequency at which the reducer will be automatically called by the database. - -`#[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>`. - -```rust -#[spacetimedb(reducer)] -fn give_player_item(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::filter_by_item_id(id).ok_or(GameErr::InvalidId)?; - item.owner = Some(player_id); - Item::update_by_id(id, item); - Ok(()) -} - -struct Item { - #[unique] - item_id: u64, - - owner: Option, -} -``` - -Note that reducers can call non-reducer functions, including standard library functions. - -Reducers that are called periodically take an additional macro argument specifying the frequency at which they will be invoked. Durations are parsed according to https://docs.rs/humantime/latest/humantime/fn.parse_duration.html and will usually be a number of milliseconds or seconds. - -Both of these examples are invoked every second. - -```rust -#[spacetimedb(reducer, repeat = 1s)] -fn every_second() {} - -#[spacetimedb(reducer, repeat = 1000ms)] -fn every_thousand_milliseconds() {} -``` - -Finally, reducers can also receive a ReducerContext object, or the Timestamp at which they are invoked, just by taking parameters of those types first. - -```rust -#[spacetimedb(reducer, repeat = 1s)] -fn tick_timestamp(time: Timestamp) { - println!("tick at {time}"); -} - -#[spacetimedb(reducer, repeat = 500ms)] -fn tick_ctx(ctx: ReducerContext) { - println!("tick at {}", ctx.timestamp) -} -``` - -Note that each distinct time a repeating reducer is invoked, a seperate schedule is created for that reducer. So invoking `every_second` three times from the spacetimedb cli will result in the reducer being called times times each second. - -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. - -#[SpacetimeType] - -#[sats] - -## Client API - -Besides the macros for creating tables and reducers, there's two other parts of the Rust SpacetimeDB library. One is a collection of macros for logging, and the other is all the automatically generated functions for operating on those tables. - -### `println!` and friends - -Because reducers run in a WASM sandbox, they don't have access to general purpose I/O from the Rust standard library. There's no filesystem or network access, and no input or output. This means no access to things like `std::println!`, which prints to standard output. - -SpacetimeDB modules have access to logging output. These are exposed as macros, just like their `std` equivalents. The names, and all the Rust formatting machinery, work the same; just the location of the output is different. - -Logs for a module can be viewed with the `spacetime logs` command from the CLI. - -```rust -use spacetimedb::{ - println, - print, - eprintln, - eprint, - dbg, -}; - -#[spacetimedb(reducer)] -fn output(i: i32) { - // These will be logged at log::Level::Info. - println!("an int with a trailing newline: {i}"); - print!("some more text...\n"); - - // These log at log::Level::Error. - eprint!("Oops..."); - eprintln!(", we hit an error"); - - // Just like std::dbg!, this prints its argument and returns the value, - // as a drop-in way to print expressions. So this will print out |i| - // before passing the value of |i| along to the calling function. - // - // The output is logged log::Level::Debug. - OutputtedNumbers::insert(dbg!(i)); -} -``` - -### Generated functions on a SpacetimeDB table - -We'll work off these structs to see what functions SpacetimeDB generates: - -This table has a plain old column. - -```rust -#[spacetimedb(table)] -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. - -```rust -#[spacetimedb(table)] -struct Unique { - // A unique column: - #[unique] - unique_field: u64, -} -``` - -This table has an automatically incrementing column. SpacetimeDB automatically provides an incrementing sequence of values for this field, and sets the field to that value when you insert the row. - -Only integer types can be `#[unique]`: `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64` and `i128`. - -```rust -#[spacetimedb(table)] -struct Autoinc { - #[autoinc] - autoinc_field: u64, -} -``` - -These attributes can be combined, to create an automatically assigned ID usable for filtering. - -```rust -#[spacetimedb(table)] -struct Identity { - #[autoinc] - #[unique] - id_field: u64, -} -``` - -### Insertion - -We'll talk about insertion first, as there a couple of special semantics to know about. - -When we define |Ordinary| as a spacetimedb table, we get the ability to insert into it with the generated `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) { - let ordinary = Ordinary { ordinary_field: value }; - let result = Ordinary::insert(ordinary); - assert_eq!(ordinary.ordinary_field, result.ordinary_field); -} -``` - -When there is a unique column constraint on the table, insertion can fail if a uniqueness constraint is violated. - -If we insert two rows which have the same value of a unique column, the second will fail. - -```rust -#[spacetimedb(reducer)] -fn insert_unique(value: u64) { - let result = Ordinary::insert(Unique { unique_field: value }); - assert!(result.is_ok()); - - let result = Ordinary::insert(Unique { unique_field: value }); - assert!(result.is_err()); -} -``` - -When inserting a table with an `#[autoinc]` column, the database will automatically overwrite whatever we give it with an atomically increasing value. - -The returned row has the `autoinc` column set to the value that was actually written into the database. - -```rust -#[spacetimedb(reducer)] -fn insert_autoinc() { - 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 }) - assert_eq!(actual.autoinc_field, i); - } -} - -#[spacetimedb(reducer)] -fn insert_id() { - 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 }) - } -} -``` - -### Iterating - -Given a table, we can iterate over all the rows in it. - -```rust -#[spacetimedb(table)] -struct Person { - #[unique] - id: u64, - - age: u32, - name: String, - address: String, -} -``` - -// Every table structure an iter function, like: - -```rust -fn MyTable::iter() -> TableIter -``` - -`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() { - let mut addresses = HashSet::new(); - - for person in Person::iter() { - addresses.insert(person.address); - } - - for address in addresses.iter() { - println!("{address}"); - } -} -``` - -### Filtering - -Often, we don't need to look at the entire table, and instead are looking for rows with specific values in certain columns. - -Our `Person` table has a unique id column, so we can filter for a row matching that ID. Since it is unique, we will find either 0 or 1 matching rows in the database. This gets represented naturally as an `Option` in Rust. SpacetimeDB automatically creates and uses indexes for filtering on unique columns, so it is very efficient. - -The name of the filter method just corresponds to the column name. - -```rust -#[spacetimedb(reducer)] -fn filtering(id: u64) { - match Person::filter_by_id(&id) { - Some(person) => println!("Found {person}"), - None => println!("No person with id {id}"), - } -} -``` - -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::filter_by_age(&21) { - println!("{person} has turned 21"); - } -} -``` - -### Deleting - -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) -} -``` - -[macro library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/bindings-macro -[module library]: https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/lib -[demo]: /#demo diff --git a/Writerside2/topics/modules/rust/rust_quickstart.md b/Writerside2/topics/modules/rust/rust_quickstart.md deleted file mode 100644 index baa62a0d..00000000 --- a/Writerside2/topics/modules/rust/rust_quickstart.md +++ /dev/null @@ -1,272 +0,0 @@ -# Rust Module Quickstart - -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. - -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. - -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. - -## 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. - -## 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: - -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - -If you're on Windows, go [here](https://learn.microsoft.com/en-us/windows/dev-environment/rust/setup). - -## Project structure - -Create and enter a directory `quickstart-chat`: - -```bash -mkdir quickstart-chat -cd quickstart-chat -``` - -Now create `server`, our module, which runs in the database: - -```bash -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. - -To the top of `server/src/lib.rs`, add some imports we'll be using: - -```rust -use spacetimedb::{spacetimedb, ReducerContext, Identity, Timestamp}; -``` - -From `spacetimedb`, we import: - -- `spacetimedb`, an attribute macro we'll use to define tables and reducers. -- `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. - -## Define tables - -To get our chat server running, we'll need to store two kinds of data: information about each user, and records of all the messages that have been sent. - -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. - -To `server/src/lib.rs`, add the definition of the table `User`: - -```rust -#[spacetimedb(table)] -pub struct User { - #[primarykey] - identity: Identity, - name: Option, - online: bool, -} -``` - -For each `Message`, we'll store the `Identity` of the user who sent it, the `Timestamp` when it was sent, and the text of the message. - -To `server/src/lib.rs`, add the definition of the table `Message`: - -```rust -#[spacetimedb(table)] -pub struct Message { - sender: Identity, - sent: Timestamp, - text: String, -} -``` - -## Set users' names - -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`. - -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> { - 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 }); - Ok(()) - } else { - Err("Cannot set name for unknown user".to_string()) - } -} -``` - -For now, we'll just do a bare minimum of validation, rejecting the empty name. You could extend this in various ways, like: - -- Comparing against a blacklist for moderation purposes. -- Unicode-normalizing names. -- Rejecting names that contain non-printable characters, or removing characters or replacing them with a placeholder. -- Rejecting or truncating long names. -- Rejecting duplicate names. - -To `server/src/lib.rs`, add: - -```rust -/// Takes a name and checks if it's acceptable as a user's name. -fn validate_name(name: String) -> Result { - if name.is_empty() { - Err("Names must not be empty".to_string()) - } else { - Ok(name) - } -} -``` - -## 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`. - -To `server/src/lib.rs`, add: - -```rust -#[spacetimedb(reducer)] -/// Clients invoke this reducer to send messages. -pub fn send_message(ctx: ReducerContext, text: String) -> Result<(), String> { - let text = validate_message(text)?; - log::info!("{}", text); - Message::insert(Message { - sender: ctx.sender, - text, - sent: ctx.timestamp, - }); - Ok(()) -} -``` - -We'll want to validate messages' texts in much the same way we validate users' chosen names. As above, we'll do the bare minimum, rejecting only empty messages. - -To `server/src/lib.rs`, add: - -```rust -/// Takes a message's text and checks if it's acceptable to send. -fn validate_message(text: String) -> Result { - if text.is_empty() { - Err("Messages must not be empty".to_string()) - } else { - Ok(text) - } -} -``` - -You could extend the validation in `validate_message` in similar ways to `validate_name`, or add additional checks to `send_message`, like: - -- Rejecting messages from senders who haven't set their names. -- Rate-limiting users so they can't send new messages too quickly. - -## 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. - -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)]` attribute, 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`. - -To `server/src/lib.rs`, add the definition of the connect reducer: - -```rust -#[spacetimedb(connect)] -// Called when a client connects to the SpacetimeDB -pub fn identity_connected(ctx: ReducerContext) { - if let Some(user) = User::filter_by_identity(&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 }); - } 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 { - 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. - -```rust -#[spacetimedb(disconnect)] -// 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 }); - } else { - // This branch should be unreachable, - // as it doesn't make sense for a client to disconnect without connecting first. - log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender); - } -} -``` - -## 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 ``. - -From the `quickstart-chat` directory, run: - -```bash -spacetime publish --project-path server -``` - -## Call Reducers - -You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. - -```bash -spacetime call send_message 'Hello, World!' -``` - -Once we've called our `send_message` reducer, we can check to make sure it ran by running the `logs` command. - -```bash -spacetime logs -``` - -You should now see the output that your module printed in the database. - -```bash -info: Hello, World! -``` - -## SQL Queries - -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" -``` - -```bash - text ---------- - "Hello, World!" -``` - -## What's next? - -You can find the full code for this module [in the SpacetimeDB module examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/modules/quickstart-chat). - -You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quickstart guide for your client language of choice: [Rust](quickstart2.), [C#](quickstart3.), [TypeScript](quickstart4.) or [Python](quickstart5.). - -If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](part-1.) or check out our example game, [BitcraftMini](part-3.). diff --git a/Writerside2/topics/satn.md b/Writerside2/topics/satn.md deleted file mode 100644 index 774ff1b3..00000000 --- a/Writerside2/topics/satn.md +++ /dev/null @@ -1,163 +0,0 @@ -# SATN JSON Format - -The Spacetime Algebraic Type Notation JSON format defines how Spacetime `AlgebraicType`s and `AlgebraicValue`s are encoded as JSON. Algebraic types and values are JSON-encoded for transport via the [HTTP Databases API](database.) and the [WebSocket text protocol](ws#text-protocol.). - -## Values - -### At a glance - -| Type | Description | -| ---------------- | ---------------------------------------------------------------- | -| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype.). | -| `SumValue` | A value whose type is a [`SumType`](#sumtype.). | -| `ProductValue` | A value whose type is a [`ProductType`](#producttype.). | -| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype.). | -| | | - -### `AlgebraicValue` - -```json -SumValue | ProductValue | BuiltinValue -``` - -### `SumValue` - -An instance of a [`SumType`](#sumtype.). `SumValue`s are encoded as a JSON object with a single key, a non-negative integer tag which identifies the variant. The value associated with this key is the variant data. Variants which hold no data will have an empty array as their value. - -The tag is an index into the [`SumType.variants`](#sumtype.) array of the value's [`SumType`](#sumtype.). - -```json -{ - "": AlgebraicValue -} -``` - -### `ProductValue` - -An instance of a [`ProductType`](#producttype.). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#productype.) array of the value's [`ProductType`](#producttype.). - -```json -array -``` - -### `BuiltinValue` - -An instance of a [`BuiltinType`](#builtintype.). `BuiltinValue`s are encoded as JSON values of corresponding types. - -```json -boolean | number | string | array | map -``` - -| [`BuiltinType`](#builtintype.) | JSON type | -| ----------------------------- | ------------------------------------- | -| `Bool` | `boolean` | -| Integer types | `number` | -| Float types | `number` | -| `String` | `string` | -| Array types | `array` | -| Map types | `map` | - -All SATS integer types are encoded as JSON `number`s, so values of 64-bit and 128-bit integer types may lose precision when encoding values larger than 2⁵². - -## Types - -All SATS types are JSON-encoded by converting them to an `AlgebraicValue`, then JSON-encoding that meta-value. - -### At a glance - -| Type | Description | -| --------------------------------------- | ------------------------------------------------------------------------------------ | -| [`AlgebraicType`](#algebraictype.) | Any SATS type. | -| [`SumType`](#sumtype.) | Sum types, i.e. tagged unions. | -| [`ProductType`](#productype.) | Product types, i.e. structures. | -| [`BuiltinType`](#builtintype.) | Built-in and primitive types, including booleans, numbers, strings, arrays and maps. | -| [`AlgebraicTypeRef`](#algebraictyperef.) | An indirect reference to a type, used to implement recursive types. | - -#### `AlgebraicType` - -`AlgebraicType` is the most general meta-type in the Spacetime Algebraic Type System. Any SATS type can be represented as an `AlgebraicType`. `AlgebraicType` is encoded as a tagged union, with variants for [`SumType`](#sumtype.), [`ProductType`](#producttype.), [`BuiltinType`](#builtintype.) and [`AlgebraicTypeRef`](#algebraictyperef.). - -```json -{ "Sum": SumType } -| { "Product": ProductType } -| { "Builtin": BuiltinType } -| { "Ref": AlgebraicTypeRef } -``` - -#### `SumType` - -The meta-type `SumType` represents sum types, also called tagged unions or Rust `enum`s. A sum type has some number of variants, each of which has an `AlgebraicType` of variant data, and an optional string discriminant. For each instance, exactly one variant will be active. The instance will contain only that variant's data. - -A `SumType` with zero variants is called an empty type or never type because it is impossible to construct an instance. - -Instances of `SumType`s are [`SumValue`s](#sumvalue.), and store a tag which identifies the active variant. - -```json -// SumType: -{ - "variants": array, -} - -// SumTypeVariant: -{ - "algebraic_type": AlgebraicType, - "name": { "some": string } | { "none": [] } -} -``` - -### `ProductType` - -The meta-type `ProductType` represents product types, also called structs or tuples. A product type has some number of fields, each of which has an `AlgebraicType` of field data, and an optional string field name. Each instance will contain data for all of the product type's fields. - -A `ProductType` with zero fields is called a unit type because it has a single instance, the unit, which is empty. - -Instances of `ProductType`s are [`ProductValue`s](#productvalue.), and store an array of field data. - -```json -// ProductType: -{ - "elements": array, -} - -// ProductTypeElement: -{ - "algebraic_type": AlgebraicType, - "name": { "some": string } | { "none": [] } -} -``` - -### `BuiltinType` - -The meta-type `BuiltinType` represents SATS primitive types: booleans, integers, floating-point numbers, strings, arrays and maps. `BuiltinType` is encoded as a tagged union, with a variant for each SATS primitive type. - -SATS integer types are identified by their signedness and width in bits. SATS supports the same set of integer types as Rust, i.e. 8, 16, 32, 64 and 128-bit signed and unsigned integers. - -SATS floating-point number types are identified by their width in bits. SATS supports 32 and 64-bit floats, which correspond to [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) single- and double-precision binary floats, respectively. - -SATS array and map types are homogeneous, meaning that each array has a single element type to which all its elements must conform, and each map has a key type and a value type to which all of its keys and values must conform. - -```json -{ "Bool": [] } -| { "I8": [] } -| { "U8": [] } -| { "I16": [] } -| { "U16": [] } -| { "I32": [] } -| { "U32": [] } -| { "I64": [] } -| { "U64": [] } -| { "I128": [] } -| { "U128": [] } -| { "F32": [] } -| { "F64": [] } -| { "String": [] } -| { "Array": AlgebraicType } -| { "Map": { - "key_ty": AlgebraicType, - "ty": AlgebraicType, - } } -``` - -### `AlgebraicTypeRef` - -`AlgebraicTypeRef`s are JSON-encoded as non-negative integers. These are indices into a typespace, like the one returned by the [`/database/schema/:name_or_address GET` HTTP endpoint](database#databaseschemaname_or_address-get.). diff --git a/Writerside2/topics/sdks/c-sharp/c-sharp_quickstart.md b/Writerside2/topics/sdks/c-sharp/c-sharp_quickstart.md deleted file mode 100644 index 4d5b1e92..00000000 --- a/Writerside2/topics/sdks/c-sharp/c-sharp_quickstart.md +++ /dev/null @@ -1,438 +0,0 @@ -# C# Client SDK Quick Start - -In this guide we'll show you how to get up and running with a simple SpacetimeDB app with a client written in C#. - -We'll implement a command-line client for the module created in our [Rust](rust_quickstart.md) or [C# Module](quickstart.md) Quickstart guides. Ensure you followed one of these guides before continuing. - -## Project structure - -Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: - -```bash -cd quickstart-chat -``` - -Within it, create a new C# console application project called `client` using either Visual Studio, Rider or the .NET CLI: - -```bash -dotnet new console -o client -``` - -Open the project in your IDE of choice. - -## Add the NuGet package for the C# SpacetimeDB SDK - -Add the `SpacetimeDB.ClientSDK` [NuGet package](https://www.nuget.org/packages/spacetimedbsdk) using Visual Studio or Rider _NuGet Package Manager_ or via the .NET CLI: - -```bash -dotnet add package SpacetimeDB.ClientSDK -``` - -## Generate your module types - -The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. - -In your `quickstart-chat` directory, run: - -```bash -mkdir -p client/module_bindings -spacetime generate --lang csharp --out-dir client/module_bindings --project-path server -``` - -Take a look inside `client/module_bindings`. The CLI should have generated five files: - -``` -module_bindings -├── Message.cs -├── ReducerEvent.cs -├── SendMessageReducer.cs -├── SetNameReducer.cs -└── User.cs -``` - -## Add imports to Program.cs - -Open `client/Program.cs` and add the following imports: - -```csharp -using SpacetimeDB; -using SpacetimeDB.Types; -using System.Collections.Concurrent; -``` - -We will also need to create some global variables that will be explained when we use them later. Add the following to the top of `Program.cs`: - -```csharp -// our local client SpacetimeDB identity -Identity? local_identity = null; - -// declare a thread safe queue to store commands in format (command, args) -ConcurrentQueue<(string,string)> input_queue = new ConcurrentQueue<(string, string)>(); - -// declare a threadsafe cancel token to cancel the process loop -CancellationTokenSource cancel_token = new CancellationTokenSource(); -``` - -## Define 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 several things: - -1. Initialize the `AuthToken` module, which loads and stores our authentication token to/from local storage. -2. Create the `SpacetimeDBClient` instance. -3. Register callbacks on any events we want to handle. These will print to standard output messages received from the database and updates about users' names and online statuses. -4. Start our processing thread which connects to the SpacetimeDB module, updates the SpacetimeDB client and processes commands that come in from the input loop running in the main thread. -5. Start the input loop, which reads commands from standard input and sends them to the processing thread. -6. When the input loop exits, stop the processing thread and wait for it to exit. - -```csharp -void Main() -{ - AuthToken.Init(".spacetime_csharp_quickstart"); - - // create the client, pass in a logger to see debug messages - SpacetimeDBClient.CreateInstance(new ConsoleLogger()); - - RegisterCallbacks(); - - // spawn a thread to call process updates and process commands - var thread = new Thread(ProcessThread); - thread.Start(); - - InputLoop(); - - // this signals the ProcessThread to stop - cancel_token.Cancel(); - thread.Join(); -} -``` - -## Register callbacks - -We need to handle several sorts of events: - -1. `onConnect`: When we connect, we will call `Subscribe` to tell the module what tables we care about. -2. `onIdentityReceived`: When we receive our credentials, we'll use the `AuthToken` module to save our token so that the next time we connect, we can re-authenticate as the same user. -3. `onSubscriptionApplied`: When we get the onSubscriptionApplied callback, that means our local client cache has been fully populated. At this time we'll print the user menu. -4. `User.OnInsert`: When a new user joins, we'll print a message introducing them. -5. `User.OnUpdate`: When a user is updated, we'll print their new name, or declare their new online status. -6. `Message.OnInsert`: When we receive a new message, we'll print it. -7. `Reducer.OnSetNameEvent`: If the server rejects our attempt to set our name, we'll print an error. -8. `Reducer.OnSendMessageEvent`: If the server rejects a message we send, we'll print an error. - -```csharp -void RegisterCallbacks() -{ - SpacetimeDBClient.instance.onConnect += OnConnect; - SpacetimeDBClient.instance.onIdentityReceived += OnIdentityReceived; - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - - User.OnInsert += User_OnInsert; - User.OnUpdate += User_OnUpdate; - - Message.OnInsert += Message_OnInsert; - - Reducer.OnSetNameEvent += Reducer_OnSetNameEvent; - Reducer.OnSendMessageEvent += Reducer_OnSendMessageEvent; -} -``` - -### 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 `OnInsert` and `OnDelete` methods, which are automatically generated 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.OnInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. - -`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `ReducerEvent`. This will be `null` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument. - -Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `UserNameOrIdentity` to handle this. - -```csharp -string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()!.Substring(0, 8); - -void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) -{ - if (insertedValue.Online) - { - Console.WriteLine($"{UserNameOrIdentity(insertedValue)} is online"); - } -} -``` - -### Notify about updated users - -Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User::update_by_identity` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column. - -`OnUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. - -In our module, users can be updated for three reasons: - -1. They've set their name using the `SetName` reducer. -2. They're an existing user re-connecting, so their `Online` has been set to `true`. -3. They've disconnected, so their `Online` has been set to `false`. - -We'll print an appropriate message in each of these cases. - -```csharp -void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) -{ - if (oldValue.Name != newValue.Name) - { - Console.WriteLine($"{UserNameOrIdentity(oldValue)} renamed to {newValue.Name}"); - } - - if (oldValue.Online == newValue.Online) - return; - - if (newValue.Online) - { - Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); - } - else - { - Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); - } -} -``` - -### 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 `SendMessage` reducer invocation. We have to handle the backlog we receive when our subscription is initialized separately, to ensure they're printed in the correct order. To that effect, our `OnInsert` callback will check if its `ReducerEvent` argument is not `null`, and only print in that case. - -To find the `User` based on the message's `Sender` identity, we'll use `User::FilterByIdentity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `FilterByIdentity` accepts a `byte[]`, rather than an `Identity`. The `Sender` identity stored in the message is also a `byte[]`, not an `Identity`, so we can just pass it to the filter method. - -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`. - -```csharp -void PrintMessage(Message message) -{ - var sender = User.FilterByIdentity(message.Sender); - var senderName = "unknown"; - if (sender != null) - { - senderName = UserNameOrIdentity(sender); - } - - Console.WriteLine($"{senderName}: {message.Text}"); -} - -void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent) -{ - if (dbEvent != null) - { - PrintMessage(insertedValue); - } -} -``` - -### Warn if our name was rejected - -We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducerEvent` method of the `Reducer` namespace, which is automatically implemented for each reducer by `spacetime generate`. - -Each reducer callback takes one fixed argument: - -The ReducerEvent that triggered the callback. It contains several fields. The ones we care about are: - -1. The `Identity` of the client that called the reducer. -2. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`. -3. The error message, if any, that the reducer returned. - -It also takes a variable amount of additional arguments that match the reducer's arguments. - -These callbacks will be invoked in one of two cases: - -1. If the reducer was successful and altered any of our subscribed rows. -2. If we requested an invocation which failed. - -Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. - -We already handle successful `SetName` invocations using our `User.OnUpdate` 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 `Reducer_OnSetNameEvent` as a `Reducer.OnSetNameEvent` callback which checks if the reducer failed, and if it did, prints an error message including the rejected name. - -We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. - -```csharp -void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) -{ - bool localIdentityFailedToChangeName = - reducerEvent.Identity == local_identity && - reducerEvent.Status == ClientApi.Event.Types.Status.Failed; - - if (localIdentityFailedToChangeName) - { - Console.Write($"Failed to change name to {name}"); - } -} -``` - -### 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. - -```csharp -void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) -{ - bool localIdentityFailedToSendMessage = - reducerEvent.Identity == local_identity && - reducerEvent.Status == ClientApi.Event.Types.Status.Failed; - - if (localIdentityFailedToSendMessage) - { - Console.Write($"Failed to send message {text}"); - } -} -``` - -## Connect callback - -Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. - -```csharp -void OnConnect() -{ - SpacetimeDBClient.instance.Subscribe(new List - { - "SELECT * FROM User", "SELECT * FROM Message" - }); -} -``` - -## OnIdentityReceived callback - -This callback is executed when we receive our credentials from the SpacetimeDB module. We'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app. - -```csharp -void OnIdentityReceived(string authToken, Identity identity, Address _address) -{ - local_identity = identity; - AuthToken.SaveToken(authToken); -} -``` - -## OnSubscriptionApplied callback - -Once our subscription is applied, we'll print all the previously sent messages. We'll define a function `PrintMessagesInOrder` to do this. `PrintMessagesInOrder` calls the automatically generated `Iter` function on our `Message` table, which returns an iterator over all rows in the table. We'll use the `OrderBy` method on the iterator to sort the messages by their `Sent` timestamp. - -```csharp -void PrintMessagesInOrder() -{ - foreach (Message message in Message.Iter().OrderBy(item => item.Sent)) - { - PrintMessage(message); - } -} - -void OnSubscriptionApplied() -{ - Console.WriteLine("Connected"); - PrintMessagesInOrder(); -} -``` - - - -## Process thread - -Since the input loop will be blocking, we'll run our processing code in a separate thread. This thread will: - -1. Connect to the module. We'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`. We will also store if SSL is enabled in a constant called `SSL_ENABLED`. This only needs to be `true` if we are using `SpacetimeDB Cloud`. Replace `` with the name you chose when publishing your module during the module quickstart. - -`Connect` takes an auth token, which is `null` for a new connection, or a stored string for a returning user. We are going to use the optional AuthToken module which uses local storage to store the auth token. If you want to use your own way to associate an auth token with a user, you can pass in your own auth token here. - -2. Loop until the thread is signaled to exit, calling `Update` on the SpacetimeDBClient to process any updates received from the module, and `ProcessCommand` to process any commands received from the input loop. - -3. Finally, Close the connection to the module. - -```csharp -const string HOST = "http://localhost:3000"; -const string DBNAME = "module"; - -void ProcessThread() -{ - SpacetimeDBClient.instance.Connect(AuthToken.Token, HOST, DBNAME); - - // loop until cancellation token - while (!cancel_token.IsCancellationRequested) - { - SpacetimeDBClient.instance.Update(); - - ProcessCommands(); - - Thread.Sleep(100); - } - - SpacetimeDBClient.instance.Close(); -} -``` - -## Input loop and ProcessCommands - -The input loop will read commands from standard input and send them to the processing thread using the input queue. The `ProcessCommands` function is called every 100ms by the processing thread to process any pending commands. - -Supported Commands: - -1. Send a message: `message`, send the message to the module by calling `Reducer.SendMessage` which is automatically generated by `spacetime generate`. - -2. Set name: `name`, will send the new name to the module by calling `Reducer.SetName` which is automatically generated by `spacetime generate`. - -```csharp -void InputLoop() -{ - while (true) - { - var input = Console.ReadLine(); - if (input == null) - { - break; - } - - if (input.StartsWith("/name ")) - { - input_queue.Enqueue(("name", input.Substring(6))); - continue; - } - else - { - input_queue.Enqueue(("message", input)); - } - } -} - -void ProcessCommands() -{ - // process input queue commands - while (input_queue.TryDequeue(out var command)) - { - switch (command.Item1) - { - case "message": - Reducer.SendMessage(command.Item2); - break; - case "name": - Reducer.SetName(command.Item2); - break; - } - } -} -``` - -## Run the client - -Finally we just need to add a call to `Main` in `Program.cs`: - -```csharp -Main(); -``` - -Now, we can run the client by hitting start in Visual Studio or Rider; or by running the following command in the `client` directory: - -```bash -dotnet run --project client -``` - -## What's next? - -Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example. diff --git a/Writerside2/topics/sdks/c-sharp/sdks_c-sharp_index.md b/Writerside2/topics/sdks/c-sharp/sdks_c-sharp_index.md deleted file mode 100644 index a0f1c7f3..00000000 --- a/Writerside2/topics/sdks/c-sharp/sdks_c-sharp_index.md +++ /dev/null @@ -1,959 +0,0 @@ -# The SpacetimeDB C# client SDK - -The SpacetimeDB client C# for Rust contains all the tools you need to build native clients for SpacetimeDB modules using C#. - -## Table of Contents - -- [The SpacetimeDB C# client SDK](#the-spacetimedb-c-client-sdk.) - - [Table of Contents](#table-of-contents.) - - [Install the SDK](#install-the-sdk.) - - [Using the `dotnet` CLI tool](#using-the-dotnet-cli-tool.) - - [Using Unity](#using-unity.) - - [Generate module bindings](#generate-module-bindings.) - - [Initialization](#initialization.) - - [Static Method `SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) - - [Property `SpacetimeDBClient.instance`](#property-spacetimedbclientinstance.) - - [Class `NetworkManager`](#class-networkmanager.) - - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect.) - - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived.) - - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect.) - - [Query subscriptions & one-time actions](#subscribe-to-queries.) - - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.) - - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied.) - - [Method `SpacetimeDBClient.OneOffQuery`](#event-spacetimedbclientoneoffquery.) - - [View rows of subscribed tables](#view-rows-of-subscribed-tables.) - - [Class `{TABLE}`](#class-table.) - - [Static Method `{TABLE}.Iter`](#static-method-tableiter.) - - [Static Method `{TABLE}.FilterBy{COLUMN}`](#static-method-tablefilterbycolumn.) - - [Static Method `{TABLE}.Count`](#static-method-tablecount.) - - [Static Event `{TABLE}.OnInsert`](#static-event-tableoninsert.) - - [Static Event `{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) - - [Static Event `{TABLE}.OnDelete`](#static-event-tableondelete.) - - [Static Event `{TABLE}.OnUpdate`](#static-event-tableonupdate.) - - [Observe and invoke reducers](#observe-and-invoke-reducers.) - - [Class `Reducer`](#class-reducer.) - - [Static Method `Reducer.{REDUCER}`](#static-method-reducerreducer.) - - [Static Event `Reducer.On{REDUCER}`](#static-event-reduceronreducer.) - - [Class `ReducerEvent`](#class-reducerevent.) - - [Enum `Status`](#enum-status.) - - [Variant `Status.Committed`](#variant-statuscommitted.) - - [Variant `Status.Failed`](#variant-statusfailed.) - - [Variant `Status.OutOfEnergy`](#variant-statusoutofenergy.) - - [Identity management](#identity-management.) - - [Class `AuthToken`](#class-authtoken.) - - [Static Method `AuthToken.Init`](#static-method-authtokeninit.) - - [Static Property `AuthToken.Token`](#static-property-authtokentoken.) - - [Static Method `AuthToken.SaveToken`](#static-method-authtokensavetoken.) - - [Class `Identity`](#class-identity.) - - [Class `Identity`](#class-identity-1.) - - [Customizing logging](#customizing-logging.) - - [Interface `ISpacetimeDBLogger`](#interface-ispacetimedblogger.) - - [Class `ConsoleLogger`](#class-consolelogger.) - - [Class `UnityDebugLogger`](#class-unitydebuglogger.) - -## Install the SDK - -### Using the `dotnet` CLI tool - -If you would like to create a console application using .NET, you can create a new project using `dotnet new console` and add the SpacetimeDB SDK to your dependencies: - -```bash -dotnet add package spacetimedbsdk -``` - -(See also the [CSharp Quickstart](quickstart1.) for an in-depth example of such a console application.) - -### Using Unity - -To install the SpacetimeDB SDK into a Unity project, [download the SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest), packaged as a `.unitypackage`. - -In Unity navigate to the `Assets > Import Package > Custom Package` menu in the menu bar. Select your `SpacetimeDB.Unity.Comprehensive.Tutorial.unitypackage` file and leave all folders checked. - -(See also the [Unity Tutorial](part-1.)) - -## Generate module bindings - -Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the C# interface files using the Spacetime CLI. From your project directory, run: - -```bash -mkdir -p module_bindings -spacetime generate --lang cs --out-dir module_bindings --project-path PATH-TO-MODULE-DIRECTORY -``` - -Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. - -## Initialization - -### Static Method `SpacetimeDBClient.CreateInstance` - -```cs -namespace SpacetimeDB { - -public class SpacetimeDBClient { - public static void CreateInstance(ISpacetimeDBLogger loggerToUse); -} - -} -``` - -Create a global SpacetimeDBClient instance, accessible via [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance.) - -| Argument | Type | Meaning | -| ------------- | ----------------------------------------------------- | --------------------------------- | -| `loggerToUse` | [`ISpacetimeDBLogger`](#interface-ispacetimedblogger.) | The logger to use to log messages | - -There is a provided logger called [`ConsoleLogger`](#class-consolelogger.) which logs to `System.Console`, and can be used as follows: - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; -SpacetimeDBClient.CreateInstance(new ConsoleLogger()); -``` - -### Property `SpacetimeDBClient.instance` - -```cs -namespace SpacetimeDB { - -public class SpacetimeDBClient { - public static SpacetimeDBClient instance; -} - -} -``` - -This is the global instance of a SpacetimeDB client in a particular .NET/Unity process. Much of the SDK is accessible through this instance. - -### Class `NetworkManager` - -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. - -![Unity-AddNetworkManager](Unity-AddNetworkManager.JPG) - -This component will handle calling [`SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect.), you still need to handle that yourself. See the [Unity Quickstart](UnityQuickStart.) and [Unity Tutorial](UnityTutorialPart1.) for more information. - -### Method `SpacetimeDBClient.Connect` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public void Connect( - string? token, - string host, - string addressOrName, - bool sslEnabled = true - ); -} - -} -``` - - - -Connect to a database named `addressOrName` accessible over the internet at the URI `host`. - -| Argument | Type | Meaning | -| --------------- | --------- | -------------------------------------------------------------------------- | -| `token` | `string?` | Identity token to use, if one is available. | -| `host` | `string` | URI of the SpacetimeDB instance running the module. | -| `addressOrName` | `string` | Address or name of the module. | -| `sslEnabled` | `bool` | Whether or not to use SSL when connecting to SpacetimeDB. Default: `true`. | - -If a `token` is supplied, it will be passed to the new connection to identify and authenticate the user. Otherwise, a new token and [`Identity`](#class-identity.) will be generated by the server and returned in [`onConnect`](#event-spacetimedbclientonconnect.). - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -const string DBNAME = "chat"; - -// Connect to a local DB with a fresh identity -SpacetimeDBClient.instance.Connect(null, "localhost:3000", DBNAME, false); - -// Connect to cloud with a fresh identity -SpacetimeDBClient.instance.Connect(null, "dev.spacetimedb.net", DBNAME, true); - -// Connect to cloud using a saved identity from the filesystem, or get a new one and save it -AuthToken.Init(); -Identity localIdentity; -SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); -SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { - AuthToken.SaveToken(authToken); - localIdentity = identity; -} -``` - -(You should probably also store the returned `Identity` somewhere; see the [`onIdentityReceived`](#event-spacetimedbclientonidentityreceived.) event.) - -### Event `SpacetimeDBClient.onIdentityReceived` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public event Action onIdentityReceived; -} - -} -``` - -+Called when we receive an auth token, [`Identity`](#class-identity.) and [`Address`](#class-address.) from the server. The [`Identity`](#class-identity.) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The [`Address`](#class-address.) is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity.). - -To store the auth token to the filesystem, use the static method [`AuthToken.SaveToken`](#static-method-authtokensavetoken.). You may also want to store the returned [`Identity`](#class-identity.) in a local variable. - -If an existing auth token is used to connect to the database, the same auth token and the identity it came with will be returned verbatim in `onIdentityReceived`. - -```cs -// Connect to cloud using a saved identity from the filesystem, or get a new one and save it -AuthToken.Init(); -Identity localIdentity; -SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); -SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { - AuthToken.SaveToken(authToken); - localIdentity = identity; -} -``` - -### Event `SpacetimeDBClient.onConnect` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public event Action onConnect; -} - -} -``` - -Allows registering delegates to be invoked upon authentication with the database. - -Once this occurs, the SDK is prepared for calls to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.). - -## Subscribe to queries - -### Method `SpacetimeDBClient.Subscribe` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public void Subscribe(List queries); -} - -} -``` - -| Argument | Type | Meaning | -| --------- | -------------- | ---------------------------- | -| `queries` | `List` | SQL queries to subscribe to. | - -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -`Subscribe` will return an error if called before establishing a connection with the [`SpacetimeDBClient.Connect`](#method-connect.) function. In that case, the queries are not registered. - -The `Subscribe` method does not return data directly. `spacetime generate` will generate classes [`SpacetimeDB.Types.{TABLE}`](#class-table.) for each table in your module. These classes are used to reecive information from the database. See the section [View Rows of Subscribed Tables](#view-rows-of-subscribed-tables.) for more information. - -A new call to `Subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache, and [`{TABLE}.OnDelete`](#event-tableondelete.) callbacks will be invoked for them. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -void Main() -{ - AuthToken.Init(); - SpacetimeDBClient.CreateInstance(new ConsoleLogger()); - - SpacetimeDBClient.instance.onConnect += OnConnect; - - // Our module contains a table named "Loot" - Loot.OnInsert += Loot_OnInsert; - - SpacetimeDBClient.instance.Connect(/* ... */); -} - -void OnConnect() -{ - SpacetimeDBClient.instance.Subscribe(new List { - "SELECT * FROM Loot" - }); -} - -void Loot_OnInsert( - Loot loot, - ReducerEvent? event -) { - Console.Log($"Loaded loot {loot.itemType} at coordinates {loot.position}"); -} -``` - -### Event `SpacetimeDBClient.onSubscriptionApplied` - -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public event Action onSubscriptionApplied; -} - -} -``` - -Register a delegate to be invoked when a subscription is registered with the database. - -```cs -using SpacetimeDB; - -void OnSubscriptionApplied() -{ - Console.WriteLine("Now listening on queries."); -} - -void Main() -{ - // ...initialize... - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; -} -``` - -### Method [`OneTimeQuery`](#method-spacetimedbclientsubscribe.) - -You may not want to subscribe to a query, but instead want to run a query once and receive the results immediately via a `Task` result: - -```csharp -// Query all Messages from the sender "bob" -SpacetimeDBClient.instance.OneOffQuery("WHERE sender = \"bob\""); -``` - -## View rows of subscribed tables - -The SDK maintains a local view of the database called the "client cache". This cache contains whatever rows are selected via a call to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.). These rows are represented in the SpacetimeDB .Net SDK as instances of [`SpacetimeDB.Types.{TABLE}`](#class-table.). - -ONLY the rows selected in a [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe.) call will be available in the client cache. All operations in the client sdk operate on these rows exclusively, and have no information about the state of the rest of the database. - -In particular, SpacetimeDB does not support foreign key constraints. This means that if you are using a column as a foreign key, SpacetimeDB will not automatically bring in all of the rows that key might reference. You will need to manually subscribe to all tables you need information from. - -To optimize network performance, prefer selecting as few rows as possible in your [`Subscribe`](#method-spacetimedbclientsubscribe.) query. Processes that need to view the entire state of the database are better run inside the database -- that is, inside modules. - -### Class `{TABLE}` - -For each table defined by a module, `spacetime generate` will generate a class [`SpacetimeDB.Types.{TABLE}`](#class-table.) whose name is that table's name converted to `PascalCase`. The generated class contains a property for each of the table's columns, whose names are the column names converted to `camelCase`. It also contains various static events and methods. - -Static Methods: - -- [`{TABLE}.Iter()`](#static-method-tableiter.) iterates all subscribed rows in the client cache. -- [`{TABLE}.FilterBy{COLUMN}(value)`](#static-method-tablefilterbycolumn.) filters subscribed rows in the client cache by a column value. -- [`{TABLE}.Count()`](#static-method-tablecount.) counts the number of subscribed rows in the client cache. - -Static Events: - -- [`{TABLE}.OnInsert`](#static-event-tableoninsert.) is called when a row is inserted into the client cache. -- [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) is called when a row is about to be removed from the client cache. -- If the table has a primary key attribute, [`{TABLE}.OnUpdate`](#static-event-tableonupdate.) is called when a row is updated. -- [`{TABLE}.OnDelete`](#static-event-tableondelete.) is called while a row is being removed from the client cache. You should almost always use [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.) instead. - -Note that it is not possible to directly insert into the database from the client SDK! All insertion validation should be performed inside serverside modules for security reasons. You can instead [invoke reducers](#observe-and-invoke-reducers.), which run code inside the database that can insert rows for you. - -#### Static Method `{TABLE}.Iter` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public static System.Collections.Generic.IEnumerable
Iter(); -} - -} -``` - -Iterate over all the subscribed rows in the table. This method is only available after [`SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied.) has occurred. - -When iterating over rows and filtering for those containing a particular column, [`TableType::filter`](#method-filter.) will be more efficient, so prefer it when possible. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { - SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); -}; -SpacetimeDBClient.instance.onSubscriptionApplied += () => { - // Will print a line for each `User` row in the database. - foreach (var user in User.Iter()) { - Console.WriteLine($"User: {user.Name}"); - } -}; -SpacetimeDBClient.instance.connect(/* ... */); -``` - -#### Static Method `{TABLE}.FilterBy{COLUMN}` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - // If the column has no #[unique] or #[primarykey] constraint - public static System.Collections.Generic.IEnumerable
FilterBySender(COLUMNTYPE value); - - // If the column has a #[unique] or #[primarykey] constraint - public static TABLE? FilterBySender(COLUMNTYPE value); -} - -} -``` - -For each column of a table, `spacetime generate` generates a static method on the [table class](#class-table.) to filter or seek subscribed rows where that column matches a requested value. These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `PascalCase`. - -The method's return type depends on the column's attributes: - -- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filterBy{COLUMN}` method returns a `{TABLE}?`, where `{TABLE}` is the [table class](#class-table.). -- For non-unique columns, the `filter_by` method returns an `IEnumerator<{TABLE}>`. - -#### Static Method `{TABLE}.Count` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public static int Count(); -} - -} -``` - -Return the number of subscribed rows in the table, or 0 if there is no active connection. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { - SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); -}; -SpacetimeDBClient.instance.onSubscriptionApplied += () => { - Console.WriteLine($"There are {User.Count()} users in the database."); -}; -SpacetimeDBClient.instance.connect(/* ... */); -``` - -#### Static Event `{TABLE}.OnInsert` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void InsertEventHandler( - TABLE insertedValue, - ReducerEvent? dbEvent - ); - public static event InsertEventHandler OnInsert; -} - -} -``` - -Register a delegate for when a subscribed row is newly inserted into the database. - -The delegate takes two arguments: - -- A [`{TABLE}`](#class-table.) instance with the data of the inserted row -- A [`ReducerEvent?`], which contains the data of the reducer that inserted the row, or `null` if the row is being inserted while initializing a subscription. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnInsert += (User user, ReducerEvent? reducerEvent) => { - if (reducerEvent == null) { - Console.WriteLine($"New user '{user.Name}' received during subscription update."); - } else { - Console.WriteLine($"New user '{user.Name}' inserted by reducer {reducerEvent.Reducer}."); - } -}; -``` - -#### Static Event `{TABLE}.OnBeforeDelete` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void DeleteEventHandler( - TABLE deletedValue, - ReducerEvent dbEvent - ); - public static event DeleteEventHandler OnBeforeDelete; -} - -} -``` - -Register a delegate for when a subscribed row is about to be deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked for each of those rows before any of them is deleted. - -The delegate takes two arguments: - -- A [`{TABLE}`](#class-table.) instance with the data of the deleted row -- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that deleted the row. - -This event should almost always be used instead of [`OnDelete`](#static-event-tableondelete.). This is because often, many rows will be deleted at once, and `OnDelete` can be invoked in an arbitrary order on these rows. This means that data related to a row may already be missing when `OnDelete` is called. `OnBeforeDelete` does not have this problem. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { - Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); -}; -``` - -#### Static Event `{TABLE}.OnDelete` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void DeleteEventHandler( - TABLE deletedValue, - SpacetimeDB.ReducerEvent dbEvent - ); - public static event DeleteEventHandler OnDelete; -} - -} -``` - -Register a delegate for when a subscribed row is being deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked on those rows in arbitrary order, and data for some rows may already be missing when it is invoked. For this reason, prefer the event [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete.). - -The delegate takes two arguments: - -- A [`{TABLE}`](#class-table.) instance with the data of the deleted row -- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that deleted the row. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { - Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); -}; -``` - -#### Static Event `{TABLE}.OnUpdate` - -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void UpdateEventHandler( - TABLE oldValue, - TABLE newValue, - ReducerEvent dbEvent - ); - public static event UpdateEventHandler OnUpdate; -} - -} -``` - -Register a delegate for when a subscribed row is being updated. This event is only available if the row has a column with the `#[primary_key]` attribute. - -The delegate takes three arguments: - -- A [`{TABLE}`](#class-table.) instance with the old data of the updated row -- A [`{TABLE}`](#class-table.) instance with the new data of the updated row -- A [`ReducerEvent`](#class-reducerevent.), which contains the data of the reducer that updated the row. - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnUpdate += (User oldUser, User newUser, ReducerEvent reducerEvent) => { - Debug.Assert(oldUser.UserId == newUser.UserId, "Primary key never changes in an update"); - - Console.WriteLine($"User with ID {oldUser.UserId} had name changed "+ - $"from '{oldUser.Name}' to '{newUser.Name}' by reducer {reducerEvent.Reducer}."); -}; -``` - -## Observe and invoke reducers - -"Reducer" is SpacetimeDB's name for the stored procedures that run in modules inside the database. You can invoke reducers from a connected client SDK, and also receive information about which reducers are running. - -`spacetime generate` generates a class [`SpacetimeDB.Types.Reducer`](#class-reducer.) that contains methods and events for each reducer defined in a module. To invoke a reducer, use the method [`Reducer.{REDUCER}`](#static-method-reducerreducer.) generated for it. To receive a callback each time a reducer is invoked, use the static event [`Reducer.On{REDUCER}`](#static-event-reduceronreducer.). - -### Class `Reducer` - -```cs -namespace SpacetimeDB.Types { - -class Reducer {} - -} -``` - -This class contains a static method and event for each reducer defined in a module. - -#### Static Method `Reducer.{REDUCER}` - -```cs -namespace SpacetimeDB.Types { -class Reducer { - -/* void {REDUCER_NAME}(...ARGS...) */ - -} -} -``` - -For each reducer defined by a module, `spacetime generate` generates a static method which sends a request to the database to invoke that reducer. The generated function's name is the reducer's name converted to `PascalCase`. - -Reducers don't run immediately! They run as soon as the request reaches the database. Don't assume data inserted by a reducer will be available immediately after you call this method. - -For reducers which accept a `ReducerContext` as their first argument, the `ReducerContext` is not included in the generated function's argument list. - -For example, if we define a reducer in Rust as follows: - -```rust -#[spacetimedb(reducer)] -pub fn set_name( - ctx: ReducerContext, - user_id: u64, - name: String -) -> Result<(), Error>; -``` - -The following C# static method will be generated: - -```cs -namespace SpacetimeDB.Types { -class Reducer { - -public static void SendMessage(UInt64 userId, string name); - -} -} -``` - -#### Static Event `Reducer.On{REDUCER}` - -```cs -namespace SpacetimeDB.Types { -class Reducer { - -public delegate void /*{REDUCER}*/Handler(ReducerEvent reducerEvent, /* {ARGS...} */); - -public static event /*{REDUCER}*/Handler On/*{REDUCER}*/Event; - -} -} -``` - -For each reducer defined by a module, `spacetime generate` generates an event to run each time the reducer is invoked. The generated functions are named `on{REDUCER}Event`, where `{REDUCER}` is the reducer's name converted to `PascalCase`. - -The first argument to the event handler is an instance of [`SpacetimeDB.Types.ReducerEvent`](#class-reducerevent.) describing the invocation -- its timestamp, arguments, and whether it succeeded or failed. The remaining arguments are the arguments passed to the reducer. Reducers cannot have return values, so no return value information is included. - -For example, if we define a reducer in Rust as follows: - -```rust -#[spacetimedb(reducer)] -pub fn set_name( - ctx: ReducerContext, - user_id: u64, - name: String -) -> Result<(), Error>; -``` - -The following C# static method will be generated: - -```cs -namespace SpacetimeDB.Types { -class Reducer { - -public delegate void SetNameHandler( - ReducerEvent reducerEvent, - UInt64 userId, - string name -); -public static event SetNameHandler OnSetNameEvent; - -} -} -``` - -Which can be used as follows: - -```cs -/* initialize, wait for onSubscriptionApplied... */ - -Reducer.SetNameHandler += ( - ReducerEvent reducerEvent, - UInt64 userId, - string name -) => { - if (reducerEvent.Status == ClientApi.Event.Types.Status.Committed) { - Console.WriteLine($"User with id {userId} set name to {name}"); - } else if (reducerEvent.Status == ClientApi.Event.Types.Status.Failed) { - Console.WriteLine( - $"User with id {userId} failed to set name to {name}:" - + reducerEvent.ErrMessage - ); - } else if (reducerEvent.Status == ClientApi.Event.Types.Status.OutOfEnergy) { - Console.WriteLine( - $"User with id {userId} failed to set name to {name}:" - + "Invoker ran out of energy" - ); - } -}; -Reducer.SetName(USER_ID, NAME); -``` - -### Class `ReducerEvent` - -`spacetime generate` defines an class `ReducerEvent` containing an enum `ReducerType` with a variant for each reducer defined by a module. The variant's name will be the reducer's name converted to `PascalCase`. - -For example, the example project shown in the Rust Module quickstart will generate the following (abridged) code. - -```cs -namespace SpacetimeDB.Types { - -public enum ReducerType -{ - /* A member for each reducer in the module, with names converted to PascalCase */ - None, - SendMessage, - SetName, -} -public partial class SendMessageArgsStruct -{ - /* A member for each argument of the reducer SendMessage, with names converted to PascalCase. */ - public string Text; -} -public partial class SetNameArgsStruct -{ - /* A member for each argument of the reducer SetName, with names converted to PascalCase. */ - public string Name; -} -public partial class ReducerEvent : ReducerEventBase { - // Which reducer was invoked - public ReducerType Reducer { get; } - // If event.Reducer == ReducerType.SendMessage, the arguments - // sent to the SendMessage reducer. Otherwise, accesses will - // throw a runtime error. - public SendMessageArgsStruct SendMessageArgs { get; } - // If event.Reducer == ReducerType.SetName, the arguments - // passed to the SetName reducer. Otherwise, accesses will - // throw a runtime error. - public SetNameArgsStruct SetNameArgs { get; } - - /* Additional information, present on any ReducerEvent */ - // The name of the reducer. - public string ReducerName { get; } - // The timestamp of the reducer invocation inside the database. - public ulong Timestamp { get; } - // The identity of the client that invoked the reducer. - public SpacetimeDB.Identity Identity { get; } - // Whether the reducer succeeded, failed, or ran out of energy. - public ClientApi.Event.Types.Status Status { get; } - // If event.Status == Status.Failed, the error message returned from inside the module. - public string ErrMessage { get; } -} - -} -``` - -#### Enum `Status` - -```cs -namespace ClientApi { -public sealed partial class Event { -public static partial class Types { - -public enum Status { - Committed = 0, - Failed = 1, - OutOfEnergy = 2, -} - -} -} -} -``` - -An enum whose variants represent possible reducer completion statuses of a reducer invocation. - -##### Variant `Status.Committed` - -The reducer finished successfully, and its row changes were committed to the database. - -##### Variant `Status.Failed` - -The reducer failed, either by panicking or returning a `Err`. - -##### Variant `Status.OutOfEnergy` - -The reducer was canceled because the module owner had insufficient energy to allow it to run to completion. - -## Identity management - -### Class `AuthToken` - -The AuthToken helper class handles creating and saving SpacetimeDB identity tokens in the filesystem. - -#### Static Method `AuthToken.Init` - -```cs -namespace SpacetimeDB { - -class AuthToken { - public static void Init( - string configFolder = ".spacetime_csharp_sdk", - string configFile = "settings.ini", - string? configRoot = null - ); -} - -} -``` - -Creates a file `$"{configRoot}/{configFolder}/{configFile}"` to store tokens. -If no arguments are passed, the default is `"%HOME%/.spacetime_csharp_sdk/settings.ini"`. - -| Argument | Type | Meaning | -| -------------- | -------- | ---------------------------------------------------------------------------------- | -| `configFolder` | `string` | The folder to store the config file in. Default is `"spacetime_csharp_sdk"`. | -| `configFile` | `string` | The name of the config file. Default is `"settings.ini"`. | -| `configRoot` | `string` | The root folder to store the config file in. Default is the user's home directory. | - -#### Static Property `AuthToken.Token` - -```cs -namespace SpacetimeDB { - -class AuthToken { - public static string? Token { get; } -} - -} -``` - -The auth token stored on the filesystem, if one exists. - -#### Static Method `AuthToken.SaveToken` - -```cs -namespace SpacetimeDB { - -class AuthToken { - public static void SaveToken(string token); -} - -} -``` - -Save a token to the filesystem. - -### Class `Identity` - -```cs -namespace SpacetimeDB -{ - public struct Identity : IEquatable - { - public byte[] Bytes { get; } - public static Identity From(byte[] bytes); - public bool Equals(Identity other); - public static bool operator ==(Identity a, Identity b); - public static bool operator !=(Identity a, Identity b); - } -} -``` - -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. - -### Class `Identity` -```cs -namespace SpacetimeDB -{ - public struct Address : IEquatable
- { - public byte[] Bytes { get; } - public static Address? From(byte[] bytes); - public bool Equals(Address other); - public static bool operator ==(Address a, Address b); - public static bool operator !=(Address a, Address b); - } -} -``` - -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity.). - -## Customizing logging - -The SpacetimeDB C# SDK performs internal logging. Instances of [`ISpacetimeDBLogger`](#interface-ispacetimedblogger.) can be passed to [`SpacetimeDBClient.CreateInstance`](#static-method-spacetimedbclientcreateinstance.) to customize how SDK logs are delivered to your application. - -This is set up automatically for you if you use Unity-- adding a [`NetworkManager`](#class-networkmanager.) component to your unity scene will automatically initialize the `SpacetimeDBClient` with a [`UnityDebugLogger`](#class-unitydebuglogger.). - -Outside of unity, all you need to do is the following: - -```cs -using SpacetimeDB; -using SpacetimeDB.Types; -SpacetimeDBClient.CreateInstance(new ConsoleLogger()); -``` - -### Interface `ISpacetimeDBLogger` - -```cs -namespace SpacetimeDB -{ - -public interface ISpacetimeDBLogger -{ - void Log(string message); - void LogError(string message); - void LogWarning(string message); - void LogException(Exception e); -} - -} -``` - -This interface provides methods that are invoked when the SpacetimeDB C# SDK needs to log at various log levels. You can create custom implementations if needed to integrate with existing logging solutions. - -### Class `ConsoleLogger` - -```cs -namespace SpacetimeDB { - -public class ConsoleLogger : ISpacetimeDBLogger {} - -} -``` - -An `ISpacetimeDBLogger` implementation for regular .NET applications, using `Console.Write` when logs are received. - -### Class `UnityDebugLogger` - -```cs -namespace SpacetimeDB { - -public class UnityDebugLogger : ISpacetimeDBLogger {} - -} -``` - -An `ISpacetimeDBLogger` implementation for Unity, using the Unity `Debug.Log` api. diff --git a/Writerside2/topics/sdks/python/python_index.md b/Writerside2/topics/sdks/python/python_index.md deleted file mode 100644 index a87d8ac5..00000000 --- a/Writerside2/topics/sdks/python/python_index.md +++ /dev/null @@ -1,552 +0,0 @@ -# The SpacetimeDB Python client SDK - -The SpacetimeDB client SDK for Python contains all the tools you need to build native clients for SpacetimeDB modules using Python. - -## Install the SDK - -Use pip to install the SDK: - -```bash -pip install spacetimedb-sdk -``` - -## Generate module bindings - -Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the Python interface files using the Spacetime CLI. From your project directory, run: - -```bash -mkdir -p module_bindings -spacetime generate --lang python \ - --out-dir module_bindings \ - --project-path PATH-TO-MODULE-DIRECTORY -``` - -Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. - -Import your bindings in your client's code: - -```python -import module_bindings -``` - -## Basic vs Async SpacetimeDB Client - -This SDK provides two different client modules for interacting with your SpacetimeDB module. - -The Basic client allows you to have control of the main loop of your application and you are responsible for regularly calling the client's `update` function. This is useful in settings like PyGame where you want to have full control of the main loop. - -The Async client has a run function that you call after you set up all your callbacks and it will take over the main loop and handle updating the client for you. With the async client, you can have a regular "tick" function by using the `schedule_event` function. - -## Common Client Reference - -The following functions and types are used in both the Basic and Async clients. - -### API at a glance - -| Definition | Description | -|---------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------| -| Type [`Identity`](#type-identity.) | A unique public identifier for a client. | -| Type [`Address`](#type-address.) | An opaque identifier for differentiating connections by the same `Identity`. | -| Type [`ReducerEvent`](#type-reducerevent.) | `class` containing information about the reducer that triggered a row update event. | -| Type [`module_bindings::{TABLE}`](#type-table.) | Autogenerated `class` type for a table, holding one row. | -| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](#method-filter_by_column.) | Autogenerated method to iterate over or seek subscribed rows where a column matches a value. | -| Method [`module_bindings::{TABLE}::iter`](#method-iter.) | Autogenerated method to iterate over all subscribed rows. | -| Method [`module_bindings::{TABLE}::register_row_update`](#method-register_row_update.) | Autogenerated method to register a callback that fires when a row changes. | -| Function [`module_bindings::{REDUCER_NAME}::{REDUCER_NAME}`](#function-reducer.) | Autogenerated function to invoke a reducer. | -| Function [`module_bindings::{REDUCER_NAME}::register_on_{REDUCER_NAME}`](#function-register_on_reducer.) | Autogenerated function to register a callback to run whenever the reducer is invoked. | - -### Type `Identity` - -```python -class Identity: - @staticmethod - def from_string(string) - - @staticmethod - def from_bytes(data) - - def __str__(self) - - def __eq__(self, other) -``` - -| Member | Args | Meaning | -| ------------- | ---------- | ------------------------------------ | -| `from_string` | `str` | Create an Identity from a hex string | -| `from_bytes` | `bytes` | Create an Identity from raw bytes | -| `__str__` | `None` | Convert the Identity to a hex string | -| `__eq__` | `Identity` | Compare two Identities for equality | - -A unique public identifier for a user of a database. - -### Type `Address` - -```python -class Address: - @staticmethod - def from_string(string) - - @staticmethod - def from_bytes(data) - - def __str__(self) - - def __eq__(self, other) -``` - -| Member | Type | Meaning | -|---------------|-----------|-------------------------------------| -| `from_string` | `str` | Create an Address from a hex string | -| `from_bytes` | `bytes` | Create an Address from raw bytes | -| `__str__` | `None` | Convert the Address to a hex string | -| `__eq__` | `Address` | Compare two Identities for equality | - -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity.). - -### Type `ReducerEvent` - -```python -class ReducerEvent: - def __init__(self, caller_identity, reducer_name, status, message, args): - self.caller_identity = caller_identity - self.reducer_name = reducer_name - self.status = status - self.message = message - self.args = args -``` - -| Member | Type | Meaning | -|-------------------|---------------------|------------------------------------------------------------------------------------| -| `caller_identity` | `Identity` | The identity of the user who invoked the reducer | -| `caller_address` | `Optional[Address]` | The address of the user who invoked the reducer, or `None` for scheduled reducers. | -| `reducer_name` | `str` | The name of the reducer that was invoked | -| `status` | `str` | The status of the reducer invocation ("committed", "failed", "outofenergy") | -| `message` | `str` | The message returned by the reducer if it fails | -| `args` | `List[str]` | The arguments passed to the reducer | - -This class contains the information about a reducer event to be passed to row update callbacks. - -### Type `{TABLE}` - -```python -class TABLE: - is_table_class = True - - primary_key = "identity" - - @classmethod - def register_row_update(cls, callback: Callable[[str,TABLE,TABLE,ReducerEvent], None]) - - @classmethod - def iter(cls) -> Iterator[User] - - @classmethod - def filter_by_COLUMN_NAME(cls, COLUMN_VALUE) -> TABLE -``` - -This class is autogenerated for each table in your module. It contains methods for filtering and iterating over subscribed rows. - -### Method `filter_by_{COLUMN}` - -```python -def filter_by_COLUMN(self, COLUMN_VALUE) -> TABLE -``` - -| Argument | Type | Meaning | -| -------------- | ------------- | ---------------------- | -| `column_value` | `COLUMN_TYPE` | The value to filter by | - -For each column of a table, `spacetime generate` generates a `classmethod` on the [table class](#type-table.) to filter or seek 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 depends on the column's attributes: - -- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filter_by` method returns a `{TABLE}` or None, where `{TABLE}` is the [table struct](#type-table.). -- For non-unique columns, the `filter_by` method returns an `Iterator` that can be used in a `for` loop. - -### Method `iter` - -```python -def iter(self) -> Iterator[TABLE] -``` - -Iterate over all the subscribed rows in the table. - -### Method `register_row_update` - -```python -def register_row_update(self, callback: Callable[[str,TABLE,TABLE,ReducerEvent], None]) -``` - -| Argument | Type | Meaning | -| ---------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | -| `callback` | `Callable[[str,TABLE,TABLE,ReducerEvent]` | Callback to be invoked when a row is updated (Args: row_op, old_value, new_value, reducer_event) | - -Register a callback function to be executed when a row is updated. Callback arguments are: - -- `row_op`: The type of row update event. One of `"insert"`, `"delete"`, or `"update"`. -- `old_value`: The previous value of the row, `None` if the row was inserted. -- `new_value`: The new value of the row, `None` if the row was deleted. -- `reducer_event`: The [`ReducerEvent`](#type-reducerevent.) that caused the row update, or `None` if the row was updated as a result of a subscription change. - -### Function `{REDUCER_NAME}` - -```python -def {REDUCER_NAME}(arg1, arg2) -``` - -This function is autogenerated for each reducer in your module. It is used to invoke the reducer. The arguments match the arguments defined in the reducer's `#[reducer]` attribute. - -### Function `register_on_{REDUCER_NAME}` - -```python -def register_on_{REDUCER_NAME}(callback: Callable[[Identity, Optional[Address], str, str, ARG1_TYPE, ARG1_TYPE], None]) -``` - -| Argument | Type | Meaning | -| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -| `callback` | `Callable[[Identity, str, str, ARG1_TYPE, ARG1_TYPE], None]` | Callback to be invoked when the reducer is invoked (Args: caller_identity, status, message, args) | - -Register a callback function to be executed when the reducer is invoked. Callback arguments are: - -- `caller_identity`: The identity of the user who invoked the reducer. -- `caller_address`: The address of the user who invoked the reducer, or `None` for scheduled reducers. -- `status`: The status of the reducer invocation ("committed", "failed", "outofenergy"). -- `message`: The message returned by the reducer if it fails. -- `args`: Variable number of arguments passed to the reducer. - -## Async Client Reference - -### API at a glance - -| Definition | Description | -| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| Function [`SpacetimeDBAsyncClient::run`](#function-run.) | Run the client. This function will not return until the client is closed. | -| Function [`SpacetimeDBAsyncClient::subscribe`](#function-subscribe.) | Subscribe to receive data and transaction updates for the provided queries. | -| Function [`SpacetimeDBAsyncClient::register_on_subscription_applied`](#function-register_on_subscription_applied.) | Register a callback when the local cache is updated as a result of a change to the subscription queries. | -| Function [`SpacetimeDBAsyncClient::force_close`](#function-force_close.) | Signal the client to stop processing events and close the connection to the server. | -| Function [`SpacetimeDBAsyncClient::schedule_event`](#function-schedule_event.) | Schedule an event to be fired after a delay | - -### Function `run` - -```python -async def run( - self, - auth_token, - host, - address_or_name, - ssl_enabled, - on_connect, - subscription_queries=[], - ) -``` - -Run the client. This function will not return until the client is closed. - -| Argument | Type | Meaning | -| ---------------------- | --------------------------------- | -------------------------------------------------------------- | -| `auth_token` | `str` | Auth token to authenticate the user. (None if new user) | -| `host` | `str` | Hostname of SpacetimeDB server | -| `address_or_name` | `&str` | Name or address of the module. | -| `ssl_enabled` | `bool` | Whether to use SSL when connecting to the server. | -| `on_connect` | `Callable[[str, Identity], None]` | Callback to be invoked when the client connects to the server. | -| `subscription_queries` | `List[str]` | List of queries to subscribe to. | - -If `auth_token` is not None, they will be passed to the new connection to identify and authenticate the user. Otherwise, a new Identity and auth token will be generated by the server. An optional [local_config](#local_config.) module can be used to store the user's auth token to local storage. - -If you are connecting to SpacetimeDB Cloud `testnet` the host should be `testnet.spacetimedb.com` and `ssl_enabled` should be `True`. If you are connecting to SpacetimeDB Standalone locally, the host should be `localhost:3000` and `ssl_enabled` should be `False`. For instructions on how to deploy to these environments, see the [Deployment Section](testnet.) - -```python -asyncio.run( - spacetime_client.run( - AUTH_TOKEN, - "localhost:3000", - "my-module-name", - False, - on_connect, - ["SELECT * FROM User", "SELECT * FROM Message"], - ) -) -``` - -### Function `subscribe` - -```rust -def subscribe(self, queries: List[str]) -``` - -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -| Argument | Type | Meaning | -| --------- | ----------- | ---------------------------- | -| `queries` | `List[str]` | SQL queries to subscribe to. | - -The `queries` should be a slice of strings representing SQL queries. - -A new call to `subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache. Row update events will be dispatched for any inserts and deletes that occur as a result of the new queries. For these events, the [`ReducerEvent`](#type-reducerevent.) argument will be `None`. - -This should be called before the async client is started with [`run`](#function-run.). - -```python -spacetime_client.subscribe(["SELECT * FROM User;", "SELECT * FROM Message;"]) -``` - -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -### Function `register_on_subscription_applied` - -```python -def register_on_subscription_applied(self, callback) -``` - -Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. - -| Argument | Type | Meaning | -| ---------- | -------------------- | ------------------------------------------------------ | -| `callback` | `Callable[[], None]` | Callback to be invoked when subscriptions are applied. | - -The callback will be invoked after a successful [`subscribe`](#function-subscribe.) call when the initial set of matching rows becomes available. - -```python -spacetime_client.register_on_subscription_applied(on_subscription_applied) -``` - -### Function `force_close` - -```python -def force_close(self) -) -``` - -Signal the client to stop processing events and close the connection to the server. - -```python -spacetime_client.force_close() -``` - -### Function `schedule_event` - -```python -def schedule_event(self, delay_secs, callback, *args) -``` - -Schedule an event to be fired after a delay - -To create a repeating event, call schedule_event() again from within the callback function. - -| Argument | Type | Meaning | -| ------------ | -------------------- | -------------------------------------------------------------- | -| `delay_secs` | `float` | number of seconds to wait before firing the event | -| `callback` | `Callable[[], None]` | Callback to be invoked when the event fires. | -| `args` | `*args` | Variable number of arguments to pass to the callback function. | - -```python -def application_tick(): - # ... do some work - - spacetime_client.schedule_event(0.1, application_tick) - -spacetime_client.schedule_event(0.1, application_tick) -``` - -## Basic Client Reference - -### API at a glance - -| Definition | Description | -|------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| -| Function [`SpacetimeDBClient::init`](#function-init.) | Create a network manager instance. | -| Function [`SpacetimeDBClient::subscribe`](#function-subscribe.) | Subscribe to receive data and transaction updates for the provided queries. | -| Function [`SpacetimeDBClient::register_on_event`](#function-register_on_event.) | Register a callback function to handle transaction update events. | -| Function [`SpacetimeDBClient::unregister_on_event`](#function-unregister_on_event.) | Unregister a callback function that was previously registered using `register_on_event`. | -| Function [`SpacetimeDBClient::register_on_subscription_applied`](#function-register_on_subscription_applied.) | Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. | -| Function [`SpacetimeDBClient::unregister_on_subscription_applied`](#function-unregister_on_subscription_applied.) | Unregister a callback function from the subscription update event. | -| Function [`SpacetimeDBClient::update`](#function-update.) | Process all pending incoming messages from the SpacetimeDB module. | -| Function [`SpacetimeDBClient::close`](#function-close.) | Close the WebSocket connection. | -| Type [`TransactionUpdateMessage`](#type-transactionupdatemessage.) | Represents a transaction update message. | - -### Function `init` - -```python -@classmethod -def init( - auth_token: str, - host: str, - address_or_name: str, - ssl_enabled: bool, - autogen_package: module, - on_connect: Callable[[], NoneType] = None, - on_disconnect: Callable[[str], NoneType] = None, - on_identity: Callable[[str, Identity, Address], NoneType] = None, - on_error: Callable[[str], NoneType] = None -) -``` - -Create a network manager instance. - -| Argument | Type | Meaning | -|-------------------|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `auth_token` | `str` | This is the token generated by SpacetimeDB that matches the user's identity. If None, token will be generated | -| `host` | `str` | Hostname:port for SpacetimeDB connection | -| `address_or_name` | `str` | The name or address of the database to connect to | -| `ssl_enabled` | `bool` | Whether to use SSL when connecting to the server. | -| `autogen_package` | `ModuleType` | Python package where SpacetimeDB module generated files are located. | -| `on_connect` | `Callable[[], None]` | Optional callback called when a connection is made to the SpacetimeDB module. | -| `on_disconnect` | `Callable[[str], None]` | Optional callback called when the Python client is disconnected from the SpacetimeDB module. The argument is the close message. | -| `on_identity` | `Callable[[str, Identity, Address], None]` | Called when the user identity is recieved from SpacetimeDB. First argument is the auth token used to login in future sessions. Third argument is the client connection's [`Address`](#type-address.). | -| `on_error` | `Callable[[str], None]` | Optional callback called when the Python client connection encounters an error. The argument is the error message. | - -This function creates a new SpacetimeDBClient instance. It should be called before any other functions in the SpacetimeDBClient class. This init will call connect for you. - -```python -SpacetimeDBClient.init(autogen, on_connect=self.on_connect) -``` - -### Function `subscribe` - -```python -def subscribe(queries: List[str]) -``` - -Subscribe to receive data and transaction updates for the provided queries. - -| Argument | Type | Meaning | -| --------- | ----------- | -------------------------------------------------------------------------------------------------------- | -| `queries` | `List[str]` | A list of queries to subscribe to. Each query is a string representing an sql formatted query statement. | - -This function sends a subscription request to the SpacetimeDB module, indicating that the client wants to receive data and transaction updates related to the specified queries. - -```python -queries = ["SELECT * FROM table1", "SELECT * FROM table2 WHERE col2 = 0"] -SpacetimeDBClient.instance.subscribe(queries) -``` - -### Function `register_on_event` - -```python -def register_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) -``` - -Register a callback function to handle transaction update events. - -| Argument | Type | Meaning | -| ---------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `callback` | `Callable[[TransactionUpdateMessage], None]` | A callback function that takes a single argument of type `TransactionUpdateMessage`. This function will be invoked with a `TransactionUpdateMessage` instance containing information about the transaction update event. | - -This function registers a callback function that will be called when a reducer modifies a table matching any of the subscribed queries or if a reducer called by this Python client encounters a failure. - -```python -def handle_event(transaction_update): - # Code to handle the transaction update event - -SpacetimeDBClient.instance.register_on_event(handle_event) -``` - -### Function `unregister_on_event` - -```python -def unregister_on_event(callback: Callable[[TransactionUpdateMessage], NoneType]) -``` - -Unregister a callback function that was previously registered using `register_on_event`. - -| Argument | Type | Meaning | -| ---------- | -------------------------------------------- | ------------------------------------ | -| `callback` | `Callable[[TransactionUpdateMessage], None]` | The callback function to unregister. | - -```python -SpacetimeDBClient.instance.unregister_on_event(handle_event) -``` - -### Function `register_on_subscription_applied` - -```python -def register_on_subscription_applied(callback: Callable[[], NoneType]) -``` - -Register a callback function to be executed when the local cache is updated as a result of a change to the subscription queries. - -| Argument | Type | Meaning | -| ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `callback` | `Callable[[], None]` | A callback function that will be invoked on each subscription update. The callback function should not accept any arguments and should not return any value. | - -```python -def subscription_callback(): - # Code to be executed on each subscription update - -SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) -``` - -### Function `unregister_on_subscription_applied` - -```python -def unregister_on_subscription_applied(callback: Callable[[], NoneType]) -``` - -Unregister a callback function from the subscription update event. - -| Argument | Type | Meaning | -| ---------- | -------------------- | -------------------------------------------------------------------------------------------------------- | -| `callback` | `Callable[[], None]` | A callback function that was previously registered with the `register_on_subscription_applied` function. | - -```python -def subscription_callback(): - # Code to be executed on each subscription update - -SpacetimeDBClient.instance.register_on_subscription_applied(subscription_callback) -``` - -### Function `update` - -```python -def update() -``` - -Process all pending incoming messages from the SpacetimeDB module. - -This function must be called on a regular interval in the main loop to process incoming messages. - -```python -while True: - SpacetimeDBClient.instance.update() # Call the update function in a loop to process incoming messages - # Additional logic or code can be added here -``` - -### Function `close` - -```python -def close() -``` - -Close the WebSocket connection. - -This function closes the WebSocket connection to the SpacetimeDB module. - -```python -SpacetimeDBClient.instance.close() -``` - -### Type `TransactionUpdateMessage` - -```python -class TransactionUpdateMessage: - def __init__( - self, - caller_identity: Identity, - status: str, - message: str, - reducer_name: str, - args: Dict - ) -``` - -| Member | Args | Meaning | -| ----------------- | ---------- | ------------------------------------------------- | -| `caller_identity` | `Identity` | The identity of the caller. | -| `status` | `str` | The status of the transaction. | -| `message` | `str` | A message associated with the transaction update. | -| `reducer_name` | `str` | The reducer used for the transaction. | -| `args` | `Dict` | Additional arguments for the transaction. | - -Represents a transaction update message. Used in on_event callbacks. - -For more details, see [`register_on_event`](#function-register_on_event.). diff --git a/Writerside2/topics/sdks/python/python_quickstart.md b/Writerside2/topics/sdks/python/python_quickstart.md deleted file mode 100644 index fe6dbc22..00000000 --- a/Writerside2/topics/sdks/python/python_quickstart.md +++ /dev/null @@ -1,379 +0,0 @@ -# Python Client SDK Quick Start - -In this guide, we'll show you how to get up and running with a simple SpacetimDB app with a client written in Python. - -We'll implement a command-line client for the module created in our [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart6.) guides. Make sure you follow one of these guides before you start on this one. - -## Install the SpacetimeDB SDK Python Package - -1. Run pip install - -```bash -pip install spacetimedb_sdk -``` - -## Project structure - -Enter the directory `quickstart-chat` you created in the Rust or C# Module Quickstart guides and create a `client` folder: - -```bash -cd quickstart-chat -mkdir client -``` - -## Create the Python main file - -Create a file called `main.py` in the `client` and open it in your favorite editor. We prefer [VS Code](https://code.visualstudio.com/). - -## Add imports - -We need to add several imports for this quickstart: - -- [`asyncio`](https://docs.python.org/3/library/asyncio.html) is required to run the async code in the SDK. -- [`multiprocessing.Queue`](https://docs.python.org/3/library/multiprocessing.html) allows us to pass our input to the async code, which we will run in a separate thread. -- [`threading`](https://docs.python.org/3/library/threading.html) allows us to spawn our async code in a separate thread so the main thread can run the input loop. - -- `spacetimedb_sdk.spacetimedb_async_client.SpacetimeDBAsyncClient` is the async wrapper around the SpacetimeDB client which we use to interact with our SpacetimeDB module. -- `spacetimedb_sdk.local_config` is an optional helper module to load the auth token from local storage. - -```python -import asyncio -from multiprocessing import Queue -import threading - -from spacetimedb_sdk.spacetimedb_async_client import SpacetimeDBAsyncClient -import spacetimedb_sdk.local_config as local_config -``` - -## Generate your module types - -The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. - -In your `client` directory, run: - -```bash -mkdir -p module_bindings -spacetime generate --lang python --out-dir module_bindings --project-path ../server -``` - -Take a look inside `client/module_bindings`. The CLI should have generated five files: - -``` -module_bindings -+-- message.py -+-- send_message_reducer.py -+-- set_name_reducer.py -+-- user.py -``` - -Now we import these types by adding the following lines to `main.py`: - -```python -import module_bindings -from module_bindings.user import User -from module_bindings.message import Message -import module_bindings.send_message_reducer as send_message_reducer -import module_bindings.set_name_reducer as set_name_reducer -``` - -## Global variables - -Next we will add our global `input_queue` and `local_identity` variables which we will explain later when they are used. - -```python -input_queue = Queue() -local_identity = None -``` - -## Define 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 four things: - -1. Init the optional local config module. The first parameter is the directory name to be created in the user home directory. -1. Create our async SpacetimeDB client. -1. Register our callbacks. -1. Start the async client in a thread. -1. Run a loop to read user input and send it to a repeating event in the async client. -1. When the user exits, stop the async client and exit the program. - -```python -if __name__ == "__main__": - local_config.init(".spacetimedb-python-quickstart") - - spacetime_client = SpacetimeDBAsyncClient(module_bindings) - - register_callbacks(spacetime_client) - - thread = threading.Thread(target=run_client, args=(spacetime_client,)) - thread.start() - - input_loop() - - spacetime_client.force_close() - thread.join() -``` - -## Register callbacks - -We need to handle several sorts of events: - -1. OnSubscriptionApplied is a special callback that is executed when the local client cache is populated. We will talk more about this later. -2. When a new user joins or a user is updated, we'll print an appropriate message. -3. When we receive a new message, we'll print it. -4. If the server rejects our attempt to set our name, we'll print an error. -5. If the server rejects a message we send, we'll print an error. -6. We use the `schedule_event` function to register a callback to be executed after 100ms. This callback will check the input queue for any user input and execute the appropriate command. - -Because python requires functions to be defined before they're used, the following code must be added to `main.py` before main block: - -```python -def register_callbacks(spacetime_client): - spacetime_client.client.register_on_subscription_applied(on_subscription_applied) - - User.register_row_update(on_user_row_update) - Message.register_row_update(on_message_row_update) - - set_name_reducer.register_on_set_name(on_set_name_reducer) - send_message_reducer.register_on_send_message(on_send_message_reducer) - - spacetime_client.schedule_event(0.1, check_commands) -``` - -### Handling User row updates - -For each table, we can register a row update callback to be run whenever a subscribed row is inserted, updated or deleted. We register these callbacks using the `register_row_update` methods that are generated automatically 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::row_update` callbacks may be invoked with users who are offline. We'll only notify about online users. - -We are also going to check for updates to the user row. This can happen for three reasons: - -1. They've set their name using the `set_name` reducer. -2. They're an existing user re-connecting, so their `online` has been set to `true`. -3. They've disconnected, so their `online` has been set to `false`. - -We'll print an appropriate message in each of these cases. - -`row_update` callbacks take four arguments: the row operation ("insert", "update", or "delete"), the old row if it existed, the new or updated row, and a `ReducerEvent`. This will `None` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an class that contains information about the reducer that triggered this row update event. - -Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `user_name_or_identity` handle this. - -Add these functions before the `register_callbacks` function: - -```python -def user_name_or_identity(user): - if user.name: - return user.name - else: - return (str(user.identity))[:8] - -def on_user_row_update(row_op, user_old, user, reducer_event): - if row_op == "insert": - if user.online: - print(f"User {user_name_or_identity(user)} connected.") - elif row_op == "update": - if user_old.online and not user.online: - print(f"User {user_name_or_identity(user)} disconnected.") - elif not user_old.online and user.online: - print(f"User {user_name_or_identity(user)} connected.") - - if user_old.name != user.name: - print( - f"User {user_name_or_identity(user_old)} renamed to {user_name_or_identity(user)}." - ) -``` - -### 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 `on_message_row_update` callback will check if its `reducer_event` argument is not `None`, and only print in that case. - -To find the `User` based on the message's `sender` identity, we'll use `User::filter_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filter_by_identity` accepts a `bytes`, rather than an `&Identity`. The `sender` identity stored in the message is also a `bytes`, not an `Identity`, so we can just pass it to the filter method. - -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`. - -Add these functions before the `register_callbacks` function: - -```python -def on_message_row_update(row_op, message_old, message, reducer_event): - if reducer_event is not None and row_op == "insert": - print_message(message) - -def print_message(message): - user = User.filter_by_identity(message.sender) - user_name = "unknown" - if user is not None: - user_name = user_name_or_identity(user) - - print(f"{user_name}: {message.text}") -``` - -### Warn if our name was rejected - -We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `register_on_` method, which is automatically implemented for each reducer by `spacetime generate`. - -Each reducer callback takes four fixed arguments: - -1. The `Identity` of the client who requested the reducer invocation. -2. The `Address` of the client who requested the reducer invocation, or `None` for scheduled reducers. -3. The `Status` of the reducer run, one of `committed`, `failed` or `outofenergy`. -4. The `Message` returned by the reducer in error cases, or `None` if the reducer succeeded. - -It also takes a variable number of arguments which match the calling arguments of the reducer. - -These callbacks will be invoked in one of two cases: - -1. If the reducer was successful and altered any of our subscribed rows. -2. If we requested an invocation which failed. - -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 `on_set_name_reducer` as a callback which checks if the reducer failed, and if it did, prints an error message including the rejected name. - -We'll test both that our identity matches the sender and that the status is `failed`, even though the latter implies the former, for demonstration purposes. - -Add this function before the `register_callbacks` function: - -```python -def on_set_name_reducer(sender_id, sender_address, status, message, name): - if sender_id == local_identity: - if status == "failed": - print(f"Failed to set name: {message}") -``` - -### 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. - -Add this function before the `register_callbacks` function: - -```python -def on_send_message_reducer(sender_id, sender_address, status, message, msg): - if sender_id == local_identity: - if status == "failed": - print(f"Failed to send message: {message}") -``` - -### OnSubscriptionApplied callback - -This callback fires after the client cache is updated as a result in a change to the client subscription. This happens after connect and if after calling `subscribe` to modify the subscription. - -In this case, we want to print all the existing messages when the subscription is applied. `print_messages_in_order` iterates over all the `Message`s we've received, sorts them, and then prints them. `Message.iter()` is generated for all table types, and returns an iterator over all the messages in the client's cache. - -Add these functions before the `register_callbacks` function: - -```python -def print_messages_in_order(): - all_messages = sorted(Message.iter(), key=lambda x: x.sent) - for entry in all_messages: - print(f"{user_name_or_identity(User.filter_by_identity(entry.sender))}: {entry.text}") - -def on_subscription_applied(): - print(f"\nSYSTEM: Connected.") - print_messages_in_order() -``` - -### Check commands repeating event - -We'll use a repeating event to check the user input queue every 100ms. If there's a command in the queue, we'll execute it. If not, we'll just keep waiting. Notice that at the end of the function we call `schedule_event` again to so the event will repeat. - -If the command is to send a message, we'll call the `send_message` reducer. If the command is to set our name, we'll call the `set_name` reducer. - -Add these functions before the `register_callbacks` function: - -```python -def check_commands(): - global input_queue - - if not input_queue.empty(): - choice = input_queue.get() - if choice[0] == "name": - set_name_reducer.set_name(choice[1]) - else: - send_message_reducer.send_message(choice[1]) - - spacetime_client.schedule_event(0.1, check_commands) -``` - -### OnConnect callback - -This callback fires after the client connects to the server. We'll use it to save our credentials to a file so that we can re-authenticate as the same user next time we connect. - -The `on_connect` callback takes three arguments: - -1. The `Auth Token` is the equivalent of your private key. This is the only way to authenticate with the SpacetimeDB module as this user. -2. The `Identity` is the equivalent of your public key. This is used to uniquely identify this user and will be sent to other clients. We store this in a global variable so we can use it to identify that a given message or transaction was sent by us. -3. The `Address` is an opaque identifier 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. - -To store our auth token, we use the optional component `local_config`, which provides a simple interface for storing and retrieving a single `Identity` from a file. We'll use the `local_config::set_string` method to store the auth token. Other projects might want to associate this token with some other identifier such as an email address or Steam ID. - -The `on_connect` callback is passed to the client connect function so it just needs to be defined before the `run_client` described next. - -```python -def on_connect(auth_token, identity): - global local_identity - local_identity = identity - - local_config.set_string("auth_token", auth_token) -``` - -## Async client thread - -We are going to write a function that starts the async client, which will be executed on a separate thread. - -```python -def run_client(spacetime_client): - asyncio.run( - spacetime_client.run( - local_config.get_string("auth_token"), - "localhost:3000", - "chat", - False, - on_connect, - ["SELECT * FROM User", "SELECT * FROM Message"], - ) - ) -``` - -## Input loop - -Finally, we need a function to be executed on the main loop which listens for user input and adds it to the queue. - -```python -def input_loop(): - global input_queue - - while True: - user_input = input() - if len(user_input) == 0: - return - elif user_input.startswith("/name "): - input_queue.put(("name", user_input[6:])) - else: - input_queue.put(("message", user_input)) -``` - -## Run the client - -Make sure your module from the Rust or C# module quickstart is published. If you used a different module name than `chat`, you will need to update the `connect` call in the `run_client` function. - -Run the client: - -```bash -python main.py -``` - -If you want to connect another client, you can use the --client command line option, which is built into the local_config module. This will create different settings file for the new client's auth token. - -```bash -python main.py --client 2 -``` - -## Next steps - -Congratulations! You've built a simple chat app with a Python client. You can now use this as a starting point for your own SpacetimeDB apps. - -For a more complex example of the Spacetime Python SDK, check out our [AI Agent](https://github.com/clockworklabs/spacetime-mud/tree/main/ai-agent-python-client) for the [Spacetime Multi-User Dungeon](https://github.com/clockworklabs/spacetime-mud). The AI Agent uses the OpenAI API to create dynamic content on command. diff --git a/Writerside2/topics/sdks/rust/sdks_rust_index.md b/Writerside2/topics/sdks/rust/sdks_rust_index.md deleted file mode 100644 index 239cabff..00000000 --- a/Writerside2/topics/sdks/rust/sdks_rust_index.md +++ /dev/null @@ -1,1183 +0,0 @@ -# The SpacetimeDB Rust client SDK - -The SpacetimeDB client SDK for Rust contains all the tools you need to build native clients for SpacetimeDB modules using Rust. - -## Install the SDK - -First, create a new project using `cargo new` and add the SpacetimeDB SDK to your dependencies: - -```bash -cargo add spacetimedb -``` - -## Generate module bindings - -Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's `src` directory and generate the Rust interface files using the Spacetime CLI. From your project directory, run: - -```bash -mkdir -p src/module_bindings -spacetime generate --lang rust \ - --out-dir src/module_bindings \ - --project-path PATH-TO-MODULE-DIRECTORY -``` - -Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. - -Declare a `mod` for the bindings in your client's `src/main.rs`: - -```rust -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`](rust_#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`](rust_#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`](rust_#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`](rust_#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}`](rust_#type-table.) | Autogenerated `struct` type for a table, holding one row. | -| Method [`module_bindings::{TABLE}::filter_by_{COLUMN}`](rust_#method-filter_by_column.) | Autogenerated method to iterate over or seek subscribed rows where a 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`](rust_#method-iter.) | Iterate over all subscribed rows. | -| Method [`spacetimedb_sdk::table::TableType::filter`](rust_#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`](rust_#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}`](rust_#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` - -```rust -module_bindings::connect( - spacetimedb_uri: impl TryInto, - db_name: &str, - credentials: Option, -) -> anyhow::Result<()> -``` - -Connect to a database named `db_name` accessible over the internet at the URI `spacetimedb_uri`. - -| 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` - -```rust -spacetimedb_sdk::disconnect() -``` - -Gracefully close the current WebSocket connection. - -If there is no active connection, this operation does nothing. - -```rust -connect(SPACETIMEDB_URI, MODULE_NAME, credentials) - .expect("Connection failed"); - -run_app(); - -disconnect(); -``` - -### 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!" -``` - -### Function `once_on_disconnect` - -```rust -spacetimedb_sdk::once_on_disconnect( - callback: impl FnOnce() + Send + 'static, -) -> DisconnectCallbackId -``` - -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. - -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. - -```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. -``` - -### Function `remove_on_disconnect` - -```rust -spacetimedb_sdk::remove_on_disconnect( - id: DisconnectCallbackId, -) -``` - -Unregister a previously-registered [`on_disconnect`](#function-on_disconnect.) callback. - -| Argument | Type | Meaning | -| -------- | ---------------------- | ------------------------------------------ | -| `id` | `DisconnectCallbackId` | 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_disconnect(|| unreachable!()); - -remove_on_disconnect(id); - -disconnect(); - -// No `unreachable` panic. -``` - -## Subscribe to queries - -### Function `subscribe` - -```rust -spacetimedb_sdk::subscribe(queries: &[&str]) -> anyhow::Result<()> -``` - -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}`](rust_#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. - -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. - -```rust -subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) - .expect("Called `subscribe` before `connect`"); -``` - -### Function `subscribe_owned` - -```rust -spacetimedb_sdk::subscribe_owned(queries: Vec) -> 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. - -A new call to `subscribe_owned` (or [`subscribe`](rust_#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. - -`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. - -```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` - -```rust -spacetimedb_sdk::on_subscription_applied( - callback: impl FnMut() + Send + 'static, -) -> SubscriptionCallbackId -``` - -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`](rust_#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. -``` - -### Function `once_on_subscription_applied` - -```rust -spacetimedb_sdk::once_on_subscription_applied( - callback: impl FnOnce() + Send + 'static, -) -> SubscriptionCallbackId -``` - -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`](rust_#function-subscribe.) or [`subscribe_owned`](#function-subscribe_owned.) call when the initial set of matching rows becomes available. - -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. - -```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. -``` - -### 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)); - -// Will print "Subscription applied!" - -remove_on_subscription_applied(id); - -subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]) - .expect("Called `subscribe` before `connect`"); - -// Nothing printed this time. -``` - -## Identify a client - -### Type `Identity` - -```rust -spacetimedb_sdk::identity::Identity -``` - -A unique public identifier for a client connected to a database. - -### Type `Token` - -```rust -spacetimedb_sdk::identity::Token -``` - -A private access token for a client connected to a database. - -### Type `Credentials` - -```rust -spacetimedb_sdk::identity::Credentials -``` - -Credentials, including a private access token, sufficient to authenticate a client connected to a database. - -| Field | Type | -| ---------- | ---------------------------- | -| `identity` | [`Identity`](rust_#type-identity.) | -| `token` | [`Token`](#type-token.) | - -### Type `Address` - -```rust -spacetimedb_sdk::Address -``` - -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](rust_#type-identity.). - -### Function `identity` - -```rust -spacetimedb_sdk::identity::identity() -> Result -``` - -Read the current connection's public [`Identity`](rust_#type-identity.). - -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 identity is {:?}", identity()); - -// Prints "My identity is Ok(Identity { bytes: [...several u8s...] })" -``` - -### Function `token` - -```rust -spacetimedb_sdk::identity::token() -> Result -``` - -Read the current connection's private [`Token`](#type-token.). - -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` - -```rust -spacetimedb_sdk::identity::credentials() -> Result -``` - -Read the current connection's [`Credentials`](#type-credentials.), including a public [`Identity`](rust_#type-identity.) and a private [`Token`](#type-token.). - -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 credentials are {:?}", credentials()); - -// Prints "My credentials are Ok(Credentials { -// identity: Identity { bytes: [...several u8s...] }, -// token: Token { string: "...several Base64 digits..."}, -// })" -``` - -### Function `address` - -```rust -spacetimedb_sdk::identity::address() -> Result
-``` - -Read the current connection's [`Address`](rust_#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()); -``` - -### Function `on_connect` - -```rust -spacetimedb_sdk::identity::on_connect( - callback: impl FnMut(&Credentials, Address) + Send + 'static, -) -> ConnectCallbackId -``` - -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`](rust_#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. - -The returned `ConnectCallbackId` can be passed to [`remove_on_connect`](#function-remove_on_connect.) to unregister the callback. - -```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`. -``` - -### Function `once_on_connect` - -```rust -spacetimedb_sdk::identity::once_on_connect( - callback: impl FnOnce(&Credentials, Address) + Send + 'static, -) -> ConnectCallbackId -``` - -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`](rust_#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. - -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` - -```rust -spacetimedb_sdk::identity::remove_on_connect(id: ConnectCallbackId) -``` - -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. | - -If `id` does not refer to a currently-registered callback, this operation does nothing. - -```rust -let id = on_connect(|_creds, _addr| unreachable!()); - -remove_on_connect(id); - -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -sleep(Duration::from_secs(1)); - -// No `unreachable` panic. -``` - -### Function `load_credentials` - -```rust -spacetimedb_sdk::identity::load_credentials( - dirname: &str, -) -> Result> -``` - -Load a saved [`Credentials`](#type-credentials.) from a file within `~/dirname`, if one exists. - -| Argument | Type | Meaning | -| --------- | ------ | ----------------------------------------------------- | -| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | - -`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. - -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.). - -```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"); -``` - -### Function `save_credentials` - -```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.). - -| Argument | Type | Meaning | -| ------------- | -------------- | ----------------------------------------------------- | -| `dirname` | `&str` | Name of a sub-directory in the user's home directory. | -| `credentials` | `&Credentials` | [`Credentials`](#type-credentials.) to store. | - -`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. - -Returns `Err` when IO or serialization fails. - -```rust -const CREDENTIALS_DIR = ".my-module"; - -let creds = load_credentials(CREDENTIALS_DIRectory) - .expect("Error while loading credentials"); - -on_connect(|creds, _addr| { - if let Err(e) = save_credentials(CREDENTIALS_DIR, creds) { - eprintln!("Error while saving credentials: {:?}", e); - } -}); - -connect(SPACETIMEDB_URI, DB_NAME, creds) - .expect("Failed to connect"); -``` - -## View subscribed rows of tables - -### Type `{TABLE}` - -```rust -module_bindings::{TABLE} -``` - -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}` - -```rust -module_bindings::{TABLE}::filter_by_{COLUMN}( - value: {COLUMN_TYPE}, -) -> {FILTER_RESULT}<{TABLE}> -``` - -For each column of a table, `spacetime generate` generates a static method on the [table struct](rust_#type-table.) to filter or seek 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 depends on the column's attributes: - -- For unique columns, including those annotated `#[unique]` and `#[primarykey]`, the `filter_by` method returns an `Option<{TABLE}>`, where `{TABLE}` is the [table struct](rust_#type-table.). -- For non-unique columns, the `filter_by` method returns an `impl Iterator`. - -### Trait `TableType` - -```rust -spacetimedb_sdk::table::TableType -``` - -Every [generated table struct](rust_#type-table.) implements the trait `TableType`. - -#### Method `count` - -```rust -TableType::count() -> usize -``` - -Return the number of subscribed rows in the table, or 0 if there is no active connection. - -This method acquires a global lock. - -```rust -connect(SPACETIMEDB_URI, DB_NAME, None) - .expect("Failed to connect"); - -on_subscription_applied(|| println!("There are {} users", User::count())); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -sleep(Duration::from_secs(1)); - -// Will the number of `User` rows in the database. -``` - -#### Method `iter` - -```rust -TableType::iter() -> impl Iterator -``` - -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`](rust_#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. - -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](rust_#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` - -```rust -TableType::find( - predicate: impl FnMut(&Self) -> bool, -) -> Option -``` - -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 `filter_by_{COLUMN}` methods](rust_#method-filter_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. -``` - -#### Method `on_insert` - -```rust -TableType::on_insert( - callback: impl FnMut(&Self, Option<&ReducerEvent>) + Send + 'static, -) -> InsertCallbackId -``` - -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`](rust_#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(); - -sleep(Duration::from_secs(1)); - -// Will print a note whenever a new `User` row is inserted. -``` - -#### Method `remove_on_insert` - -```rust -TableType::remove_on_insert(id: InsertCallbackId) -``` - -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. | - -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_insert(|_, _| unreachable!()); - -User::remove_on_insert(id); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -sleep(Duration::from_secs(1)); - -// No `unreachable` panic. -``` - -#### Method `on_delete` - -```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: - -- `row: &Self`, the previously-present row which is no longer resident in the database. -- `reducer_event: Option<&ReducerEvent>`, the [`ReducerEvent`](rust_#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. - -```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)); - -// 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" }" -``` - -#### 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. -``` - -### Trait `TableWithPrimaryKey` - -```rust -spacetimedb_sdk::table::TableWithPrimaryKey -``` - -[Generated table structs](rust_#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`](rust_#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) -``` - -| 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. - -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_update(|_, _, _| unreachable!); - -User::remove_on_update(id); - -subscribe(&["SELECT * FROM User;"]) - .unwrap(); - -// No `unreachable` panic. -``` - -## Observe and request reducer invocations - -### Type `ReducerEvent` - -```rust -module_bindings::ReducerEvent -``` - -`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.). - -[`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. - -### Type `{REDUCER}Args` - -```rust -module_bindings::{REDUCER}Args -``` - -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`. - -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...}) -``` - -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`](rust_#type-identity.) of the client which invoked the reducer. -- `caller_address: Option
`, the [`Address`](rust_#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. - -### Function `remove_on_{REDUCER}` - -```rust -module_bindings::remove_on_{REDUCER}(id: ReducerCallbackId<{REDUCER}Args>) -``` - -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. - -### Type `Status` - -```rust -spacetimedb_sdk::reducer::Status -``` - -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. diff --git a/Writerside2/topics/sdks/rust/sdks_rust_quickstart.md b/Writerside2/topics/sdks/rust/sdks_rust_quickstart.md deleted file mode 100644 index f6049bf5..00000000 --- a/Writerside2/topics/sdks/rust/sdks_rust_quickstart.md +++ /dev/null @@ -1,487 +0,0 @@ -# Rust Client SDK Quick Start - -In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in Rust. - -We'll implement a command-line client for the module created in our Rust or C# Module Quickstart guides. Make sure you follow one of these guides before you start on this one. - -## Project structure - -Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: - -```bash -cd quickstart-chat -``` - -Within it, create a `client` crate, our client application, which users run locally: - -```bash -cargo new client -``` - -## Depend on `spacetimedb-sdk` and `hex` - -`client/Cargo.toml` should be initialized without any dependencies. We'll need two: - -- [`spacetimedb-sdk`](https://crates.io/crates/spacetimedb-sdk), which defines client-side interfaces for interacting with a remote SpacetimeDB module. -- [`hex`](https://crates.io/crates/hex), which we'll use to print unnamed users' identities as hexadecimal strings. - -Below the `[dependencies]` line in `client/Cargo.toml`, add: - -```toml -spacetimedb-sdk = "0.7" -hex = "0.4" -``` - -Make sure you depend on the same version of `spacetimedb-sdk` as is reported by the SpacetimeDB CLI tool's `spacetime version`! - -## Clear `client/src/main.rs` - -`client/src/main.rs` should be initialized with a trivial "Hello world" program. Clear it out so we can write our chat client. - -In your `quickstart-chat` directory, run: - -```bash -rm client/src/main.rs -touch client/src/main.rs -``` - -## Generate your module types - -The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. - -In your `quickstart-chat` directory, run: - -```bash -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: - -``` -module_bindings -├── message.rs -├── mod.rs -├── send_message_reducer.rs -├── set_name_reducer.rs -└── user.rs -``` - -We need to declare the module in our client crate, and we'll want to import its definitions. - -To `client/src/main.rs`, add: - -```rust -mod module_bindings; -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. - -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}, -}; -``` - -## Define 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: - -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: - -```rust -fn main() { - register_callbacks(); - connect_to_db(); - subscribe_to_tables(); - user_input_loop(); -} -``` - -## Register callbacks - -We need to handle several sorts of events: - -1. When we connect and receive our credentials, we'll save them to a file so that the next time we connect, we can re-authenticate as the same user. -2. When a new user joins, we'll print a message introducing them. -3. When a user is updated, we'll print their new name, or declare their new online status. -4. When we receive a new message, we'll print it. -5. When we're informed of the backlog of past messages, we'll sort them and print them in order. -6. If the server rejects our attempt to set our name, we'll print an error. -7. If the server rejects a message we send, we'll print an error. -8. When our connection ends, we'll print a note, then exit the process. - -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); - - // When a new user joins, print a notification. - User::on_insert(on_user_inserted); - - // When a user's status changes, print a notification. - User::on_update(on_user_updated); - - // When a new message is received, print it. - Message::on_insert(on_message_inserted); - - // When we receive the message backlog, print it in timestamp order. - on_subscription_applied(on_sub_applied); - - // When we fail to set our name, print a warning. - 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); -} -``` - -### 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. - -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) { - eprintln!("Failed to save credentials: {:?}", e); - } -} - -const CREDS_DIR: &str = ".spacetime_chat"; -``` - -### 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`. - -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. - -`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. - -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. - -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>) { - if user.online { - println!("User {} connected.", user_name_or_identity(user)); - } -} - -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]) -} -``` - -### 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. - -`on_update` callbacks take three arguments: the old row, the new row, and an `Option<&ReducerEvent>`. - -In our module, users can be updated for three reasons: - -1. They've set their name using the `set_name` reducer. -2. They're an existing user re-connecting, so their `online` has been set to `true`. -3. They've disconnected, so their `online` has been set to `false`. - -We'll print an appropriate message in each of these cases. - -To `client/src/main.rs`, add: - -```rust -/// Our `User::on_update` callback: -/// print a notification about name and status changes. -fn on_user_updated(old: &User, new: &User, _: Option<&ReducerEvent>) { - if old.name != new.name { - println!( - "User {} renamed to {}.", - user_name_or_identity(old), - user_name_or_identity(new) - ); - } - if old.online && !new.online { - println!("User {} disconnected.", user_name_or_identity(new)); - } - if !old.online && new.online { - println!("User {} connected.", user_name_or_identity(new)); - } -} -``` - -### 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. - -To find the `User` based on the message's `sender` identity, we'll use `User::filter_by_identity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filter_by_identity` accepts an owned `Identity`, rather than a reference. We can `clone` the identity held in `message.sender`. - -We'll print the user's name or identity in the same way as we did when notifying about `User` table events, but here we have to handle the case where we don't find a matching `User` row. This can happen when the module owner sends a message using the CLI's `spacetime call`. In this case, we'll print `unknown`. - -To `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 print_message(message: &Message) { - let sender = User::filter_by_identity(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: - -```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::>(); - messages.sort_by_key(|m| m.sent); - for message in messages { - print_message(&message); - } -} -``` - -### Warn if our name was rejected - -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. - -These callbacks will be invoked in one of two cases: - -1. If the reducer was successful and altered any of our subscribed rows. -2. If we requested an invocation which failed. - -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. - -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); - } -} -``` - -### 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); - } -} -``` - -### 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 -/// The URL of the SpacetimeDB instance hosting our chat module. -const SPACETIMEDB_URI: &str = "http://localhost:3000"; - -/// The module name we chose when we published our module. -const DB_NAME: &str = ""; - -/// 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"); -} -``` - -## Subscribe to queries - -SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. - -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(); -} -``` - -## 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. - -`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`. - -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() { - 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()); - } else { - send_message(line); - } - } -} -``` - -## Run it - -Change your directory to the client app, then compile and run it. From the `quickstart-chat` directory, run: - -```bash -cd client -cargo run -``` - -You should see something like: - -``` -User d9e25c51996dea2f connected. -``` - -Now try sending a message. Type `Hello, world!` and press enter. You should see something like: - -``` -d9e25c51996dea2f: Hello, world! -``` - -Next, set your name. Type `/name `, replacing `` with your name. You should see something like: - -``` -User d9e25c51996dea2f renamed to . -``` - -Then send another message. Type `Hello after naming myself.` and press enter. You should see: - -``` -: 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: - -``` -User connected. -: Hello, world! -: Hello after naming myself. -``` - -## What's next? - -You can find the full code for this client [in the Rust SDK's examples](https://github.com/clockworklabs/SpacetimeDB/tree/master/crates/sdk/examples/quickstart-chat). - -Check out the [Rust SDK Reference](rust1.) 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. - -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). - -Or, you could extend the module and the client together, perhaps: - -- 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. diff --git a/Writerside2/topics/sdks/sdks_index.md b/Writerside2/topics/sdks/sdks_index.md deleted file mode 100644 index bcc59bfd..00000000 --- a/Writerside2/topics/sdks/sdks_index.md +++ /dev/null @@ -1,74 +0,0 @@ - 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 - -- [Rust](rust1.) - [(Quickstart)](quickstart2.) -- [C#](c-sharp1.) - [(Quickstart)](quickstart3.) -- [TypeScript](typescript.) - [(Quickstart)](quickstart4.) -- [Python](python.) - [(Quickstart)](quickstart5.) - -## Key Features - -The SpacetimeDB Client SDKs offer the following key functionalities: - -### Connection Management - -The SDKs handle the process of connecting and disconnecting from the SpacetimeDB server, simplifying this process for the client applications. - -### Authentication - -The SDKs support authentication using an auth token, allowing clients to securely establish a session with the SpacetimeDB server. - -### Local Database View - -Each client can define a local view of the database via a subscription consisting of a set of queries. This local view is maintained by the server and populated into a local cache on the client side. - -### Reducer Calls - -The SDKs allow clients to call transactional functions (reducers) on the server. - -### Callback Registrations - -The SpacetimeDB Client SDKs offer powerful callback functionality that allow clients to monitor changes in their local database view. These callbacks come in two forms: - -#### Connection and Subscription Callbacks - -Clients can also register callbacks that trigger when the connection to the server is established or lost, or when a subscription is updated. This allows clients to react to changes in the connection status. - -#### Row Update Callbacks - -Clients can register callbacks that trigger when any row in their local cache is updated by the server. These callbacks contain information about the reducer that triggered the change. This feature enables clients to react to changes in data that they're interested in. - -#### Reducer Call Callbacks - -Clients can also register callbacks that fire when a reducer call modifies something in the client's local view. This allows the client to know when a transactional function it has executed has had an effect on the data it cares about. - -Additionally, when a client makes a reducer call that fails, the SDK triggers the registered reducer callback on the client that initiated the failed call with the error message that was returned from the server. This allows for appropriate error handling or user notifications. - -## Choosing a Language - -When selecting a language for your client application with SpacetimeDB, a variety of factors come into play. While the functionality of the SDKs remains consistent across different languages, the choice of language will often depend on the specific needs and context of your application. Here are a few considerations: - -### Team Expertise - -The familiarity of your development team with a particular language can greatly influence your choice. You might want to choose a language that your team is most comfortable with to increase productivity and reduce development time. - -### Application Type - -Different languages are often better suited to different types of applications. For instance, if you are developing a web-based application, you might opt for TypeScript due to its seamless integration with web technologies. On the other hand, if you're developing a desktop application, you might choose C# or Python, depending on your requirements and platform. Python is also very useful for utility scripts and tools. - -### Performance - -The performance characteristics of the different languages can also be a factor. If your application is performance-critical, you might opt for Rust, known for its speed and memory efficiency. - -### Platform Support - -The platform you're targeting can also influence your choice. For instance, if you're developing a game or a 3D application using the Unity engine, you'll want to choose the C# SDK, as Unity uses C# as its primary scripting language. - -### Ecosystem and Libraries - -Each language has its own ecosystem of libraries and tools that can help in developing your application. If there's a library in a particular language that you want to use, it may influence your choice. - -Remember, the best language to use is the one that best fits your use case and the one you and your team are most comfortable with. It's worth noting that due to the consistent functionality across different SDKs, transitioning from one language to another should you need to in the future will primarily involve syntax changes rather than changes in the application's logic. - -You may want to use multiple languages in your application. For instance, you might want to use C# in Unity for your game logic, TypeScript for a web-based administration panel, and Python for utility scripts. This is perfectly fine, as the SpacetimeDB server is completely client-agnostic. diff --git a/Writerside2/topics/sdks/typescript/typescript_index.md b/Writerside2/topics/sdks/typescript/typescript_index.md deleted file mode 100644 index 2316ecbb..00000000 --- a/Writerside2/topics/sdks/typescript/typescript_index.md +++ /dev/null @@ -1,942 +0,0 @@ -# The SpacetimeDB Typescript client SDK - -The SpacetimeDB client SDK for TypeScript contains all the tools you need to build clients for SpacetimeDB modules using Typescript, either in the browser or with NodeJS. - -> You need a database created before use the client, so make sure to follow the Rust or C# Module Quickstart guides if need one. - -## Install the SDK - -First, create a new client project, and add the following to your `tsconfig.json` file: - -```json -{ - "compilerOptions": { - //You can use any target higher than this one - //https://www.typescriptlang.org/tsconfig#target - "target": "es2015" - } -} -``` - -Then add the SpacetimeDB SDK to your dependencies: - -```bash -cd client -npm install @clockworklabs/spacetimedb-sdk -``` - -You should have this folder layout starting from the root of your project: - -```bash -quickstart-chat -├── client -│ ├── node_modules -│ ├── public -│ └── src -└── server - └── src -``` - -### Tip for utilities/scripts - -If want to create a quick script to test your module bindings from the command line, you can use https://www.npmjs.com/package/tsx to execute TypeScript files. - -Then you create a `script.ts` file and add the imports, code and execute with: - -```bash -npx tsx src/script.ts -``` - -## Generate module bindings - -Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's `src` directory and generate the Typescript interface files using the Spacetime CLI. From your project directory, run: - -```bash -mkdir -p client/src/module_bindings -spacetime generate --lang typescript \ - --out-dir client/src/module_bindings \ - --project-path server -``` - -And now you will get the files for the `reducers` & `tables`: - -```bash -quickstart-chat -├── client -│ ├── node_modules -│ ├── public -│ └── src -| └── module_bindings -| ├── add_reducer.ts -| ├── person.ts -| └── say_hello_reducer.ts -└── server - └── src -``` - -Import the `module_bindings` in your client's _main_ file: - -```typescript -import { SpacetimeDBClient, Identity } from "@clockworklabs/spacetimedb-sdk"; - -import Person from "./module_bindings/person"; -import AddReducer from "./module_bindings/add_reducer"; -import SayHelloReducer from "./module_bindings/say_hello_reducer"; -console.log(Person, AddReducer, SayHelloReducer); -``` - -> There is a known issue where if you do not use every type in your file, it will not pull them into the published build. To fix this, we are using `console.log` to force them to get pulled in. - -## API at a glance - -### Classes - -| Class | Description | -|-------------------------------------------------|------------------------------------------------------------------------------| -| [`SpacetimeDBClient`](#class-spacetimedbclient.) | The database client connection to a SpacetimeDB server. | -| [`Identity`](typescript_#class-identity.) | The user's public identity. | -| [`Address`](typescript_#class-address.) | An opaque identifier for differentiating connections by the same `Identity`. | -| [`{Table}`](typescript_#class-table.) | `{Table}` is a placeholder for each of the generated tables. | -| [`{Reducer}`](typescript_#class-reducer.) | `{Reducer}` is a placeholder for each of the generated reducers. | - -### Class `SpacetimeDBClient` - -The database client connection to a SpacetimeDB server. - -Defined in [spacetimedb-sdk.spacetimedb](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/spacetimedb.ts): - -| Constructors | Description | -| ----------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`SpacetimeDBClient.constructor`](#spacetimedbclient-constructor.) | Creates a new `SpacetimeDBClient` database client. | -| Properties | -| [`SpacetimeDBClient.identity`](#spacetimedbclient-identity.) | The user's public identity. | -| [`SpacetimeDBClient.live`](#spacetimedbclient-live.) | Whether the client is connected. | -| [`SpacetimeDBClient.token`](#spacetimedbclient-token.) | The user's private authentication token. | -| Methods | | -| [`SpacetimeDBClient.connect`](#spacetimedbclient-connect.) | Connect to a SpacetimeDB module. | -| [`SpacetimeDBClient.disconnect`](#spacetimedbclient-disconnect.) | Close the current connection. | -| [`SpacetimeDBClient.subscribe`](#spacetimedbclient-subscribe.) | Subscribe to a set of queries. | -| Events | | -| [`SpacetimeDBClient.onConnect`](#spacetimedbclient-onconnect.) | Register a callback to be invoked upon authentication with the database. | -| [`SpacetimeDBClient.onError`](#spacetimedbclient-onerror.) | Register a callback to be invoked upon a error. | - -## Constructors - -### `SpacetimeDBClient` constructor - -Creates a new `SpacetimeDBClient` database client and set the initial parameters. - -```ts -new SpacetimeDBClient(host: string, name_or_address: string, auth_token?: string, protocol?: "binary" | "json") -``` - -#### Parameters - -| Name | Type | Description | -| :---------------- | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | -| `host` | `string` | The host of the SpacetimeDB server. | -| `name_or_address` | `string` | The name or address of the SpacetimeDB module. | -| `auth_token?` | `string` | The credentials to use to connect to authenticate with SpacetimeDB. | -| `protocol?` | `"binary"` \| `"json"` | Define how encode the messages: `"binary"` \| `"json"`. Binary is more efficient and compact, but JSON provides human-readable debug information. | - -#### Example - -```ts -const host = "ws://localhost:3000"; -const name_or_address = "database_name"; -const auth_token = undefined; -const protocol = "binary"; - -var spacetimeDBClient = new SpacetimeDBClient( - host, - name_or_address, - auth_token, - protocol -); -``` - -## Class methods - -### `SpacetimeDBClient.registerReducers` - -Registers reducer classes for use with a SpacetimeDBClient - -```ts -registerReducers(...reducerClasses: ReducerClass[]) -``` - -#### Parameters - -| Name | Type | Description | -| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | -| `reducerClasses` | `ReducerClass` | A list of classes to register | - -#### Example - -```ts -import SayHelloReducer from './types/say_hello_reducer'; -import AddReducer from './types/add_reducer'; - -SpacetimeDBClient.registerReducers(SayHelloReducer, AddReducer); -``` - ---- - -### `SpacetimeDBClient.registerTables` - -Registers table classes for use with a SpacetimeDBClient - -```ts -registerTables(...reducerClasses: TableClass[]) -``` - -#### Parameters - -| Name | Type | Description | -| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | -| `tableClasses` | `TableClass` | A list of classes to register | - -#### Example - -```ts -import User from './types/user'; -import Player from './types/player'; - -SpacetimeDBClient.registerTables(User, Player); -``` - ---- - -## Properties - -### `SpacetimeDBClient` identity - -The user's public [Identity](typescript_#class-identity.). - -``` -identity: Identity | undefined -``` - ---- - -### `SpacetimeDBClient` live - -Whether the client is connected. - -```ts -live: boolean; -``` - ---- - -### `SpacetimeDBClient` token - -The user's private authentication token. - -``` -token: string | undefined -``` - -#### Parameters - -| Name | Type | Description | -| :------------ | :----------------------------------------------------- | :------------------------------ | -| `reducerName` | `string` | The name of the reducer to call | -| `serializer` | [`Serializer`](serializer.Serializer.md) | - | - ---- - -### `SpacetimeDBClient` connect - -Connect to The SpacetimeDB Websocket For Your Module. By default, this will use a secure websocket connection. The parameters are optional, and if not provided, will use the values provided on construction of the client. - -```ts -connect(host: string?, name_or_address: string?, auth_token: string?): Promise -``` - -#### Parameters - -| Name | Type | Description | -| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------ | -| `host?` | `string` | The hostname of the SpacetimeDB server. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | -| `name_or_address?` | `string` | The name or address of the SpacetimeDB module. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | -| `auth_token?` | `string` | The credentials to use to authenticate with SpacetimeDB. Defaults to the value passed to the [constructor](#spacetimedbclient-constructor.). | - -#### Returns - -`Promise`<`void`\> - -#### Example - -```ts -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 -); -// Connect with the initial parameters -spacetimeDBClient.connect(); -//Set the `auth_token` -spacetimeDBClient.connect(undefined, undefined, NEW_TOKEN); -``` - ---- - -### `SpacetimeDBClient` disconnect - -Close the current connection. - -```ts -disconnect(): void -``` - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); - -spacetimeDBClient.disconnect(); -``` - ---- - -### `SpacetimeDBClient` subscribe - -Subscribe to a set of queries, to be notified when rows which match those queries are altered. - -> A new call to `subscribe` will remove all previous subscriptions and replace them with the new `queries`. -> If any rows matched the previous subscribed queries but do not match the new queries, -> those rows will be removed from the client cache, and [`{Table}.on_delete`](#table-ondelete.) callbacks will be invoked for them. - -```ts -subscribe(queryOrQueries: string | string[]): void -``` - -#### Parameters - -| Name | Type | Description | -| :--------------- | :--------------------- | :------------------------------- | -| `queryOrQueries` | `string` \| `string`[] | A `SQL` query or list of queries | - -#### Example - -```ts -spacetimeDBClient.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); -``` - -## Events - -### `SpacetimeDBClient` onConnect - -Register a callback to be invoked upon authentication with the database. - -```ts -onConnect(callback: (token: string, identity: Identity) => void): void -``` - -The callback will be invoked with the public user [Identity](typescript_#class-identity.), private authentication token and connection [`Address`](typescript_#class-address.) provided by the database. If credentials were supplied to [connect](#spacetimedbclient-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 will be generated by the database to identify this user. - -The credentials passed to the callback can be saved and used to authenticate the same user in future connections. - -#### Parameters - -| Name | Type | -|:-----------|:-----------------------------------------------------------------------------------------------------------------| -| `callback` | (`token`: `string`, `identity`: [`Identity`](typescript_#class-identity.), `address`: [`Address`](typescript_#class-address.)) => `void` | - -#### Example - -```ts -spacetimeDBClient.onConnect((token, identity, address) => { - console.log("Connected to SpacetimeDB"); - console.log("Token", token); - console.log("Identity", identity); - console.log("Address", address); -}); -``` - ---- - -### `SpacetimeDBClient` onError - -Register a callback to be invoked upon an error. - -```ts -onError(callback: (...args: any[]) => void): void -``` - -#### Parameters - -| Name | Type | -| :--------- | :----------------------------- | -| `callback` | (...`args`: `any`[]) => `void` | - -#### Example - -```ts -spacetimeDBClient.onError((...args: any[]) => { - console.error("ERROR", args); -}); -``` - -### Class `Identity` - -A unique public identifier for a user of a database. - -Defined in [spacetimedb-sdk.identity](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/identity.ts): - -| Constructors | Description | -| ----------------------------------------------- | -------------------------------------------- | -| [`Identity.constructor`](#identity-constructor.) | Creates a new `Identity`. | -| Methods | | -| [`Identity.isEqual`](#identity-isequal.) | Compare two identities for equality. | -| [`Identity.toHexString`](#identity-tohexstring.) | Print the identity as a hexadecimal string. | -| Static methods | | -| [`Identity.fromString`](#identity-fromstring.) | Parse an Identity from a hexadecimal string. | - -## Constructors - -### `Identity` constructor - -```ts -new Identity(data: Uint8Array) -``` - -#### Parameters - -| Name | Type | -| :----- | :----------- | -| `data` | `Uint8Array` | - -## Methods - -### `Identity` isEqual - -Compare two identities for equality. - -```ts -isEqual(other: Identity): boolean -``` - -#### Parameters - -| Name | Type | -| :------ | :---------------------------- | -| `other` | [`Identity`](typescript_#class-identity.) | - -#### Returns - -`boolean` - ---- - -### `Identity` toHexString - -Print an `Identity` as a hexadecimal string. - -```ts -toHexString(): string -``` - -#### Returns - -`string` - ---- - -### `Identity` fromString - -Static method; parse an Identity from a hexadecimal string. - -```ts -Identity.fromString(str: string): Identity -``` - -#### Parameters - -| Name | Type | -| :---- | :------- | -| `str` | `string` | - -#### Returns - -[`Identity`](typescript_#class-identity.) - -### Class `Address` - -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](typescript_#type-identity.). - -Defined in [spacetimedb-sdk.address](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/address.ts): - -| Constructors | Description | -| ----------------------------------------------- | -------------------------------------------- | -| [`Address.constructor`](#address-constructor.) | Creates a new `Address`. | -| Methods | | -| [`Address.isEqual`](#address-isequal.) | Compare two identities for equality. | -| [`Address.toHexString`](#address-tohexstring.) | Print the address as a hexadecimal string. | -| Static methods | | -| [`Address.fromString`](#address-fromstring.) | Parse an Address from a hexadecimal string. | - -## Constructors - -### `Address` constructor - -```ts -new Address(data: Uint8Array) -``` - -#### Parameters - -| Name | Type | -| :----- | :----------- | -| `data` | `Uint8Array` | - -## Methods - -### `Address` isEqual - -Compare two addresses for equality. - -```ts -isEqual(other: Address): boolean -``` - -#### Parameters - -| Name | Type | -| :------ | :---------------------------- | -| `other` | [`Address`](typescript_#class-address.) | - -#### Returns - -`boolean` - -___ - -### `Address` toHexString - -Print an `Address` as a hexadecimal string. - -```ts -toHexString(): string -``` - -#### Returns - -`string` - -___ - -### `Address` fromString - -Static method; parse an Address from a hexadecimal string. - -```ts -Address.fromString(str: string): Address -``` - -#### Parameters - -| Name | Type | -| :---- | :------- | -| `str` | `string` | - -#### Returns - -[`Address`](typescript_#class-address.) - -### Class `{Table}` - -For each table defined by a module, `spacetime generate` generates a `class` in the `module_bindings` folder whose name is that table's name converted to `PascalCase`. - -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; returned subscribed rows with a given value in a particular 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 - -### {Table} name - -• **name**: `string` - -The name of the `Class`. - ---- - -### {Table} tableName - -The name of the table in the database. - -▪ `Static` **tableName**: `string` = `"Person"` - -## Methods - -### {Table} all - -Return all the subscribed rows in the table. - -```ts -{Table}.all(): {Table}[] -``` - -#### Returns - -`{Table}[]` - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); - -spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(["SELECT * FROM Person"]); - - setTimeout(() => { - console.log(Person.all()); // Prints all the `Person` rows in the database. - }, 5000); -}); -``` - ---- - -### {Table} count - -Return the number of subscribed rows in the table, or 0 if there is no active connection. - -```ts -{Table}.count(): number -``` - -#### Returns - -`number` - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); - -spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(["SELECT * FROM Person"]); - - setTimeout(() => { - console.log(Person.count()); - }, 5000); -}); -``` - ---- - -### {Table} filterBy{COLUMN} - -For each column of a table, `spacetime generate` generates a static method on the `Class` to filter or seek subscribed rows where that column matches a requested value. - -These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `camelCase`. - -```ts -{Table}.filterBy{COLUMN}(value): {Table}[] -``` - -#### Parameters - -| Name | Type | -| :------ | :-------------------------- | -| `value` | The type of the `{COLUMN}`. | - -#### Returns - -`{Table}[]` - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); - -spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(["SELECT * FROM Person"]); - - setTimeout(() => { - console.log(Person.filterByName("John")); // prints all the `Person` rows named John. - }, 5000); -}); -``` - ---- - -### {Table} fromValue - -Deserialize an `AlgebraicType` into this `{Table}`. - -```ts - {Table}.fromValue(value: AlgebraicValue): {Table} -``` - -#### Parameters - -| Name | Type | -| :------ | :--------------- | -| `value` | `AlgebraicValue` | - -#### Returns - -`{Table}` - ---- - -### {Table} getAlgebraicType - -Serialize `this` into an `AlgebraicType`. - -#### Example - -```ts -{Table}.getAlgebraicType(): AlgebraicType -``` - -#### Returns - -`AlgebraicType` - ---- - -### {Table} onInsert - -Register an `onInsert` callback for when a subscribed row is newly inserted into the database. - -```ts -{Table}.onInsert(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | Description | -| :--------- | :---------------------------------------------------------------------------- | :----------------------------------------------------- | -| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is inserted. | - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); -spacetimeDBClient.onConnect((token, identity, address) => { - 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); - } -}); -``` - ---- - -### {Table} removeOnInsert - -Unregister a previously-registered [`onInsert`](#table-oninsert.) callback. - -```ts -{Table}.removeOnInsert(callback: (value: Person, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | -| :--------- | :---------------------------------------------------------------------------- | -| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | - ---- - -### {Table} onUpdate - -Register an `onUpdate` callback to run when an existing row is modified by primary key. - -```ts -{Table}.onUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -`onUpdate` callbacks are only meaningful for tables with a column declared as a primary key. Tables without primary keys will never fire `onUpdate` callbacks. - -#### Parameters - -| Name | Type | Description | -| :--------- | :------------------------------------------------------------------------------------------------------ | :---------------------------------------------------- | -| `callback` | (`oldValue`: `{Table}`, `newValue`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is updated. | - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); -spacetimeDBClient.onConnect((token, identity, address) => { - spacetimeDBClient.subscribe(["SELECT * FROM Person"]); -}); - -Person.onUpdate((oldPerson, newPerson, reducerEvent) => { - console.log("Person updated by reducer", reducerEvent, oldPerson, newPerson); -}); -``` - ---- - -### {Table} removeOnUpdate - -Unregister a previously-registered [`onUpdate`](#table-onupdate.) callback. - -```ts -{Table}.removeOnUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | -| :--------- | :------------------------------------------------------------------------------------------------------ | -| `callback` | (`oldValue`: `{Table}`, `newValue`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | - ---- - -### {Table} onDelete - -Register an `onDelete` callback for when a subscribed row is removed from the database. - -```ts -{Table}.onDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | Description | -| :--------- | :---------------------------------------------------------------------------- | :---------------------------------------------------- | -| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | Callback to run whenever a subscribed row is removed. | - -#### Example - -```ts -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "database_name" -); -spacetimeDBClient.onConnect((token, identity, address) => { - 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 - ); - } -}); -``` - ---- - -### {Table} removeOnDelete - -Unregister a previously-registered [`onDelete`](#table-ondelete.) callback. - -```ts -{Table}.removeOnDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void -``` - -#### Parameters - -| Name | Type | -| :--------- | :---------------------------------------------------------------------------- | -| `callback` | (`value`: `{Table}`, `reducerEvent`: `undefined` \| `ReducerEvent`) => `void` | - -### Class `{Reducer}` - -`spacetime generate` defines an `{Reducer}` class in the `module_bindings` folder for each reducer defined by a module. - -The class's name will be the reducer's name converted to `PascalCase`. - -| Static methods | Description | -| ------------------------------- | ------------------------------------------------------------ | -| [`Reducer.call`](#reducer-call.) | Executes the reducer. | -| Events | | -| [`Reducer.on`](#reducer-on.) | Register a callback to run each time the reducer is invoked. | - -## Static methods - -### {Reducer} call - -Executes the reducer. - -```ts -{Reducer}.call(): void -``` - -#### Example - -```ts -SayHelloReducer.call(); -``` - -## Events - -### {Reducer} on - -Register a callback to run each time the reducer is invoked. - -```ts -{Reducer}.on(callback: (reducerEvent: ReducerEvent, ...reducerArgs: any[]) => void): void -``` - -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. - -#### Parameters - -| Name | Type | -| :--------- | :---------------------------------------------------------- | -| `callback` | `(reducerEvent: ReducerEvent, ...reducerArgs: any[]) => void)` | - -#### Example - -```ts -SayHelloReducer.on((reducerEvent, ...reducerArgs) => { - console.log("SayHelloReducer called", reducerEvent, reducerArgs); -}); -``` diff --git a/Writerside2/topics/sdks/typescript/typescript_quickstart.md b/Writerside2/topics/sdks/typescript/typescript_quickstart.md deleted file mode 100644 index 13ccd4d6..00000000 --- a/Writerside2/topics/sdks/typescript/typescript_quickstart.md +++ /dev/null @@ -1,502 +0,0 @@ -# Typescript Client SDK Quick Start - -In this guide we'll show you how to get up and running with a simple SpacetimDB app with a client written in Typescript. - -We'll implement a basic single page web app for the module created in our Rust or C# Module Quickstart guides. **Make sure you follow one of these guides before you start on this one.** - -## Project structure - -Enter the directory `quickstart-chat` you created in the [Rust Module Quickstart](quickstart.) or [C# Module Quickstart](quickstart1.) guides: - -```bash -cd quickstart-chat -``` - -Within it, create a `client` react app: - -```bash -npx create-react-app client --template typescript -``` - -We also need to install the `spacetime-client-sdk` package: - -```bash -cd client -npm install @clockworklabs/spacetimedb-sdk -``` - -## Basic layout - -We are going to start by creating a basic layout for our app. The page contains four sections: - -1. A profile section, where we can set our name. -2. A message section, where we can see all the messages. -3. A system section, where we can see system messages. -4. A new message section, where we can send a new message. - -The `onSubmitNewName` and `onMessageSubmit` callbacks will be called when the user clicks the submit button in the profile and new message sections, respectively. We'll hook these up later. - -Replace the entire contents of `client/src/App.tsx` with the following: - -```typescript -import React, { useEffect, useState } from "react"; -import logo from "./logo.svg"; -import "./App.css"; - -export type MessageType = { - name: string; - message: string; -}; - -function App() { - const [newName, setNewName] = useState(""); - const [settingName, setSettingName] = useState(false); - const [name, setName] = useState(""); - const [systemMessage, setSystemMessage] = useState(""); - const [messages, setMessages] = useState([]); - - const [newMessage, setNewMessage] = useState(""); - - const onSubmitNewName = (e: React.FormEvent) => { - e.preventDefault(); - setSettingName(false); - // Fill in app logic here - }; - - const onMessageSubmit = (e: React.FormEvent) => { - e.preventDefault(); - // Fill in app logic here - setNewMessage(""); - }; - - return ( -
-
-

Profile

- {!settingName ? ( - <> -

{name}

- - - ) : ( -
- setNewName(e.target.value)} - /> - - - )} -
-
-

Messages

- {messages.length < 1 &&

No messages

} -
- {messages.map((message, key) => ( -
-

- {message.name} -

-

{message.message}

-
- ))} -
-
-
-

System

-
-

{systemMessage}

-
-
-
-
-

New Message

- - - -
-
- ); -} - -export default App; -``` - -Now when you run `npm start`, you should see a basic chat app that does not yet send or receive messages. - -## Generate your module types - -The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. - -In your `quickstart-chat` directory, run: - -```bash -mkdir -p client/src/module_bindings -spacetime generate --lang typescript --out-dir client/src/module_bindings --project-path server -``` - -Take a look inside `client/src/module_bindings`. The CLI should have generated four files: - -``` -module_bindings -├── message.ts -├── send_message_reducer.ts -├── set_name_reducer.ts -└── user.ts -``` - -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 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); -``` - -## Create your SpacetimeDB client - -First, we need to create a SpacetimeDB client and connect to the module. Create your client at the top of the `App` function. - -We are going to create a stateful variable to store our client's SpacetimeDB identity when we receive it. Also, we are using `localStorage` to retrieve your auth token if this client has connected before. We will explain these later. - -Replace `` with the name you chose when publishing your module during the module quickstart. If you are using SpacetimeDB Cloud, the host will be `wss://spacetimedb.com/spacetimedb`. - -Add this before the `App` function declaration: - -```typescript -let token = localStorage.getItem("auth_token") || undefined; -var spacetimeDBClient = new SpacetimeDBClient( - "ws://localhost:3000", - "chat", - token -); -``` - -Inside the `App` function, add a few refs: - -```typescript -let local_identity = useRef(undefined); -let initialized = useRef(false); -const client = useRef(spacetimeDBClient); -``` - -## Register callbacks and connect - -We need to handle several sorts of events: - -1. `onConnect`: When we connect and receive our credentials, we'll save them to browser local storage, so that the next time we connect, we can re-authenticate as the same user. -2. `initialStateSync`: When we're informed of the backlog of past messages, we'll sort them and update the `message` section of the page. -3. `Message.onInsert`: When we receive a new message, we'll update the `message` section of the page. -4. `User.onInsert`: When a new user joins, we'll update the `system` section of the page with an appropiate message. -5. `User.onUpdate`: When a user is updated, we'll add a message with their new name, or declare their new online status to the `system` section of the page. -6. `SetNameReducer.on`: If the server rejects our attempt to set our name, we'll update the `system` section of the page with an appropriate error message. -7. `SendMessageReducer.on`: If the server rejects a message we send, we'll update the `system` section of the page with an appropriate error message. - -We will add callbacks for each of these items in the following sections. All of these callbacks will be registered inside the `App` function after the `useRef` declarations. - -### onConnect Callback - -On connect SpacetimeDB will provide us with our client credentials. - -Each user has a set of 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. - -These 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. - -We want to store our local client identity in a stateful variable and also save our `token` to local storage for future connections. - -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. - -Once we are connected, we can send our subscription to the SpacetimeDB module. SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation compared. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database. - -To the body of `App`, add: - -```typescript -client.current.onConnect((token, identity, address) => { - console.log("Connected to SpacetimeDB"); - - local_identity.current = identity; - - localStorage.setItem("auth_token", token); - - client.current.subscribe(["SELECT * FROM User", "SELECT * FROM Message"]); -}); -``` - -### initialStateSync callback - -This callback fires when our local client cache of the database is populated. This is a good time to set the initial messages list. - -We'll define a helper function, `setAllMessagesInOrder`, to supply the `MessageType` class for our React application. It will call the autogenerated `Message.all` function to get an array of `Message` rows, then sort them and convert them to `MessageType`. - -To find the `User` based on the message's `sender` identity, we'll use `User::filterByIdentity`, which behaves like the same function on the server. The key difference is that, unlike on the module side, the client's `filterByIdentity` accepts a `UInt8Array`, rather than an `Identity`. The `sender` identity stored in the message is also a `UInt8Array`, not an `Identity`, so we can just pass it to the filter method. - -Whenever we want to display a user name, if they have set a name, we'll use that. If they haven't set a name, we'll instead use the first 8 bytes of their identity, encoded as hexadecimal. We'll define the function `userNameOrIdentity` to handle this. - -We also 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 display `unknown`. - -To the body of `App`, add: - -```typescript -function userNameOrIdentity(user: User): string { - console.log(`Name: ${user.name} `); - if (user.name !== null) { - return user.name || ""; - } else { - var identityStr = new Identity(user.identity).toHexString(); - console.log(`Name: ${identityStr} `); - return new Identity(user.identity).toHexString().substring(0, 8); - } -} - -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 sender_identity = User.filterByIdentity(message.sender); - let display_name = sender_identity - ? userNameOrIdentity(sender_identity) - : "unknown"; - - return { - name: display_name, - message: message.text, - }; - }); - - setMessages(messagesType); -} - -client.current.on("initialStateSync", () => { - setAllMessagesInOrder(); - var user = User.filterByIdentity(local_identity?.current?.toUint8Array()!); - setName(userNameOrIdentity(user!)); -}); -``` - -### Message.onInsert callback - Update messages - -When we receive a new message, we'll update the messages section of the page. Keep in mind that we only want to do this for new messages, i.e. those inserted by a `send_message` reducer invocation. When the server is initializing our cache, we'll get a callback for each existing message, but we don't want to update the page for those. To that effect, our `onInsert` callback will check if its `ReducerEvent` argument is not `undefined`, and only update the `message` section in that case. - -To the body of `App`, add: - -```typescript -Message.onInsert((message, reducerEvent) => { - if (reducerEvent !== undefined) { - setAllMessagesInOrder(); - } -}); -``` - -### User.onInsert callback - 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 `onInsert` and `onDelete` methods of the trait `TableType`, 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.onInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. - -`onInsert` and `onDelete` callbacks take two arguments: the altered row, and a `ReducerEvent | undefined`. This will be `undefined` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is a class containing information about the reducer that triggered this event. For now, we can ignore this argument. - -We are going to add a helper function called `appendToSystemMessage` that will append a line to the `systemMessage` state. We will use this to update the `system` message when a new user joins. - -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); -} - -User.onInsert((user, reducerEvent) => { - if (user.online) { - appendToSystemMessage(`${userNameOrIdentity(user)} has connected.`); - } -}); -``` - -### User.onUpdate callback - 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 `onUpdate` method which is automatically implemented by `spacetime generate` for any table with a `#[primarykey]` column. - -`onUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. - -In our module, users can be updated for three reasons: - -1. They've set their name using the `set_name` reducer. -2. They're an existing user re-connecting, so their `online` has been set to `true`. -3. They've disconnected, so their `online` has been set to `false`. - -We'll update the `system` message in each of these cases. - -To the body of `App`, add: - -```typescript -User.onUpdate((oldUser, user, reducerEvent) => { - if (oldUser.online === false && user.online === true) { - appendToSystemMessage(`${userNameOrIdentity(user)} has connected.`); - } else if (oldUser.online === true && user.online === false) { - appendToSystemMessage(`${userNameOrIdentity(user)} has disconnected.`); - } - - if (user.name !== oldUser.name) { - appendToSystemMessage( - `User ${userNameOrIdentity(oldUser)} renamed to ${userNameOrIdentity( - user - )}.` - ); - } -}); -``` - -### SetNameReducer.on callback - Handle errors and update profile name - -We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducer` method which is automatically implemented for each reducer by `spacetime generate`. - -Each reducer callback takes a number of parameters: - -1. `ReducerEvent` that contains information about the reducer that triggered this event. It contains several fields. The ones we care about are: - - - `callerIdentity`: The `Identity` of the client that called the reducer. - - `status`: The `Status` of the reducer run, one of `"Committed"`, `"Failed"` or `"OutOfEnergy"`. - - `message`: The error message, if any, that the reducer returned. - -2. The rest of the parameters are arguments passed to the reducer. - -These callbacks will be invoked in one of two cases: - -1. If the reducer was successful and altered any of our subscribed rows. -2. If we requested an invocation which failed. - -Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity. - -We already handle other users' `set_name` calls using our `User.onUpdate` callback, but we need some additional behavior for setting our own name. If our name was rejected, we'll update the `system` message. If our name was accepted, we'll update our name in the app. - -We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. - -If the reducer status comes back as `committed`, we'll update the name in our app. - -To the body of `App`, add: - -```typescript -SetNameReducer.on((reducerEvent, newName) => { - if ( - local_identity.current && - reducerEvent.callerIdentity.isEqual(local_identity.current) - ) { - if (reducerEvent.status === "failed") { - appendToSystemMessage(`Error setting name: ${reducerEvent.message} `); - } else if (reducerEvent.status === "committed") { - setName(newName); - } - } -}); -``` - -### SendMessageReducer.on callback - Handle errors - -We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different. We don't need to do anything for successful SendMessage reducer runs; our Message.onInsert callback already displays them. - -To the body of `App`, add: - -```typescript -SendMessageReducer.on((reducerEvent, newMessage) => { - if ( - local_identity.current && - reducerEvent.callerIdentity.isEqual(local_identity.current) - ) { - if (reducerEvent.status === "failed") { - appendToSystemMessage(`Error sending message: ${reducerEvent.message} `); - } - } -}); -``` - -## Update the UI button callbacks - -We need to update the `onSubmitNewName` and `onMessageSubmit` callbacks to send the appropriate reducer to the module. - -`spacetime generate` defined two functions for us, `SetNameReducer.call` and `SendMessageReducer.call`, 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 `SetNameReducer.call` and `SendMessageReducer.call` take one argument, a `String`. - -Add the following to the `onSubmitNewName` callback: - -```typescript -SetNameReducer.call(newName); -``` - -Add the following to the `onMessageSubmit` callback: - -```typescript -SendMessageReducer.call(newMessage); -``` - -## Connecting to the module - -We need to connect to the module when the app loads. We'll do this by adding a `useEffect` hook to the `App` function. This hook should only run once, when the component is mounted, but we are going to use an `initialized` boolean to ensure that it only runs once. - -```typescript -useEffect(() => { - if (!initialized.current) { - client.current.connect(); - initialized.current = true; - } -}, []); -``` - -## What's next? - -When you run `npm start` you should see a chat app that can send and receive messages. If you open it in multiple private browser windows, you should see that messages are synchronized between them. - -Congratulations! You've built a simple chat app with SpacetimeDB. You can find the full source code for this app [here](https://github.com/clockworklabs/spacetimedb-typescript-sdk/tree/main/examples/quickstart) - -For a more advanced example of the SpacetimeDB TypeScript SDK, take a look at the [Spacetime MUD (multi-user dungeon)](https://github.com/clockworklabs/spacetime-mud/tree/main/react-client). - -## Troubleshooting - -If you encounter the following error: - -``` -TS2802: Type 'IterableIterator' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher. -``` - -You can fix it by changing your compiler target. Add the following to your `tsconfig.json` file: - -```json -{ - "compilerOptions": { - "target": "es2015" - } -} -``` diff --git a/Writerside2/topics/sql/sql_index.md b/Writerside2/topics/sql/sql_index.md deleted file mode 100644 index 96f0c223..00000000 --- a/Writerside2/topics/sql/sql_index.md +++ /dev/null @@ -1,407 +0,0 @@ -# SQL Support - -SpacetimeDB supports a subset of SQL as a query language. Developers can evaluate SQL queries against a Spacetime database via the `spacetime sql` command-line tool and the [`/database/sql/:name_or_address POST` HTTP endpoint](database#databasesqlname_or_address-post.). Client developers also write SQL queries when subscribing to events in the [WebSocket API](ws#subscribe.) or via an SDK `subscribe` function. - -SpacetimeDB aims to support much of the [SQL 2016 standard](https://www.iso.org/standard/63555.html), and in particular aims to be compatible with [PostgreSQL](https://www.postgresql.org/). - -SpacetimeDB 0.6 implements a relatively small subset of SQL. Future SpacetimeDB versions will implement additional SQL features. - -## Types - -| Type | Description | -| --------------------------------------------- | -------------------------------------- | -| [Nullable types](#nullable-types.) | Types which may not hold a value. | -| [Logic types](#logic-types.) | Booleans, i.e. `true` and `false`. | -| [Integer types](#integer-types.) | Numbers without fractional components. | -| [Floating-point types](#floating-point-types.) | Numbers with fractional components. | -| [Text types](#text-types.) | UTF-8 encoded text. | - -### Definition statements - -| Statement | Description | -| ----------------------------- | ------------------------------------ | -| [CREATE TABLE](#create-table.) | Create a new table. | -| [DROP TABLE](#drop-table.) | Remove a table, discarding all rows. | - -### Query statements - -| Statement | Description | -| ----------------- | -------------------------------------------------------------------------------------------- | -| [FROM](#from.) | A source of data, like a table or a value. | -| [JOIN](#join.) | Combine several data sources. | -| [SELECT](#select.) | Select specific rows and columns from a data source, and optionally compute a derived value. | -| [DELETE](#delete.) | Delete specific rows from a table. | -| [INSERT](#insert.) | Insert rows into a table. | -| [UPDATE](#update.) | Update specific rows in a table. | - -## Data types - -SpacetimeDB is built on the Spacetime Algebraic Type System, or SATS. SATS is a richer, more expressive type system than the one included in the SQL language. - -Because SATS is a richer type system than SQL, some SATS types cannot cleanly correspond to SQL types. In particular, the SpacetimeDB SQL interface is unable to construct or compare instances of product and sum types. As such, SpacetimeDB SQL must largely restrict themselves to interacting with columns of builtin types. - -Most SATS builtin types map cleanly to SQL types. - -### Nullable types - -SpacetimeDB types, by default, do not permit `NULL` as a value. Nullable types are encoded in SATS using a sum type which corresponds to [Rust's `Option`](https://doc.rust-lang.org/stable/std/option/enum.Option.html). In SQL, such types can be written by adding the constraint `NULL`, like `INT NULL`. - -### Logic types - -| SQL | SATS | Example | -| --------- | ------ | --------------- | -| `BOOLEAN` | `Bool` | `true`, `false` | - -### Numeric types - -#### Integer types - -An integer is a number without a fractional component. - -Adding the `UNSIGNED` constraint to an integer type allows only positive values. This allows representing a larger positive range without increasing the width of the integer. - -| SQL | SATS | Example | Min | Max | -| ------------------- | ----- | ------- | ------ | ----- | -| `TINYINT` | `I8` | 1 | -(2⁷) | 2⁷-1 | -| `TINYINT UNSIGNED` | `U8` | 1 | 0 | 2⁸-1 | -| `SMALLINT` | `I16` | 1 | -(2¹⁵) | 2¹⁵-1 | -| `SMALLINT UNSIGNED` | `U16` | 1 | 0 | 2¹⁶-1 | -| `INT`, `INTEGER` | `I32` | 1 | -(2³¹) | 2³¹-1 | -| `INT UNSIGNED` | `U32` | 1 | 0 | 2³²-1 | -| `BIGINT` | `I64` | 1 | -(2⁶³) | 2⁶³-1 | -| `BIGINT UNSIGNED` | `U64` | 1 | 0 | 2⁶⁴-1 | - -#### Floating-point types - -SpacetimeDB supports single- and double-precision [binary IEEE-754 floats](https://en.wikipedia.org/wiki/IEEE_754). - -| SQL | SATS | Example | Min | Max | -| ----------------- | ----- | ------- | ------------------------ | ----------------------- | -| `REAL` | `F32` | 1.0 | -3.40282347E+38 | 3.40282347E+38 | -| `DOUBLE`, `FLOAT` | `F64` | 1.0 | -1.7976931348623157E+308 | 1.7976931348623157E+308 | - -### Text types - -SpacetimeDB supports a single string type, `String`. SpacetimeDB strings are UTF-8 encoded. - -| SQL | SATS | Example | Notes | -| ----------------------------------------------- | -------- | ------- | -------------------- | -| `CHAR`, `VARCHAR`, `NVARCHAR`, `TEXT`, `STRING` | `String` | 'hello' | Always UTF-8 encoded | - -> SpacetimeDB SQL currently does not support length contraints like `CHAR(10)`. - -## Syntax - -### Comments - -SQL line comments begin with `--`. - -```sql --- This is a comment -``` - -### Expressions - -We can express different, composable, values that are universally called `expressions`. - -An expression is one of the following: - -#### Literals - -| Example | Description | -| --------- | ----------- | -| `1` | An integer. | -| `1.0` | A float. | -| `'hello'` | A string. | -| `true` | A boolean. | - -#### Binary operators - -| Example | Description | -| ------- | ------------------- | -| `1 > 2` | Integer comparison. | -| `1 + 2` | Integer addition. | - -#### Logical expressions - -Any expression which returns a boolean, i.e. `true` or `false`, is a logical expression. - -| Example | Description | -| ---------------- | ------------------------------------------------------------ | -| `1 > 2` | Integer comparison. | -| `1 + 2 == 3` | Equality comparison between a constant and a computed value. | -| `true AND false` | Boolean and. | -| `true OR false` | Boolean or. | -| `NOT true` | Boolean inverse. | - -#### Function calls - -| Example | Description | -| --------------- | -------------------------------------------------- | -| `lower('JOHN')` | Apply the function `lower` to the string `'JOHN'`. | - -#### Table identifiers - -| Example | Description | -| ------------- | ------------------------- | -| `inventory` | Refers to a table. | -| `"inventory"` | Refers to the same table. | - -#### Column references - -| Example | Description | -| -------------------------- | ------------------------------------------------------- | -| `inventory_id` | Refers to a column. | -| `"inventory_id"` | Refers to the same column. | -| `"inventory.inventory_id"` | Refers to the same column, explicitly naming its table. | - -#### Wildcards - -Special "star" expressions which select all the columns of a table. - -| Example | Description | -| ------------- | ------------------------------------------------------- | -| `*` | Refers to all columns of a table identified by context. | -| `inventory.*` | Refers to all columns of the `inventory` table. | - -#### Parenthesized expressions - -Sub-expressions can be enclosed in parentheses for grouping and to override operator precedence. - -| Example | Description | -| ------------- | ----------------------- | -| `1 + (2 / 3)` | One plus a fraction. | -| `(1 + 2) / 3` | A sum divided by three. | - -### `CREATE TABLE` - -A `CREATE TABLE` statement creates a new, initially empty table in the database. - -The syntax of the `CREATE TABLE` statement is: - -> **CREATE TABLE** _table_name_ (_column_name_ _data_type_, ...); - -![create-table](create_table.svg) - -#### Examples - -Create a table `inventory` with two columns, an integer `inventory_id` and a string `name`: - -```sql -CREATE TABLE inventory (inventory_id INTEGER, name TEXT); -``` - -Create a table `player` with two integer columns, an `entity_id` and an `inventory_id`: - -```sql -CREATE TABLE player (entity_id INTEGER, inventory_id INTEGER); -``` - -Create a table `location` with three columns, an integer `entity_id` and floats `x` and `z`: - -```sql -CREATE TABLE location (entity_id INTEGER, x REAL, z REAL); -``` - -### `DROP TABLE` - -A `DROP TABLE` statement removes a table from the database, deleting all its associated rows, indexes, constraints and sequences. - -To empty a table of rows without destroying the table, use [`DELETE`](#delete.). - -The syntax of the `DROP TABLE` statement is: - -> **DROP TABLE** _table_name_; - -![drop-table](drop_table.svg) - -Examples: - -```sql -DROP TABLE inventory; -``` - -## Queries - -### `FROM` - -A `FROM` clause derives a data source from a table name. - -The syntax of the `FROM` clause is: - -> **FROM** _table_name_ _join_clause_?; - -![from](from.svg) - -#### Examples - -Select all rows from the `inventory` table: - -```sql -SELECT * FROM inventory; -``` - -### `JOIN` - -A `JOIN` clause combines two data sources into a new data source. - -Currently, SpacetimeDB SQL supports only inner joins, which return rows from two data sources where the values of two columns match. - -The syntax of the `JOIN` clause is: - -> **JOIN** _table_name_ **ON** _expr_ = _expr_; - -![join](join.svg) - -### Examples - -Select all players rows who have a corresponding location: - -```sql -SELECT player.* FROM player - JOIN location - ON location.entity_id = player.entity_id; -``` - -Select all inventories which have a corresponding player, and where that player has a corresponding location: - -```sql -SELECT inventory.* FROM inventory - JOIN player - ON inventory.inventory_id = player.inventory_id - JOIN location - ON player.entity_id = location.entity_id; -``` - -### `SELECT` - -A `SELECT` statement returns values of particular columns from a data source, optionally filtering the data source to include only rows which satisfy a `WHERE` predicate. - -The syntax of the `SELECT` command is: - -> **SELECT** _column_expr_ > **FROM** _from_expr_ -> {**WHERE** _expr_}? - -![sql-select](select.svg) - -#### Examples - -Select all columns of all rows from the `inventory` table: - -```sql -SELECT * FROM inventory; -SELECT inventory.* FROM inventory; -``` - -Select only the `inventory_id` column of all rows from the `inventory` table: - -```sql -SELECT inventory_id FROM inventory; -SELECT inventory.inventory_id FROM inventory; -``` - -An optional `WHERE` clause can be added to filter the data source using a [logical expression](#logical-expressions.). The `SELECT` will return only the rows from the data source for which the expression returns `true`. - -#### Examples - -Select all columns of all rows from the `inventory` table, with a filter that is always true: - -```sql -SELECT * FROM inventory WHERE 1 = 1; -``` - -Select all columns of all rows from the `inventory` table with the `inventory_id` 1: - -```sql -SELECT * FROM inventory WHERE inventory_id = 1; -``` - -Select only the `name` column of all rows from the `inventory` table with the `inventory_id` 1: - -```sql -SELECT name FROM inventory WHERE inventory_id = 1; -``` - -Select all columns of all rows from the `inventory` table where the `inventory_id` is 2 or greater: - -```sql -SELECT * FROM inventory WHERE inventory_id > 1; -``` - -### `INSERT` - -An `INSERT INTO` statement inserts new rows into a table. - -One can insert one or more rows specified by value expressions. - -The syntax of the `INSERT INTO` statement is: - -> **INSERT INTO** _table_name_ (_column_name_, ...) **VALUES** (_expr_, ...), ...; - -![sql-insert](insert.svg) - -#### Examples - -Insert a single row: - -```sql -INSERT INTO inventory (inventory_id, name) VALUES (1, 'health1'); -``` - -Insert two rows: - -```sql -INSERT INTO inventory (inventory_id, name) VALUES (1, 'health1'), (2, 'health2'); -``` - -### UPDATE - -An `UPDATE` statement changes the values of a set of specified columns in all rows of a table, optionally filtering the table to update only rows which satisfy a `WHERE` predicate. - -Columns not explicitly modified with the `SET` clause retain their previous values. - -If the `WHERE` clause is absent, the effect is to update all rows in the table. - -The syntax of the `UPDATE` statement is - -> **UPDATE** _table_name_ **SET** > _column_name_ = _expr_, ... -> {_WHERE expr_}?; - -![sql-update](update.svg) - -#### Examples - -Set the `name` column of all rows from the `inventory` table with the `inventory_id` 1 to `'new name'`: - -```sql -UPDATE inventory - SET name = 'new name' - WHERE inventory_id = 1; -``` - -### DELETE - -A `DELETE` statement deletes rows that satisfy the `WHERE` clause from the specified table. - -If the `WHERE` clause is absent, the effect is to delete all rows in the table. In that case, the result is a valid empty table. - -The syntax of the `DELETE` statement is - -> **DELETE** _table_name_ -> {**WHERE** _expr_}?; - -![sql-delete](delete.svg) - -#### Examples - -Delete all the rows from the `inventory` table with the `inventory_id` 1: - -```sql -DELETE FROM inventory WHERE inventory_id = 1; -``` - -Delete all rows from the `inventory` table, leaving it empty: - -```sql -DELETE FROM inventory; -``` diff --git a/Writerside2/topics/unity/homeless.md b/Writerside2/topics/unity/homeless.md deleted file mode 100644 index 121fe538..00000000 --- a/Writerside2/topics/unity/homeless.md +++ /dev/null @@ -1,355 +0,0 @@ -### Create the Module - -1. It is important that you already have the SpacetimeDB CLI tool [installed](install.). - -2. Run SpacetimeDB locally using the installed CLI. In a **new** terminal or command window, run the following command: - -```bash -spacetime start -``` - -💡 Standalone mode will run in the foreground. -💡 Below examples Rust language, [but you may also use C#](c-sharp_index.md). - -## Create a Server Module - -Run the following command to initialize the SpacetimeDB server module project with Rust as the language: - -```bash -spacetime init --lang=rust server -``` - -This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. - -### SpacetimeDB Tables - -In this section we'll be making some edits to the file `server/src/lib.cs`. We recommend you open up this file in an IDE like VSCode or RustRover. - -**Important: Open the `server/src/lib.cs` file and delete its contents. We will be writing it from scratch here.** - -First we need to add some imports at the top of the file. - -**Copy and paste into lib.cs:** - -```csharp -// using SpacetimeDB; // Uncomment to omit `SpacetimeDB` attribute prefixes -using SpacetimeDB.Module; -using static SpacetimeDB.Runtime; -``` - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](c-sharp.). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.cs:** - -```csharp -/// We're using this table as a singleton, -/// so there should typically only be one element where the version is 0. -[SpacetimeDB.Table] -public partial class Config -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public Identity Version; - public string? MessageOfTheDay; -} -``` - -Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. - -**Append to the bottom of lib.cs:** - -```csharp -/// This allows us to store 3D points in tables. -[SpacetimeDB.Type] -public partial class StdbVector3 -{ - public float X; - public float Y; - public float Z; -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```csharp -/// This stores information related to all entities in our game. In this tutorial -/// all entities must at least have an entity_id, a position, a direction and they -/// must specify whether or not they are moving. -[SpacetimeDB.Table] -public partial class EntityComponent -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - public ulong EntityId; - public StdbVector3 Position; - public float Direction; - public bool Moving; -} -``` - -Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. - -**Append to the bottom of lib.cs:** - -```csharp -/// All players have this component and it associates an entity with the user's -/// Identity. It also stores their username and whether or not they're logged in. -[SpacetimeDB.Table] -public partial class PlayerComponent -{ - // An EntityId that matches an EntityId in the `EntityComponent` table. - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public ulong EntityId; - - // The user's identity, which is unique to each player - [SpacetimeDB.Column(ColumnAttrs.Unique)] - public Identity Identity; - public string? Username; - public bool LoggedIn; -} -``` - -Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.cs:** - -```csharp -/// This reducer is called when the user logs in for the first time and -/// enters a username. -[SpacetimeDB.Reducer] -public static void CreatePlayer(DbEventArgs dbEvent, string username) -{ - // Get the Identity of the client who called this reducer - Identity sender = dbEvent.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"); - } - - // Create a new entity for this player - try - { - new EntityComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, - Direction = 0, - Moving = false, - }.Insert(); - } - catch - { - Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); - Throw; - } - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - try - { - new PlayerComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Identity = dbEvent.Sender, - Username = username, - LoggedIn = true, - }.Insert(); - } - catch - { - Log("Error: Failed to insert PlayerComponent", LogLevel.Error); - throw; - } - Log($"Player created: {username}"); -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. -- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the module is initially published -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void OnInit() -{ - try - { - new Config - { - Version = 0, - MessageOfTheDay = "Hello, World!", - }.Insert(); - } - catch - { - Log("Error: Failed to insert Config", LogLevel.Error); - throw; - } -} -``` - -We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the client connects, we update the LoggedIn state to true -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void ClientConnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:true); -``` -```csharp -/// Called when the client disconnects, we update the logged_in state to false -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void ClientDisonnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:false); -``` -```csharp -/// This helper function gets the PlayerComponent, sets the LoggedIn -/// variable and updates the PlayerComponent table row. -private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) -{ - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - player.LoggedIn = loggedIn; - PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); -} -``` - -Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. - -Using the `EntityId` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. - -**Append to the bottom of lib.cs:** - -```csharp -/// Updates the position of a player. This is also called when the player stops moving. -[SpacetimeDB.Reducer] -private static void UpdatePlayerPosition( - DbEventArgs dbEvent, - StdbVector3 position, - float direction, - bool moving) -{ - // First, look up the player using the sender identity - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - // Use the Player's EntityId to retrieve and update the EntityComponent - ulong playerEntityId = player.EntityId; - EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); - if (entity is null) - { - throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); - } - - entity.Position = position; - entity.Direction = direction; - entity.Moving = moving; - EntityComponent.UpdateByEntityId(playerEntityId, entity); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -### Finally, Add Chat Support - -The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -[SpacetimeDB.Table] -public partial class ChatMessage -{ - // The primary key for this table will be auto-incremented - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - - // The entity id of the player that sent the message - public ulong SenderId; - - // Message contents - public string? Text; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -/// Adds a chat entry to the ChatMessage table -[SpacetimeDB.Reducer] -public static void SendChatMessage(DbEventArgs dbEvent, string text) -{ - // Get the player's entity id - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - - // Insert the chat message - new ChatMessage - { - SenderId = player.EntityId, - Text = text, - }.Insert(); -} -``` - -## Wrapping Up - -💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) - -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside2/topics/unity/part-1.md b/Writerside2/topics/unity/part-1.md deleted file mode 100644 index bfad0644..00000000 --- a/Writerside2/topics/unity/part-1.md +++ /dev/null @@ -1,57 +0,0 @@ -# Unity Multiplayer Tutorial - -## Part 1 of 3: Setup - -This tutorial will guide you through setting up a multiplayer game project using Unity and SpacetimeDB. We will start by cloning the project, connecting it to SpacetimeDB and running the project. - -💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! - -> [!IMPORTANT] -> TODO: This draft may link to WIP repos or docs - be sure to replace with final links after prerequisite PRs are approved (that are not yet approved upon writing this) - -## 1. Clone the Project - -Let's name it `SpacetimeDBUnityTutorial` for reference: -```bash -git clone https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade SpacetimeDBUnityTutorial -``` - -This project repo is separated into two sub-projects: - -1. [Server](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp) (SpacetimeDB Module) -1. [Client](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Client) (Unity project) - -> [!TIP] -> You may optionally _update_ the [SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk) via the Package Manager in Unity - -## 2. Publishing the Project - -From Unity, you don't need CLI commands for common functionality: - -1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) -1. Create an identity -> Select `testnet` for the server -1. Browse to your repo root `Server-Csharp` dir -> **Publish** -> **Generate** Unity files - -💡For the next section, we'll use the selected `Server` and publish result `Host` - -![Unity Publisher Tool](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-publisher.jpg) - -## 3. Connecting the Project - -1. Open `Scenes/Main` in Unity -> select the `GameManager` GameObject in the inspector. -1. Matching the earlier Publish setup: - 1. For the GameManager `Db Name or Address`, input `testnet` - 1. For the GameManager `Host`, input `https://testnet.spacetimedb.com -1. Save your scene - -## 4. Running the Project - -With the same `Main` scene open, press play! - -![Gameplay Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-action.jpg) - -![UI Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-ui.jpg) - -You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. - -Congratulations! You have successfully set up your multiplayer game project. In the next section, we will break down how Server Modules work and analyze the demo code. diff --git a/Writerside2/topics/unity/part-2.md b/Writerside2/topics/unity/part-2.md deleted file mode 100644 index 6856d42e..00000000 --- a/Writerside2/topics/unity/part-2.md +++ /dev/null @@ -1,489 +0,0 @@ -# Unity Multiplayer Tutorial - Part 2 - -## Analyzing the C# Server Module - -This progressive tutorial is continued from [Part 1](part-11.md). - -In this part of the tutorial, we will create a SpacetimeDB (SpacetimeDB) server module using C# for the Unity multiplayer game. The server module will handle the game logic and data management for the game. - -💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! - -## The Entity Component Systems (ECS) - -Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). - -We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. - -## C# Module Limitations & Nuances - -Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), you may run into unexpected issues until aware of the following: - -1. No DateTime-like types in Types or Tables: - - Use `string` for timestamps (exampled at [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs)), or `long` for Unix Epoch time. - -2. No Timers or async/await, such as those to create repeating loops: - - For repeating invokers, instead **re**schedule it from within a fired [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) function. - -3. Using `Debug` advanced option in the `Publisher` Unity editor tool will add callstack symbols for easier debugging: - - However, avoid using `Debug` mode when publishing outside a `localhost` server: - - Due to WASM buffer size limitations, this may cause publish failure. - -4. If you `throw` a new `Exception`, no error logs will appear. Instead, use either: - 1. Use `Log(message, LogLevel.Error);` before you throw. - 2. Use the demo's static [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs) class to `Utils.Throw()` to wrap the error log before throwing. - -5. `[AutoIncrement]` or `[PrimaryKeyAuto]` will never equal 0: - - Inserting a new row with an Auto key equaling 0 will always return a unique, non-0 value. - - -6. Enums cannot declare values out of the default order: - - For example, `{ Foo = 0, Bar = 3 }` will fail to compile. - -## Namespaces - -Common `using` statements include: - -```csharp -using SpacetimeDB; // Contains class|func|struct attributes like [Table], [Type], [Reducer] -using static SpacetimeDB.Runtime; // Contains Identity DbEventArgs, Log() -using SpacetimeDB.Module; // Contains prop attributes like [Column] -using Module.Utils; // Helper to workaround the `throw` and `DateTime` limitations noted above -``` - -- You will mostly see `SpacetimeDB.Module` in [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs) for schema definitions -- `SpacetimeDB` and `SpacetimeDB.Runtime` can be found in most all SpacetimeDB scripts -- `Module.Utils` parse DateTimeOffset into a timestamp string and wraps `throw` with error logs - -## Partial Classes & Structs - -- Throughout the demo, you will notice most classes or structs with a SpacetimeDB [Attribute] such as `[Table]` or `[Reducer]` will be defined with the `partial` keyword. - -- This allows the _Roslyn Compiler_ to [incrementally generate](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) additions to the SpacetimeDB SDK, such as adding helper functions and utilities. This means SpacetimeDB takes care of all the low-level tooling for you, such as inserting, updating or querying the DB. - - This further allows you to separate your models from logic within the same class. - -* Notice that the module class, itself, is also a `static partial class`. - -## Types & Tables - -`[Table]` attributes are database columns, while `[Type]` attributes are define a schema. - -### Types - -`[Type]` attributes attach to properties containing `[Table]` attributes when you want to use a custom Type that's not [SpacetimeDB natively-supported](c-sharp#supported-types.). These are generally defined as a `partial struct` or `partial class` - -Let's inspect a real example `Type`; open [Server-cs/Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): - -In Unity, you are likely familiar with the `Vector2` type. In SpacetimeDB, let's inspect the `StdbVector2` type to store 2D positions in the database: - -```csharp -/// A spacetime type which can be used in tables & reducers to represent a 2D position (such as movement) -[Type] -public partial class StdbVector2 -{ - public float X; - public float Z; - - // This allows us to use StdbVector2::ZERO in reducers - public static readonly StdbVector2 ZERO = new() - { - X = 0, - Z = 0, - }; -} -``` - -- Since `Types` are used in `Tables`, we can now use a custom SpacetimeDB `StdbVector3` `Type` in a `[Table]`. - -We may optionally include `static readonly` property "helper" functions such as the above-exampled `ZERO`. - -### Tables - -`[Table] attributes use `[Type]`s - either custom (like `StdbVector2` above) or [SpacetimeDB natively-supported types](../modules/c-sharp#supported-types). These are generally defined as a `struct` or `class` - -Let's inspect a real example `Table`, looking again at [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs): - -```csharp -/// Represents chat messages within the game, including the sender and message content -[Table] -public partial class ChatMessage -{ - /// Primary key, automatically incremented - [Column(ColumnAttrs.PrimaryKeyAuto)] - public ulong ChatEntityId; - - /// The entity id of the player (or NPC) that sent the message - public ulong SourceEntityId; - - /// Message contents - public string? ChatText; - - /// - /// Stringified ISO 8601 format (Unix Epoch Time) - /// - /// DateTime.ToUniversalTime().ToString("o"); - /// - public static string GetTimestamp(DateTimeOffset dateTimeOffset) => - dateTimeOffset.ToUniversalTime().ToString("o"); -} -``` - -- The `Id` vars are `ulong` types, commonly used for SpacetimeDB unique identifiers -- Notice how `Timestamp` is a `string` instead of DateTimeOffset (a limitation mentioned earlier). - -```csharp -/// This component will be created for all world objects that can move smoothly throughout the world, keeping track -/// of position, the last time the component was updated & the direction the mobile object is currently moving. -[Table] -public partial class MobileEntityComponent -{ - /// Primary key for the mobile entity - [Column(ColumnAttrs.PrimaryKey)] - public ulong EntityId; - - /// The last known location of this entity - public StdbVector2? Location; - - /// Movement direction, {0,0} if not moving at all. - public StdbVector2? Direction; - - /// Timestamp when movement started. Timestamp::UNIX_EPOCH if not moving. - public string? MoveStartTimestamp; -} -``` - -**Let's break this down:** - -- `EntityId` is the unique identifier for the table, declared as a `ulong` -- Location and Direction are both `StdbVector2` types discussed above -- `MoveStartTimestamp` is a string of epoch time, as you cannot use `DateTime`-like types within Tables. - - See the [Limitations](#limitations.) section below - -## Reducers - -Reducers are cloud functions that run on the server and can be called from the client, always returning `void`. - -Looking at the most straight-forward example, open [Chat.cs]( - - - - - - - -```csharp - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](/docs/modules/c-sharp). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.cs:** - -```csharp -/// We're using this table as a singleton, -/// so there should typically only be one element where the version is 0. -[SpacetimeDB.Table] -public partial class Config -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public Identity Version; - public string? MessageOfTheDay; -} -``` - -Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. - -**Append to the bottom of lib.cs:** - -```csharp -/// This allows us to store 3D points in tables. -[SpacetimeDB.Type] -public partial class StdbVector3 -{ - public float X; - public float Y; - public float Z; -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```csharp -/// This stores information related to all entities in our game. In this tutorial -/// all entities must at least have an entity_id, a position, a direction and they -/// must specify whether or not they are moving. -[SpacetimeDB.Table] -public partial class EntityComponent -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - public ulong EntityId; - public StdbVector3 Position; - public float Direction; - public bool Moving; -} -``` - -Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. - -**Append to the bottom of lib.cs:** - -```csharp -/// All players have this component and it associates an entity with the user's -/// Identity. It also stores their username and whether or not they're logged in. -[SpacetimeDB.Table] -public partial class PlayerComponent -{ - // An EntityId that matches an EntityId in the `EntityComponent` table. - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public ulong EntityId; - - // The user's identity, which is unique to each player - [SpacetimeDB.Column(ColumnAttrs.Unique)] - public Identity Identity; - public string? Username; - public bool LoggedIn; -} -``` - -Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.cs:** - -```csharp -/// This reducer is called when the user logs in for the first time and -/// enters a username. -[SpacetimeDB.Reducer] -public static void CreatePlayer(DbEventArgs dbEvent, string username) -{ - // Get the Identity of the client who called this reducer - Identity sender = dbEvent.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"); - } - - // Create a new entity for this player - try - { - new EntityComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, - Direction = 0, - Moving = false, - }.Insert(); - } - catch - { - Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); - Throw; - } - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - try - { - new PlayerComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Identity = dbEvent.Sender, - Username = username, - LoggedIn = true, - }.Insert(); - } - catch - { - Log("Error: Failed to insert PlayerComponent", LogLevel.Error); - throw; - } - Log($"Player created: {username}"); -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. -- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the module is initially published -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void OnInit() -{ - try - { - new Config - { - Version = 0, - MessageOfTheDay = "Hello, World!", - }.Insert(); - } - catch - { - Log("Error: Failed to insert Config", LogLevel.Error); - throw; - } -} -``` - -We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the client connects, we update the LoggedIn state to true -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void ClientConnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:true); -``` -```csharp -/// Called when the client disconnects, we update the logged_in state to false -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void ClientDisonnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:false); -``` -```csharp -/// This helper function gets the PlayerComponent, sets the LoggedIn -/// variable and updates the PlayerComponent table row. -private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) -{ - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - player.LoggedIn = loggedIn; - PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); -} -``` - -Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. - -Using the `EntityId` in the `PlayerComponent` we retrieved, we can look up the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. - -**Append to the bottom of lib.cs:** - -```csharp -/// Updates the position of a player. This is also called when the player stops moving. -[SpacetimeDB.Reducer] -private static void UpdatePlayerPosition( - DbEventArgs dbEvent, - StdbVector3 position, - float direction, - bool moving) -{ - // First, look up the player using the sender identity - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - // Use the Player's EntityId to retrieve and update the EntityComponent - ulong playerEntityId = player.EntityId; - EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); - if (entity is null) - { - throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); - } - - entity.Position = position; - entity.Direction = direction; - entity.Moving = moving; - EntityComponent.UpdateByEntityId(playerEntityId, entity); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -### Finally, Add Chat Support - -The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -[SpacetimeDB.Table] -public partial class ChatMessage -{ - // The primary key for this table will be auto-incremented - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - - // The entity id of the player that sent the message - public ulong SenderId; - - // Message contents - public string? Text; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -/// Adds a chat entry to the ChatMessage table -[SpacetimeDB.Reducer] -public static void SendChatMessage(DbEventArgs dbEvent, string text) -{ - // Get the player's entity id - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - - // Insert the chat message - new ChatMessage - { - SenderId = player.EntityId, - Text = text, - }.Insert(); -} -``` - -## Wrapping Up - -💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) - -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside2/topics/unity/part-2a-rust.md b/Writerside2/topics/unity/part-2a-rust.md deleted file mode 100644 index 1271b345..00000000 --- a/Writerside2/topics/unity/part-2a-rust.md +++ /dev/null @@ -1,316 +0,0 @@ -# Unity Tutorial - Basic Multiplayer - Part 2a - Server Module (Rust) - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 1 Tutorial](part-11.md) - -## Create a Server Module - -Run the following command to initialize the SpacetimeDB server module project with Rust as the language: - -```bash -spacetime init --lang=rust server -``` - -This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. - -### SpacetimeDB Tables - -In this section we'll be making some edits to the file `server/src/lib.rs`. We recommend you open up this file in an IDE like VSCode or RustRover. - -**Important: Open the `server/src/lib.rs` file and delete its contents. We will be writing it from scratch here.** - -First we need to add some imports at the top of the file. - -**Copy and paste into lib.rs:** - -```rust -use spacetimedb::{spacetimedb, Identity, SpacetimeType, ReducerContext}; -use log; -``` - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [Rust module reference](rust.). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.rs:** - -```rust -// We're using this table as a singleton, so there should typically only be one element where the version is 0. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct Config { - #[primarykey] - pub version: u32, - pub message_of_the_day: String, -} -``` - -Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `#[derive(SpacetimeType)]` and a `#[spacetimedb(table)]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. - -**Append to the bottom of lib.rs:** - -```rust -// This allows us to store 3D points in tables. -#[derive(SpacetimeType, Clone)] -pub struct StdbVector3 { - pub x: f32, - pub y: f32, - pub z: f32, -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```rust -// This stores information related to all entities in our game. In this tutorial -// all entities must at least have an entity_id, a position, a direction and they -// must specify whether or not they are moving. -#[spacetimedb(table)] -#[derive(Clone)] -pub struct EntityComponent { - #[primarykey] - // The autoinc macro here just means every time we insert into this table - // we will receive a new row where this value will be increased by one. This - // allows us to easily get rows where `entity_id` is unique. - #[autoinc] - pub entity_id: u64, - pub position: StdbVector3, - pub direction: f32, - pub moving: bool, -} -``` - -Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `entity_id`. You'll see how this works later in the `create_player` reducer. - -**Append to the bottom of lib.rs:** - -```rust -// All players have this component and it associates an entity with the user's -// Identity. It also stores their username and whether or not they're logged in. -#[derive(Clone)] -#[spacetimedb(table)] -pub struct PlayerComponent { - // An entity_id that matches an entity_id in the `EntityComponent` table. - #[primarykey] - pub entity_id: u64, - - // The user's identity, which is unique to each player - #[unique] - pub owner_id: Identity, - pub username: String, - pub logged_in: bool, -} -``` - -Next, we write our very first reducer, `create_player`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.rs:** - -```rust -// This reducer is called when the user logs in for the first time and -// enters a username -#[spacetimedb(reducer)] -pub fn create_player(ctx: ReducerContext, username: String) -> Result<(), String> { - // Get the Identity of the client who called this reducer - let owner_id = ctx.sender; - - // Make sure we don't already have a player with this identity - if PlayerComponent::filter_by_owner_id(&owner_id).is_some() { - log::info!("Player already exists"); - return Err("Player already exists".to_string()); - } - - // Create a new entity for this player and get a unique `entity_id`. - let entity_id = EntityComponent::insert(EntityComponent - { - entity_id: 0, - position: StdbVector3 { x: 0.0, y: 0.0, z: 0.0 }, - direction: 0.0, - moving: false, - }).expect("Failed to create a unique PlayerComponent.").entity_id; - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - PlayerComponent::insert(PlayerComponent { - entity_id, - owner_id, - username: username.clone(), - logged_in: true, - }).expect("Failed to insert player component."); - - log::info!("Player created: {}({})", username, entity_id); - - Ok(()) -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `sender` value of the `ReducerContext`. -- `disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. - -**Append to the bottom of lib.rs:** - -```rust -// Called when the module is initially published -#[spacetimedb(init)] -pub fn init() { - Config::insert(Config { - version: 0, - message_of_the_day: "Hello, World!".to_string(), - }).expect("Failed to insert config."); -} -``` - -We use the `connect` and `disconnect` reducers to update the logged in state of the player. The `update_player_login_state` helper function looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `logged_in` variable and calls the auto-generated `update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.rs:** - -```rust -// Called when the client connects, we update the logged_in state to true -#[spacetimedb(connect)] -pub fn client_connected(ctx: ReducerContext) { - update_player_login_state(ctx, true); -} -``` -```rust -// Called when the client disconnects, we update the logged_in state to false -#[spacetimedb(disconnect)] -pub fn client_disconnected(ctx: ReducerContext) { - update_player_login_state(ctx, false); -} -``` -```rust -// This helper function gets the PlayerComponent, sets the logged -// in variable and updates the PlayerComponent table row. -pub fn update_player_login_state(ctx: ReducerContext, logged_in: bool) { - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - // We clone the PlayerComponent so we can edit it and pass it back. - let mut player = player.clone(); - player.logged_in = logged_in; - PlayerComponent::update_by_entity_id(&player.entity_id.clone(), player); - } -} -``` - -Our final reducer handles player movement. In `update_player_position` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `create_player` first. - -Using the `entity_id` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `update` function. - -**Append to the bottom of lib.rs:** - -```rust -// Updates the position of a player. This is also called when the player stops moving. -#[spacetimedb(reducer)] -pub fn update_player_position( - ctx: ReducerContext, - position: StdbVector3, - direction: f32, - moving: bool, -) -> Result<(), String> { - // First, look up the player using the sender identity, then use that - // entity_id to retrieve and update the EntityComponent - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - if let Some(mut entity) = EntityComponent::filter_by_entity_id(&player.entity_id) { - entity.position = position; - entity.direction = direction; - entity.moving = moving; - EntityComponent::update_by_entity_id(&player.entity_id, entity); - return Ok(()); - } - } - - // If we can not find the PlayerComponent or EntityComponent for - // this player then something went wrong. - return Err("Player not found".to_string()); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -### Finally, Add Chat Support - -The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.rs``. - -**Append to the bottom of server/src/lib.rs:** - -```rust -#[spacetimedb(table)] -pub struct ChatMessage { - // The primary key for this table will be auto-incremented - #[primarykey] - #[autoinc] - pub message_id: u64, - - // The entity id of the player that sent the message - pub sender_id: u64, - // Message contents - pub text: String, -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.rs:** - -```rust -// Adds a chat entry to the ChatMessage table -#[spacetimedb(reducer)] -pub fn send_chat_message(ctx: ReducerContext, text: String) -> Result<(), String> { - if let Some(player) = PlayerComponent::filter_by_owner_id(&ctx.sender) { - // Now that we have the player we can insert the chat message using the player entity id. - ChatMessage::insert(ChatMessage { - // this column auto-increments so we can set it to 0 - message_id: 0, - sender_id: player.entity_id, - text, - }) - .unwrap(); - - return Ok(()); - } - - Err("Player not found".into()) -} -``` - -## Wrapping Up - -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.rs`. You can also look at the [Client Troubleshooting](part-3.md#Troubleshooting) section. - -From here, the tutorial continues with more-advanced topics. The [next tutorial](part-41.md) introduces Resources & Scheduling. diff --git a/Writerside2/topics/unity/part-3.md b/Writerside2/topics/unity/part-3.md deleted file mode 100644 index 12e85ef3..00000000 --- a/Writerside2/topics/unity/part-3.md +++ /dev/null @@ -1,479 +0,0 @@ -# Unity Tutorial - Basic Multiplayer - Part 3 - Client - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from one of the Part 2 tutorials: -- [Rust Server Module](part-2a-rust1.md) -- [C# Server Module](part-2.) - -## Updating our Unity Project Client to use SpacetimeDB - -Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. - -### Import the SDK and Generate Module Files - -1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. - -```bash -https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git -``` - -![Unity-PackageManager](Unity-PackageManager.JPG) - -3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. - -```bash -mkdir -p ../client/Assets/module_bindings -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -``` - -### Connect to Your SpacetimeDB Module - -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. - -![Unity-AddNetworkManager](Unity-AddNetworkManager.JPG) - -Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: - -**Append to the top of TutorialGameManager.cs** - -```csharp -using SpacetimeDB; -using SpacetimeDB.Types; -using System.Linq; -``` - -At the top of the class definition add the following members: - -**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** - -```csharp -// These are connection variables that are exposed on the GameManager -// inspector. -[SerializeField] private string moduleAddress = "unity-tutorial"; -[SerializeField] private string hostName = "localhost:3000"; - -// This is the identity for this player that is automatically generated -// the first time you log in. We set this variable when the -// onIdentityReceived callback is triggered by the SDK after connecting -private Identity local_identity; -``` - -The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. - -Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. - -**REPLACE the Start() function in TutorialGameManager.cs** - -```csharp -// Start is called before the first frame update -void Start() -{ - instance = this; - - SpacetimeDBClient.instance.onConnect += () => - { - Debug.Log("Connected."); - - // Request all tables - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM *", - }); - }; - - // Called when we have an error connecting to SpacetimeDB - SpacetimeDBClient.instance.onConnectError += (error, message) => - { - Debug.LogError($"Connection error: " + message); - }; - - // Called when we are disconnected from SpacetimeDB - SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => - { - Debug.Log("Disconnected."); - }; - - // Called when we receive the client identity from SpacetimeDB - SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { - AuthToken.SaveToken(token); - local_identity = identity; - }; - - // Called after our local cache is populated from a Subscribe call - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - - // Now that we’ve registered all our callbacks, lets connect to spacetimedb - SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); -} -``` - -In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. - -Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. - ---- - -**Local Client Cache** - -The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. - ---- - -Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. - -**Append after the Start() function in TutorialGameManager.cs** - -```csharp -void OnSubscriptionApplied() -{ - // If we don't have any data for our player, then we are creating a - // new one. Let's show the username dialog, which will then call the - // create player reducer - var player = PlayerComponent.FilterByOwnerId(local_identity); - if (player == null) - { - // Show username selection - UIUsernameChooser.instance.Show(); - } - - // Show the Message of the Day in our Config table of the Client Cache - UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); - - // Now that we've done this work we can unregister this callback - SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; -} -``` - -### Adding the Multiplayer Functionality - -Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. - -**Append to the top of UIUsernameChooser.cs** - -```csharp -using SpacetimeDB.Types; -``` - -Then we're doing a modification to the `ButtonPressed()` function: - -**Modify the ButtonPressed function in UIUsernameChooser.cs** - -```csharp -public void ButtonPressed() -{ - CameraController.RemoveDisabler(GetHashCode()); - _panel.SetActive(false); - - // Call the SpacetimeDB CreatePlayer reducer - Reducer.CreatePlayer(_usernameField.text); -} -``` - -We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. - -First append this using to the top of `RemotePlayer.cs` - -**Create file RemotePlayer.cs, then replace its contents:** - -```csharp -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using SpacetimeDB.Types; -using TMPro; - -public class RemotePlayer : MonoBehaviour -{ - public ulong EntityId; - - public TMP_Text UsernameElement; - - public string Username { set { UsernameElement.text = value; } } - - void Start() - { - // Initialize overhead name - UsernameElement = GetComponentInChildren(); - var canvas = GetComponentInChildren(); - canvas.worldCamera = Camera.main; - - // Get the username from the PlayerComponent for this object and set it in the UI - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - if (playerComp is null) - { - string inputUsername = UsernameElement.Text; - Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); - Reducer.CreatePlayer(inputUsername); - - // Try again, optimistically assuming success for simplicity - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - } - - Username = playerComp.Username; - - // Get the last location for this player and set the initial position - EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); - transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - - // Register for a callback that is called when the client gets an - // update for a row in the EntityComponent table - EntityComponent.OnUpdate += EntityComponent_OnUpdate; - } -} -``` - -We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. - -**Append to bottom of RemotePlayer class in RemotePlayer.cs:** - -```csharp -private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) -{ - // If the update was made to this object - if(obj.EntityId == EntityId) - { - var movementController = GetComponent(); - - // Update target position, rotation, etc. - movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); - movementController.RemoteTargetRotation = obj.Direction; - movementController.SetMoving(obj.Moving); - } -} -``` - -Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. - -**Append to bottom of Start() function in TutorialGameManager.cs:** - -```csharp -PlayerComponent.OnInsert += PlayerComponent_OnInsert; -``` - -Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. - -**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** - -```csharp -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } -} -``` - -Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. - -**Append to the top of LocalPlayer.cs** - -```csharp -using SpacetimeDB.Types; -using SpacetimeDB; -``` - -**Append to the bottom of LocalPlayer class in LocalPlayer.cs** - -```csharp -private float? lastUpdateTime; -private void FixedUpdate() -{ - float? deltaTime = Time.time - lastUpdateTime; - bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; - bool isConnected = SpacetimeDBClient.instance.IsConnected(); - - if (hasUpdatedRecently || !isConnected) - { - return; - } - - lastUpdateTime = Time.time; - var p = PlayerMovementController.Local.GetModelPosition(); - - Reducer.UpdatePlayerPosition(new StdbVector3 - { - X = p.x, - Y = p.y, - Z = p.z, - }, - PlayerMovementController.Local.GetModelRotation(), - PlayerMovementController.Local.IsMoving()); -} -``` - -Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. - -![GameManager-Inspector2](GameManager-Inspector2.JPG) - -### Play the Game! - -Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. - -![Unity-AddOpenScenes](Unity-AddOpenScenes.JPG) - -When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. - -### Implement Player Logout - -So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. - -**Append to the bottom of Start() function in TutorialGameManager.cs** -```csharp -PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; -``` - -We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. - -Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. - -**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** -```csharp -private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(newValue); -} - -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(obj); -} - -private void OnPlayerComponentChanged(PlayerComponent obj) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it - var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); - if (obj.LoggedIn) - { - // Only spawn remote players who aren't already spawned - if (existingPlayer == null) - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } - } - else - { - if (existingPlayer != null) - { - Destroy(existingPlayer.gameObject); - } - } - } -} -``` - -Now you when you play the game you should see remote players disappear when they log out. - -Before updating the client, let's generate the client files and update publish our module. - -**Execute commands in the server/ directory** -```bash -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -spacetime publish -c unity-tutorial -``` - -On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. - -**Append to the top of UIChatController.cs:** -```csharp -using SpacetimeDB.Types; -``` - -**REPLACE the OnChatButtonPress function in UIChatController.cs:** - -```csharp -public void OnChatButtonPress() -{ - Reducer.SendChatMessage(_chatInput.text); - _chatInput.text = ""; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: - -**Append to the bottom of the Start() function in TutorialGameManager.cs:** -```csharp -Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; -``` - -Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. - -**Append after the Start() function in TutorialGameManager.cs** -```csharp -private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) -{ - var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); - if (player != null) - { - UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); - } -} -``` - -Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. - -## Conclusion - -This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: - -**Next Unity Tutorial:** [Resources & Scheduling](part-41.md) - ---- - -### Troubleshooting - -- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` - -- If you get this exception when running the project: - -``` -NullReferenceException: Object reference not set to an instance of an object -TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) -``` - -Check to see if your GameManager object in the Scene has the NetworkManager component attached. - -- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. - -``` -Connection error: Unable to connect to the remote server -``` diff --git a/Writerside2/topics/unity/part-4.md b/Writerside2/topics/unity/part-4.md deleted file mode 100644 index f17ac2b0..00000000 --- a/Writerside2/topics/unity/part-4.md +++ /dev/null @@ -1,261 +0,0 @@ -# Unity Tutorial - Advanced - Part 4 - Resources and Scheduling - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 3](part-31.md) Tutorial. - -**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** - -In this second part of the lesson, we'll add resource nodes to our project and learn about scheduled reducers. Then we will spawn the nodes on the client so they are visible to the player. - -## Add Resource Node Spawner - -In this section we will add functionality to our server to spawn the resource nodes. - -### Step 1: Add the SpacetimeDB Tables for Resource Nodes - -1. Before we start adding code to the server, we need to add the ability to use the rand crate in our SpacetimeDB module so we can generate random numbers. Open the `Cargo.toml` file in the `Server` directory and add the following line to the `[dependencies]` section. - -```toml -rand = "0.8.5" -``` - -We also need to add the `getrandom` feature to our SpacetimeDB crate. Update the `spacetimedb` line to: - -```toml -spacetimedb = { "0.5", features = ["getrandom"] } -``` - -2. The first entity component we are adding, `ResourceNodeComponent`, stores the resource type. We'll define an enum to describe a `ResourceNodeComponent`'s type. For now, we'll just have one resource type: Iron. In the future, though, we'll add more resources by adding variants to the `ResourceNodeType` enum. Since we are using a custom enum, we need to mark it with the `SpacetimeType` attribute. Add the following code to lib.rs. - -```rust -#[derive(SpacetimeType, Clone)] -pub enum ResourceNodeType { - Iron, -} - -#[spacetimedb(table)] -#[derive(Clone)] -pub struct ResourceNodeComponent { - #[primarykey] - pub entity_id: u64, - - // Resource type of this resource node - pub resource_type: ResourceNodeType, -} -``` - -Because resource nodes never move, the `MobileEntityComponent` is overkill. Instead, we will add a new entity component named `StaticLocationComponent` that only stores the position and rotation. - -```rust -#[spacetimedb(table)] -#[derive(Clone)] -pub struct StaticLocationComponent { - #[primarykey] - pub entity_id: u64, - - pub location: StdbVector2, - pub rotation: f32, -} -``` - -3. We are also going to add a couple of additional column to our Config table. `map_extents` let's our spawner know where it can spawn the nodes. `num_resource_nodes` is the maximum number of nodes to spawn on the map. Update the config table in lib.rs. - -```rust -#[spacetimedb(table)] -pub struct Config { - // Config is a global table with a single row. This table will be used to - // store configuration or global variables - - #[primarykey] - // always 0 - // having a table with a primarykey field which is always zero is a way to store singleton global state - pub version: u32, - - pub message_of_the_day: String, - - // new variables for resource node spawner - // X and Z range of the map (-map_extents to map_extents) - pub map_extents: u32, - // maximum number of resource nodes to spawn on the map - pub num_resource_nodes: u32, -} -``` - -4. In the `init` reducer, we need to set the initial values of our two new variables. Update the following code: - -```rust - Config::insert(Config { - version: 0, - message_of_the_day: "Hello, World!".to_string(), - - // new variables for resource node spawner - map_extents: 25, - num_resource_nodes: 10, - }) - .expect("Failed to insert config."); -``` - -### Step 2: Write our Resource Spawner Repeating Reducer - -1. Add the following code to lib.rs. We are using a special attribute argument called repeat which will automatically schedule the reducer to run every 1000ms. - -```rust -#[spacetimedb(reducer, repeat = 1000ms)] -pub fn resource_spawner_agent(_ctx: ReducerContext, _prev_time: Timestamp) -> Result<(), String> { - let config = Config::filter_by_version(&0).unwrap(); - - // Retrieve the maximum number of nodes we want to spawn from the Config table - let num_resource_nodes = config.num_resource_nodes as usize; - - // Count the number of nodes currently spawned and exit if we have reached num_resource_nodes - let num_resource_nodes_spawned = ResourceNodeComponent::iter().count(); - if num_resource_nodes_spawned >= num_resource_nodes { - log::info!("All resource nodes spawned. Skipping."); - return Ok(()); - } - - // Pick a random X and Z based off the map_extents - let mut rng = rand::thread_rng(); - let map_extents = config.map_extents as f32; - let location = StdbVector2 { - x: rng.gen_range(-map_extents..map_extents), - z: rng.gen_range(-map_extents..map_extents), - }; - // Pick a random Y rotation in degrees - let rotation = rng.gen_range(0.0..360.0); - - // Insert our SpawnableEntityComponent which assigns us our entity_id - let entity_id = SpawnableEntityComponent::insert(SpawnableEntityComponent { entity_id: 0 }) - .expect("Failed to create resource spawnable entity component.") - .entity_id; - - // Insert our static location with the random position and rotation we selected - StaticLocationComponent::insert(StaticLocationComponent { - entity_id, - location: location.clone(), - rotation, - }) - .expect("Failed to insert resource static location component."); - - // Insert our resource node component, so far we only have iron - ResourceNodeComponent::insert(ResourceNodeComponent { - entity_id, - resource_type: ResourceNodeType::Iron, - }) - .expect("Failed to insert resource node component."); - - // Log that we spawned a node with the entity_id and location - log::info!( - "Resource node spawned: {} at ({}, {})", - entity_id, - location.x, - location.z, - ); - - Ok(()) -} -``` - -2. Since this reducer uses `rand::Rng` we need add include it. Add this `use` statement to the top of lib.rs. - -```rust -use rand::Rng; -``` - -3. Even though our reducer is set to repeat, we still need to schedule it the first time. Add the following code to the end of the `init` reducer. You can use this `schedule!` macro to schedule any reducer to run in the future after a certain amount of time. - -```rust - // Start our resource spawner repeating reducer - spacetimedb::schedule!("1000ms", resource_spawner_agent(_, Timestamp::now())); -``` - -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: - -```bash -spacetime generate --out-dir ../Assets/autogen --lang=csharp - -spacetime publish -c yourname/bitcraftmini -``` - -Your resource node spawner will start as soon as you publish since we scheduled it to run in our init reducer. You can watch the log output by using the `--follow` flag on the logs CLI command. - -```bash -spacetime logs -f yourname/bitcraftmini -``` - -### Step 3: Spawn the Resource Nodes on the Client - -1. First we need to update the `GameResource` component in Unity to work for multiplayer. Open GameResource.cs and add `using SpacetimeDB.Types;` to the top of the file. Then change the variable `Type` to be of type `ResourceNodeType` instead of `int`. Also add a new variable called `EntityId` of type `ulong`. - -```csharp - public ulong EntityId; - - public ResourceNodeType Type = ResourceNodeType.Iron; -``` - -2. Now that we've changed the `Type` variable, we need to update the code in the `PlayerAnimator` component that references it. Open PlayerAnimator.cs and update the following section of code. We need to add `using SpacetimeDB.Types;` to this file as well. This fixes the compile errors that result from changing the type of the `Type` variable to our new server generated enum. - -```csharp - var resourceType = res?.Type ?? ResourceNodeType.Iron; - switch (resourceType) - { - case ResourceNodeType.Iron: - _animator.SetTrigger("Mine"); - Interacting = true; - break; - default: - Interacting = false; - break; - } - for (int i = 0; i < _tools.Length; i++) - { - _tools[i].SetActive(((int)resourceType) == i); - } - _target = res; -``` - -3. Now that our `GameResource` is ready to be spawned, lets update the `BitcraftMiniGameManager` component to actually create them. First, we need to add the new tables to our SpacetimeDB subscription. Open BitcraftMiniGameManager.cs and update the following code: - -```csharp - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM Config", - "SELECT * FROM SpawnableEntityComponent", - "SELECT * FROM PlayerComponent", - "SELECT * FROM MobileEntityComponent", - // Our new tables for part 2 of the tutorial - "SELECT * FROM ResourceNodeComponent", - "SELECT * FROM StaticLocationComponent" - }); -``` - -4. Next let's add an `OnInsert` handler for the `ResourceNodeComponent`. Add the following line to the `Start` function. - -```csharp - ResourceNodeComponent.OnInsert += ResourceNodeComponent_OnInsert; -``` - -5. Finally we add the new function to handle the insert event. This function will be called whenever a new `ResourceNodeComponent` is inserted into our local client cache. We can use this to spawn the resource node in the world. Add the following code to the `BitcraftMiniGameManager` class. - -To get the position and the rotation of the node, we look up the `StaticLocationComponent` for this entity by using the EntityId. - -```csharp - private void ResourceNodeComponent_OnInsert(ResourceNodeComponent insertedValue, ReducerEvent callInfo) - { - switch(insertedValue.ResourceType) - { - case ResourceNodeType.Iron: - var iron = Instantiate(IronPrefab); - StaticLocationComponent loc = StaticLocationComponent.FilterByEntityId(insertedValue.EntityId); - Vector3 nodePos = new Vector3(loc.Location.X, 0.0f, loc.Location.Z); - iron.transform.position = new Vector3(nodePos.x, MathUtil.GetTerrainHeight(nodePos), nodePos.z); - iron.transform.rotation = Quaternion.Euler(0.0f, loc.Rotation, 0.0f); - break; - } - } -``` - -### Step 4: Play the Game! - -6. Hit Play in the Unity Editor and you should now see your resource nodes spawning in the world! diff --git a/Writerside2/topics/unity/part-5.md b/Writerside2/topics/unity/part-5.md deleted file mode 100644 index d4274636..00000000 --- a/Writerside2/topics/unity/part-5.md +++ /dev/null @@ -1,108 +0,0 @@ -# Unity Tutorial - Advanced - Part 5 - BitCraft Mini - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 4](part-31.md) Tutorial. - -**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** - -BitCraft Mini is a game that we developed which extends the code you've already developed in this tutorial. It is inspired by our game [BitCraft](https://bitcraftonline.com) and illustrates how you could build a more complex game from just the components we've discussed. Right now you can walk around, mine ore, and manage your inventory. - -## 1. Download - -You can git-clone BitCraftMini from here: - -```plaintext -git clone ssh://git@github.com/clockworklabs/BitCraftMini -``` - -Once you have downloaded BitCraftMini, you will need to compile the spacetime module. - -## 2. Compile the Spacetime Module - -In order to compile the BitCraftMini module, you will need to install cargo. You can install cargo from here: - -> https://www.rust-lang.org/tools/install - -Once you have cargo installed, you can compile and publish the module with these commands: - -```bash -cd BitCraftMini/Server -spacetime publish -``` - -`spacetime publish` will output an address where your module has been deployed to. You will want to copy/save this address because you will need it in step 3. Here is an example of what it should look like: - -```plaintext -$ spacetime publish -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - Finished release [optimized] target(s) in 0.03s -Publish finished successfully. -Created new database with address: c91c17ecdcea8a05302be2bad9dd59b3 -``` - -Optionally, you can specify a name when you publish the module: - -```bash -spacetime publish "unique-module-name" -``` - -Currently, all the named modules exist in the same namespace so if you get a message saying that database is not owned by you, it means that someone else has already published a module with that name. You can either choose a different name or you can use the address instead. If you specify a name when you publish, you can use that name in place of the autogenerated address in both the CLI and in the Unity client. - -In the BitCraftMini module we have a function called `initialize()`. This function should be called immediately after publishing the module to spacetimedb. This function is in charge of generating some initial settings that are required for the server to operate. You can call this function like so: - -```bash -spacetime call "" "initialize" "[]" -``` - -Here we are telling spacetime to invoke the `initialize()` function on our module "bitcraftmini". If the function had some arguments, we would json encode them and put them into the "[]". Since `initialize()` requires no parameters, we just leave it empty. - -After you have called `initialize()` on the spacetime module you shouldgenerate the client files: - -```bash -spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs -``` - -Here is some sample output: - -```plaintext -$ spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang cs -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - Finished release [optimized] target(s) in 0.03s -compilation took 234.613518ms -Generate finished successfully. -``` - -If you've gotten this message then everything should be working properly so far. - -## 3. Replace address in BitCraftMiniGameManager - -The following settings are exposed in the `BitCraftMiniGameManager` inspector: Module Address, Host Name, and SSL Enabled. - -Open the Main scene in Unity and click on the `GameManager` object in the heirarchy. The inspector window will look like this: - -![GameManager-Inspector](GameManager-Inspector.JPG) - -Update the module address with the address you got from the `spacetime publish` command. If you are using SpacetimeDB Cloud `testnet`, the host name should be `testnet.spacetimedb.com` and SSL Enabled should be checked. If you are running SpacetimeDB Standalone locally, the host name should be `localhost:3000` and SSL Enabled should be unchecked. For instructions on how to deploy to these environments, see the [Deployment Section](testnet.) - -## 4. Play Mode - -You should now be able to enter play mode and walk around! You can mine some rocks, cut down some trees and if you connect more clients you can trade with other players. - -## 5. Editing the Module - -If you want to make further updates to the module, make sure to use this publish command instead: - -```bash -spacetime publish -``` - -Where `` is your own address. If you do this instead then you won't have to change the address inside of `BitCraftMiniGameManager.cs` - -When you change the server module you should also regenerate the client files as well: - -```bash -spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs -``` - -You may want to consider putting these 2 commands into a simple shell script to make the process a bit cleaner. diff --git a/Writerside2/topics/unity/unity_index.md b/Writerside2/topics/unity/unity_index.md deleted file mode 100644 index a16870f0..00000000 --- a/Writerside2/topics/unity/unity_index.md +++ /dev/null @@ -1,24 +0,0 @@ -# Unity Tutorial Overview - -💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! - -The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, Git, using a commandline terminal and coding. - -We'll give you some CLI commands to execute. If you are using Windows, we recommend using Git Bash or PowerShell. For Mac, we recommend Terminal. - -Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll be opening .cs files in an IDE like _Visual Studio_ or _Rider_. - -## Unity Tutorial - Basic Multiplayer -Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](rust.) or [C#](c-sharp.): - -- [Part 1 - Setup](part-11.md) -- [Part 2 - Server (C#)](part-21.md) ☼ -- [Part 3 - Client (Unity)](part-31.md) - -☼ While the tutorial uses C#, the repo cloned in [Part 1](part-11.md) does include a legacy Rust example to optionally use, instead. - -## Unity Tutorial - Advanced -By this point, you should already have a basic understanding of SpacetimeDB client, server and CLI: - -- [Part 4 - Resources & Scheduling](part-41.md) -- [Part 5 - BitCraft Mini](part-51.md) diff --git a/Writerside2/topics/webassembly-abi/webassembly-abi_index.md b/Writerside2/topics/webassembly-abi/webassembly-abi_index.md deleted file mode 100644 index ceccfbd1..00000000 --- a/Writerside2/topics/webassembly-abi/webassembly-abi_index.md +++ /dev/null @@ -1,499 +0,0 @@ -# Module ABI Reference - -This document specifies the _low level details_ of module-host interactions (_"Module ABI"_). _**Most users**_ looking to interact with the host will want to use derived and higher level functionality like [`bindings`], `#[spacetimedb(table)]`, and `#[derive(SpacetimeType)]` rather than this low level ABI. For more on those, read the [Rust module quick start][module_quick_start] guide and the [Rust module reference][module_ref]. - -The Module ABI is defined in [`bindings_sys::raw`] and is used by modules to interact with their host and perform various operations like: - -- logging, -- transporting data, -- scheduling reducers, -- altering tables, -- inserting and deleting rows, -- querying tables. - -In the next few sections, we'll define the functions that make up the ABI and what these functions do. - -## General notes - -The functions in this ABI all use the [`C` ABI on the `wasm32` platform][wasm_c_abi]. They are specified in a Rust `extern "C" { .. }` block. For those more familiar with the `C` notation, an [appendix][c_header] is provided with equivalent definitions as would occur in a `.h` file. - -Many functions in the ABI take in- or out-pointers, e.g. `*const u8` and `*mut u8`. The WASM host itself does not have undefined behavior. However, what WASM does not consider a memory access violation could be one according to some other language's abstract machine. For example, running the following on a WASM host would violate Rust's rules around writing across allocations: - -```rust -fn main() { - let mut bytes = [0u8; 12]; - let other_bytes = [0u8; 4]; - unsafe { ffi_func_with_out_ptr_and_len(&mut bytes as *mut u8, 16); } - assert_eq!(other_bytes, [0u8; 4]); -} -``` - -When we note in this reference that traps occur or errors are returned on memory access violations, we only mean those that WASM can directly detected, and not cases like the one above. - -Should memory access violations occur, such as a buffer overrun, undefined behavior will never result, as it does not exist in WASM. However, in many cases, an error code will result. - -Some functions will treat UTF-8 strings _lossily_. That is, if the slice identified by a `(ptr, len)` contains non-UTF-8 bytes, these bytes will be replaced with `�` in the read string. - -Most functions return a `u16` value. This is how these functions indicate an error where a `0` value means that there were no errors. Such functions will instead return any data they need to through out pointers. - -## Logging - -```rust -/// The error log level. -const LOG_LEVEL_ERROR: u8 = 0; -/// The warn log level. -const LOG_LEVEL_WARN: u8 = 1; -/// The info log level. -const LOG_LEVEL_INFO: u8 = 2; -/// The debug log level. -const LOG_LEVEL_DEBUG: u8 = 3; -/// The trace log level. -const LOG_LEVEL_TRACE: u8 = 4; -/// The panic log level. -/// -/// A panic level is emitted just before -/// a fatal error causes the WASM module to trap. -const LOG_LEVEL_PANIC: u8 = 101; - -/// Log at `level` a `text` message occuring in `filename:line_number` -/// with `target` being the module path at the `log!` invocation site. -/// -/// These various pointers are interpreted lossily as UTF-8 strings. -/// The data pointed to are copied. Ownership does not transfer. -/// -/// See https://docs.rs/log/latest/log/struct.Record.html#method.target -/// for more info on `target`. -/// -/// Calls to the function cannot fail -/// irrespective of memory access violations. -/// If they occur, no message is logged. -fn _console_log( - // The level we're logging at. - // One of the `LOG_*` constants above. - level: u8, - // The module path, if any, associated with the message - // or to "blame" for the reason we're logging. - // - // This is a pointer to a buffer holding an UTF-8 encoded string. - // When the pointer is `NULL`, `target` is ignored. - target: *const u8, - // The length of the buffer pointed to by `text`. - // Unused when `target` is `NULL`. - target_len: usize, - // The file name, if any, associated with the message - // or to "blame" for the reason we're logging. - // - // This is a pointer to a buffer holding an UTF-8 encoded string. - // When the pointer is `NULL`, `filename` is ignored. - filename: *const u8, - // The length of the buffer pointed to by `text`. - // Unused when `filename` is `NULL`. - filename_len: usize, - // The line number associated with the message - // or to "blame" for the reason we're logging. - line_number: u32, - // A pointer to a buffer holding an UTF-8 encoded message to log. - text: *const u8, - // The length of the buffer pointed to by `text`. - text_len: usize, -); -``` - -## Buffer handling - -```rust -/// Returns the length of buffer `bufh` without -/// transferring ownership of the data into the function. -/// -/// The `bufh` must have previously been allocating using `_buffer_alloc`. -/// -/// Traps if the buffer does not exist. -fn _buffer_len( - // The buffer previously allocated using `_buffer_alloc`. - // Ownership of the buffer is not taken. - bufh: ManuallyDrop -) -> usize; - -/// Consumes the buffer `bufh`, -/// moving its contents to the WASM byte slice `(ptr, len)`. -/// -/// Returns an error if the buffer does not exist -/// or on any memory access violations associated with `(ptr, len)`. -fn _buffer_consume( - // The buffer to consume and move into `(ptr, len)`. - // Ownership of the buffer and its contents are taken. - // That is, `bufh` won't be usable after this call. - bufh: Buffer, - // A WASM out pointer to write the contents of `bufh` to. - ptr: *mut u8, - // The size of the buffer pointed to by `ptr`. - // This size must match that of `bufh` or a trap will occur. - len: usize -); - -/// Creates a buffer of size `data_len` in the host environment. -/// -/// The contents of the byte slice lasting `data_len` bytes -/// at the `data` WASM pointer are read -/// and written into the newly initialized buffer. -/// -/// Traps on any memory access violations. -fn _buffer_alloc(data: *const u8, data_len: usize) -> Buffer; -``` - -## Reducer scheduling - -```rust -/// Schedules a reducer to be called asynchronously at `time`. -/// -/// The reducer is named as the valid UTF-8 slice `(name, name_len)`, -/// and is passed the slice `(args, args_len)` as its argument. -/// -/// A generated schedule id is assigned to the reducer. -/// This id is written to the pointer `out`. -/// -/// Errors on any memory access violations, -/// if `(name, name_len)` does not point to valid UTF-8, -/// or if the `time` delay exceeds `64^6 - 1` milliseconds from now. -fn _schedule_reducer( - // A pointer to a buffer - // with a valid UTF-8 string of `name_len` many bytes. - name: *const u8, - // The number of bytes in the `name` buffer. - name_len: usize, - // A pointer to a byte buffer of `args_len` many bytes. - args: *const u8, - // The number of bytes in the `args` buffer. - args_len: usize, - // When to call the reducer. - time: u64, - // The schedule ID is written to this out pointer on a successful call. - out: *mut u64, -); - -/// Unschedules a reducer -/// using the same `id` generated as when it was scheduled. -/// -/// This assumes that the reducer hasn't already been executed. -fn _cancel_reducer(id: u64); -``` - -## Altering tables - -```rust -/// Creates an index with the name `index_name` and type `index_type`, -/// on a product of the given columns in `col_ids` -/// in the table identified by `table_id`. -/// -/// Here `index_name` points to a UTF-8 slice in WASM memory -/// and `col_ids` points to a byte slice in WASM memory -/// with each element being a column. -/// -/// Currently only single-column-indices are supported -/// and they may only be of the btree index type. -/// In the former case, the function will panic, -/// and in latter, an error is returned. -/// -/// Returns an error on any memory access violations, -/// if `(index_name, index_name_len)` is not valid UTF-8, -/// or when a table with the provided `table_id` doesn't exist. -/// -/// Traps if `index_type /= 0` or if `col_len /= 1`. -fn _create_index( - // A pointer to a buffer holding an UTF-8 encoded index name. - index_name: *const u8, - // The length of the buffer pointed to by `index_name`. - index_name_len: usize, - // The ID of the table to create the index for. - table_id: u32, - // The type of the index. - // Must be `0` currently, that is, a btree-index. - index_type: u8, - // A pointer to a buffer holding a byte slice - // where each element is the position - // of a column to include in the index. - col_ids: *const u8, - // The length of the byte slice in `col_ids`. Must be `1`. - col_len: usize, -) -> u16; -``` - -## Inserting and deleting rows - -```rust -/// Inserts a row into the table identified by `table_id`, -/// where the row is read from the byte slice `row_ptr` in WASM memory, -/// lasting `row_len` bytes. -/// -/// Errors if there were unique constraint violations, -/// if there were any memory access violations in associated with `row`, -/// if the `table_id` doesn't identify a table, -/// or if `(row, row_len)` doesn't decode from BSATN to a `ProductValue` -/// according to the `ProductType` that the table's schema specifies. -fn _insert( - // The table to insert the row into. - // The interpretation of `(row, row_len)` depends on this ID - // as it's table schema determines how to decode the raw bytes. - table_id: u32, - // An in/out pointer to a byte buffer - // holding the BSATN-encoded `ProductValue` row data to insert. - // - // The pointer is written to with the inserted row re-encoded. - // This is due to auto-incrementing columns. - row: *mut u8, - // The length of the buffer pointed to by `row`. - row_len: usize -) -> u16; - -/// Deletes all rows in the table identified by `table_id` -/// where the column identified by `col_id` matches the byte string, -/// in WASM memory, pointed to by `value`. -/// -/// Matching is defined by decoding of `value` to an `AlgebraicValue` -/// according to the column's schema and then `Ord for AlgebraicValue`. -/// -/// The number of rows deleted is written to the WASM pointer `out`. -/// -/// Errors if there were memory access violations -/// associated with `value` or `out`, -/// if no columns were deleted, -/// or if the column wasn't found. -fn _delete_by_col_eq( - // The table to delete rows from. - table_id: u32, - // The position of the column to match `(value, value_len)` against. - col_id: u32, - // A pointer to a byte buffer holding a BSATN-encoded `AlgebraicValue` - // of the `AlgebraicType` that the table's schema specifies - // for the column identified by `col_id`. - value: *const u8, - // The length of the buffer pointed to by `value`. - value_len: usize, - // An out pointer that the number of rows deleted is written to. - out: *mut u32 -) -> u16; -``` - -## Querying tables - -```rust -/// Queries the `table_id` associated with the given (table) `name` -/// where `name` points to a UTF-8 slice -/// in WASM memory of `name_len` bytes. -/// -/// The table id is written into the `out` pointer. -/// -/// Errors on memory access violations associated with `name` -/// or if the table does not exist. -fn _get_table_id( - // A pointer to a buffer holding the name of the table - // as a valid UTF-8 encoded string. - name: *const u8, - // The length of the buffer pointed to by `name`. - name_len: usize, - // An out pointer to write the table ID to. - out: *mut u32 -) -> u16; - -/// Finds all rows in the table identified by `table_id`, -/// where the row has a column, identified by `col_id`, -/// with data matching the byte string, -/// in WASM memory, pointed to at by `val`. -/// -/// Matching is defined by decoding of `value` -/// to an `AlgebraicValue` according to the column's schema -/// and then `Ord for AlgebraicValue`. -/// -/// The rows found are BSATN encoded and then concatenated. -/// The resulting byte string from the concatenation -/// is written to a fresh buffer -/// with the buffer's identifier written to the WASM pointer `out`. -/// -/// Errors if no table with `table_id` exists, -/// if `col_id` does not identify a column of the table, -/// if `(value, value_len)` cannot be decoded to an `AlgebraicValue` -/// typed at the `AlgebraicType` of the column, -/// or if memory access violations occurred associated with `value` or `out`. -fn _iter_by_col_eq( - // Identifies the table to find rows in. - table_id: u32, - // The position of the column in the table - // to match `(value, value_len)` against. - col_id: u32, - // A pointer to a byte buffer holding a BSATN encoded - // value typed at the `AlgebraicType` of the column. - value: *const u8, - // The length of the buffer pointed to by `value`. - value_len: usize, - // An out pointer to which the new buffer's id is written to. - out: *mut Buffer -) -> u16; - -/// Starts iteration on each row, as bytes, -/// of a table identified by `table_id`. -/// -/// The iterator is registered in the host environment -/// under an assigned index which is written to the `out` pointer provided. -/// -/// Errors if the table doesn't exist -/// or if memory access violations occurred in association with `out`. -fn _iter_start( - // The ID of the table to start row iteration on. - table_id: u32, - // An out pointer to which an identifier - // to the newly created buffer is written. - out: *mut BufferIter -) -> u16; - -/// Like [`_iter_start`], starts iteration on each row, -/// as bytes, of a table identified by `table_id`. -/// -/// The rows are filtered through `filter`, which is read from WASM memory -/// and is encoded in the embedded language defined by `spacetimedb_lib::filter::Expr`. -/// -/// The iterator is registered in the host environment -/// under an assigned index which is written to the `out` pointer provided. -/// -/// Errors if `table_id` doesn't identify a table, -/// if `(filter, filter_len)` doesn't decode to a filter expression, -/// or if there were memory access violations -/// in association with `filter` or `out`. -fn _iter_start_filtered( - // The ID of the table to start row iteration on. - table_id: u32, - // A pointer to a buffer holding an encoded filter expression. - filter: *const u8, - // The length of the buffer pointed to by `filter`. - filter_len: usize, - // An out pointer to which an identifier - // to the newly created buffer is written. - out: *mut BufferIter -) -> u16; - -/// Advances the registered iterator with the index given by `iter_key`. -/// -/// On success, the next element (the row as bytes) is written to a buffer. -/// The buffer's index is returned and written to the `out` pointer. -/// If there are no elements left, an invalid buffer index is written to `out`. -/// On failure however, the error is returned. -/// -/// Errors if `iter` does not identify a registered `BufferIter`, -/// or if there were memory access violations in association with `out`. -fn _iter_next( - // An identifier for the iterator buffer to advance. - // Ownership of the buffer nor the identifier is moved into the function. - iter: ManuallyDrop, - // An out pointer to write the newly created buffer's identifier to. - out: *mut Buffer -) -> u16; - -/// Drops the entire registered iterator with the index given by `iter_key`. -/// The iterator is effectively de-registered. -/// -/// Returns an error if the iterator does not exist. -fn _iter_drop( - // An identifier for the iterator buffer to unregister / drop. - iter: ManuallyDrop -) -> u16; -``` - -## Appendix, `bindings.h` - -```c -#include -#include -#include -#include -#include - -typedef uint32_t Buffer; -typedef uint32_t BufferIter; - -void _console_log( - uint8_t level, - const uint8_t *target, - size_t target_len, - const uint8_t *filename, - size_t filename_len, - uint32_t line_number, - const uint8_t *text, - size_t text_len -); - - -Buffer _buffer_alloc( - const uint8_t *data, - size_t data_len -); -void _buffer_consume( - Buffer bufh, - uint8_t *into, - size_t len -); -size_t _buffer_len(Buffer bufh); - - -void _schedule_reducer( - const uint8_t *name, - size_t name_len, - const uint8_t *args, - size_t args_len, - uint64_t time, - uint64_t *out -); -void _cancel_reducer(uint64_t id); - - -uint16_t _create_index( - const uint8_t *index_name, - size_t index_name_len, - uint32_t table_id, - uint8_t index_type, - const uint8_t *col_ids, - size_t col_len -); - - -uint16_t _insert( - uint32_t table_id, - uint8_t *row, - size_t row_len -); -uint16_t _delete_by_col_eq( - uint32_t table_id, - uint32_t col_id, - const uint8_t *value, - size_t value_len, - uint32_t *out -); - - -uint16_t _get_table_id( - const uint8_t *name, - size_t name_len, - uint32_t *out -); -uint16_t _iter_by_col_eq( - uint32_t table_id, - uint32_t col_id, - const uint8_t *value, - size_t value_len, - Buffer *out -); -uint16_t _iter_drop(BufferIter iter); -uint16_t _iter_next(BufferIter iter, Buffer *out); -uint16_t _iter_start(uint32_t table_id, BufferIter *out); -uint16_t _iter_start_filtered( - uint32_t table_id, - const uint8_t *filter, - size_t filter_len, - BufferIter *out -); -``` - -[`bindings_sys::raw`]: https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/bindings-sys/src/lib.rs#L44-L215 -[`bindings`]: https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/bindings/src/lib.rs -[module_ref]: /docs/languages/rust/rust-module-reference -[module_quick_start]: /docs/languages/rust/rust-module-quick-start -[wasm_c_abi]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md -[c_header]: #appendix-bindingsh diff --git a/Writerside2/topics/ws/ws_index.md b/Writerside2/topics/ws/ws_index.md deleted file mode 100644 index 4814bb45..00000000 --- a/Writerside2/topics/ws/ws_index.md +++ /dev/null @@ -1,318 +0,0 @@ -# The SpacetimeDB WebSocket API - -As an extension of the [HTTP API](http-api-reference.), SpacetimeDB offers a WebSocket API. Clients can subscribe to a database via a WebSocket connection to receive streaming updates as the database changes, and send requests to invoke reducers. Messages received from the server over a WebSocket will follow the same total ordering of transactions as are committed to the database. - -The SpacetimeDB SDKs comminicate with their corresponding database using the WebSocket API. - -## Connecting - -To initiate a WebSocket connection, send a `GET` request to the [`/database/subscribe/:name_or_address` endpoint](database#databasesubscribename_or_address-get.) with headers appropriate to upgrade to a WebSocket connection as per [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). - -To re-connect with an existing identity, include its token in a [SpacetimeDB Authorization header](http.). Otherwise, a new identity and token will be generated for the client. - -## Protocols - -Clients connecting via WebSocket can choose between two protocols, [`v1.bin.spacetimedb`](#binary-protocol.) and [`v1.text.spacetimedb`](#text-protocol.). Clients should include one of these protocols in the `Sec-WebSocket-Protocol` header of their request. - -| `Sec-WebSocket-Protocol` header value | Selected protocol | -| ------------------------------------- | -------------------------- | -| `v1.bin.spacetimedb` | [Binary](#binary-protocol.) | -| `v1.text.spacetimedb` | [Text](#text-protocol.) | - -### Binary Protocol - -The SpacetimeDB binary WebSocket protocol, `v1.bin.spacetimedb`, encodes messages using [ProtoBuf 3](https://protobuf.dev), and reducer and row data using [BSATN](bsatn.). - -The binary protocol's messages are defined in [`client_api.proto`](https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/client-api-messages/protobuf/client_api.proto). - -### Text Protocol - -The SpacetimeDB text WebSocket protocol, `v1.text.spacetimedb`, encodes messages, reducer and row data as JSON. Reducer arguments and table rows are JSON-encoded according to the [SATN JSON format](satn.). - -## Messages - -### Client to server - -| Message | Description | -| ------------------------------- | --------------------------------------------------------------------------- | -| [`FunctionCall`](#functioncall.) | Invoke a reducer. | -| [`Subscribe`](#subscribe.) | Register queries to receive streaming updates for a subset of the database. | - -#### `FunctionCall` - -Clients send a `FunctionCall` message to request that the database run a reducer. The message includes the reducer's name and a SATS `ProductValue` of arguments. - -##### Binary: ProtoBuf definition - -```protobuf -message FunctionCall { - string reducer = 1; - bytes argBytes = 2; -} -``` - -| Field | Value | -| ---------- | -------------------------------------------------------- | -| `reducer` | The name of the reducer to invoke. | -| `argBytes` | The reducer arguments encoded as a BSATN `ProductValue`. | - -##### Text: JSON encoding - -```typescript -{ - "call": { - "fn": string, - "args": array, - } -} -``` - -| Field | Value | -| ------ | ---------------------------------------------- | -| `fn` | The name of the reducer to invoke. | -| `args` | The reducer arguments encoded as a JSON array. | - -#### `Subscribe` - -Clients send a `Subscribe` message to register SQL queries in order to receive streaming updates. - -The client will only receive [`TransactionUpdate`s](#transactionupdate.) for rows to which it is subscribed, and for reducer runs which alter at least one subscribed row. As a special exception, the client is always notified when a reducer run it requests via a [`FunctionCall` message](#functioncall.) fails. - -SpacetimeDB responds to each `Subscribe` message with a [`SubscriptionUpdate` message](#subscriptionupdate.) containing all matching rows at the time the subscription is applied. - -Each `Subscribe` message establishes a new set of subscriptions, replacing all previous subscriptions. Clients which want to add a query to an existing subscription must send a `Subscribe` message containing all the previous queries in addition to the new query. In this case, the returned [`SubscriptionUpdate`](#subscriptionupdate.) will contain all previously-subscribed rows in addition to the newly-subscribed rows. - -Each query must be a SQL `SELECT * FROM` statement on a single table with an optional `WHERE` clause. See the [SQL Reference](sql.) for the subset of SQL supported by SpacetimeDB. - -##### Binary: ProtoBuf definition - -```protobuf -message Subscribe { - repeated string query_strings = 1; -} -``` - -| Field | Value | -| --------------- | ----------------------------------------------------------------- | -| `query_strings` | A sequence of strings, each of which contains a single SQL query. | - -##### Text: JSON encoding - -```typescript -{ - "subscribe": { - "query_strings": array - } -} -``` - -| Field | Value | -| --------------- | --------------------------------------------------------------- | -| `query_strings` | An array of strings, each of which contains a single SQL query. | - -### Server to client - -| Message | Description | -| ------------------------------------------- | -------------------------------------------------------------------------- | -| [`IdentityToken`](#identitytoken.) | Sent once upon successful connection with the client's identity and token. | -| [`SubscriptionUpdate`](#subscriptionupdate.) | Initial message in response to a [`Subscribe` message](#subscribe.). | -| [`TransactionUpdate`](#transactionupdate.) | Streaming update after a reducer runs containing altered rows. | - -#### `IdentityToken` - -Upon establishing a WebSocket connection, the server will send an `IdentityToken` message containing the client's identity and token. If the client included a [SpacetimeDB Authorization header](http.) in their connection request, the `IdentityToken` message will contain the same token used to connect, and its corresponding identity. If the client connected anonymously, SpacetimeDB will generate a new identity and token for the client. - -##### Binary: ProtoBuf definition - -```protobuf -message IdentityToken { - bytes identity = 1; - string token = 2; -} -``` - -| Field | Value | -| ---------- | --------------------------------------- | -| `identity` | The client's public Spacetime identity. | -| `token` | The client's private access token. | - -##### Text: JSON encoding - -```typescript -{ - "IdentityToken": { - "identity": array, - "token": string - } -} -``` - -| Field | Value | -| ---------- | --------------------------------------- | -| `identity` | The client's public Spacetime identity. | -| `token` | The client's private access token. | - -#### `SubscriptionUpdate` - -In response to a [`Subscribe` message](#subscribe.), the database sends a `SubscriptionUpdate` containing all of the matching rows which are resident in the database at the time the `Subscribe` was received. - -##### Binary: ProtoBuf definition - -```protobuf -message SubscriptionUpdate { - repeated TableUpdate tableUpdates = 1; -} - -message TableUpdate { - uint32 tableId = 1; - string tableName = 2; - repeated TableRowOperation tableRowOperations = 3; -} - -message TableRowOperation { - enum OperationType { - DELETE = 0; - INSERT = 1; - } - OperationType op = 1; - bytes row = 3; -} -``` - -Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribed rows. Each `TableUpdate` contains a `TableRowOperation` for each subscribed row. `SubscriptionUpdate`, `TableUpdate` and `TableRowOperation` are also used by the [`TransactionUpdate` message](#transactionupdate.) to encode rows altered by a reducer, so `TableRowOperation` includes an `OperationType` which identifies the row alteration as either an insert or a delete. When a client receives a `SubscriptionUpdate` message in response to a [`Subscribe` message](#subscribe.), all of the `TableRowOperation`s will have `op` of `INSERT`. - -| `TableUpdate` field | Value | -| -------------------- | ------------------------------------------------------------------------------------------------------------- | -| `tableId` | An integer identifier for the table. A table's `tableId` is not stable, so clients should not depend on it. | -| `tableName` | The string name of the table. Clients should use this field to identify the table, rather than the `tableId`. | -| `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`. | - -##### Text: JSON encoding - -```typescript -// SubscriptionUpdate: -{ - "SubscriptionUpdate": { - "table_updates": array - } -} - -// TableUpdate: -{ - "table_id": number, - "table_name": string, - "table_row_operations": array -} - -// TableRowOperation: -{ - "op": "insert" | "delete", - "row": array -} -``` - -Each `SubscriptionUpdate` contains a `TableUpdate` for each table with subscribed rows. Each `TableUpdate` contains a `TableRowOperation` for each subscribed row. `SubscriptionUpdate`, `TableUpdate` and `TableRowOperation` are also used by the [`TransactionUpdate` message](#transactionupdate.) to encode rows altered by a reducer, so `TableRowOperation` includes an `"op"` field which identifies the row alteration as either an insert or a delete. When a client receives a `SubscriptionUpdate` message in response to a [`Subscribe` message](#subscribe.), all of the `TableRowOperation`s will have `"op"` of `"insert"`. - -| `TableUpdate` field | Value | -| ---------------------- | -------------------------------------------------------------------------------------------------------------- | -| `table_id` | An integer identifier for the table. A table's `table_id` is not stable, so clients should not depend on it. | -| `table_name` | The string name of the table. Clients should use this field to identify the table, rather than the `table_id`. | -| `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. | - -#### `TransactionUpdate` - -Upon a reducer run, a client will receive a `TransactionUpdate` containing information about the reducer which ran and the subscribed rows which it altered. Clients will only receive a `TransactionUpdate` for a reducer invocation if either of two criteria is met: - -1. The reducer ran successfully and altered at least one row to which the client subscribes. -2. The reducer was invoked by the client, and either failed or was terminated due to insufficient energy. - -Each `TransactionUpdate` contains a [`SubscriptionUpdate`](#subscriptionupdate.) with all rows altered by the reducer, including inserts and deletes; and an `Event` with information about the reducer itself, including a [`FunctionCall`](#functioncall.) containing the reducer's name and arguments. - -##### Binary: ProtoBuf definition - -```protobuf -message TransactionUpdate { - Event event = 1; - SubscriptionUpdate subscriptionUpdate = 2; -} - -message Event { - enum Status { - committed = 0; - failed = 1; - out_of_energy = 2; - } - uint64 timestamp = 1; - bytes callerIdentity = 2; - FunctionCall functionCall = 3; - Status status = 4; - string message = 5; - int64 energy_quanta_used = 6; - uint64 host_execution_duration_micros = 7; -} -``` - -| Field | Value | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| `event` | An `Event` containing information about the reducer run. | -| `subscriptionUpdate` | A [`SubscriptionUpdate`](#subscriptionupdate.) containing all the row insertions and deletions committed by the transaction. | - -| `Event` field | Value | -| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `timestamp` | The time when the reducer started, as microseconds since the Unix epoch. | -| `callerIdentity` | The identity of the client which requested the reducer invocation. For event-driven and scheduled reducers, this is the identity of the database owner. | -| `functionCall` | A [`FunctionCall`](#functioncall.) containing the name of the reducer and the arguments passed to it. | -| `status` | `committed` if the reducer ran successfully and its changes were committed to the database; `failed` if the reducer signaled an error; `out_of_energy` if the reducer was canceled due to insufficient energy. | -| `message` | The error message with which the reducer failed if `status` is `failed`, or the empty string otherwise. | -| `energy_quanta_used` | The amount of energy consumed by running the reducer. | -| `host_execution_duration_micros` | The duration of the reducer's execution, in microseconds. | - -##### Text: JSON encoding - -```typescript -// TransactionUpdate: -{ - "TransactionUpdate": { - "event": Event, - "subscription_update": SubscriptionUpdate - } -} - -// Event: -{ - "timestamp": number, - "status": "committed" | "failed" | "out_of_energy", - "caller_identity": string, - "function_call": { - "reducer": string, - "args": array, - }, - "energy_quanta_used": number, - "message": string -} -``` - -| Field | Value | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| `event` | An `Event` containing information about the reducer run. | -| `subscription_update` | A [`SubscriptionUpdate`](#subscriptionupdate.) containing all the row insertions and deletions committed by the transaction. | - -| `Event` field | Value | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `timestamp` | The time when the reducer started, as microseconds since the Unix epoch. | -| `status` | `committed` if the reducer ran successfully and its changes were committed to the database; `failed` if the reducer signaled an error; `out_of_energy` if the reducer was canceled due to insufficient energy. | -| `caller_identity` | The identity of the client which requested the reducer invocation. For event-driven and scheduled reducers, this is the identity of the database owner. | -| `function_call.reducer` | The name of the reducer. | -| `function_call.args` | The reducer arguments encoded as a JSON array. | -| `energy_quanta_used` | The amount of energy consumed by running the reducer. | -| `message` | The error message with which the reducer failed if `status` is `failed`, or the empty string otherwise. | diff --git a/Writerside2/v.list b/Writerside2/v.list deleted file mode 100644 index 2d12cb39..00000000 --- a/Writerside2/v.list +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Writerside2/writerside.cfg b/Writerside2/writerside.cfg deleted file mode 100644 index d390a5e2..00000000 --- a/Writerside2/writerside.cfg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/docs/unity/homeless.md b/docs/unity/homeless.md index c3d1b47d..7699dc4d 100644 --- a/docs/unity/homeless.md +++ b/docs/unity/homeless.md @@ -1,3 +1,8 @@ +> [!IMPORTANT] +> TODO: Delete this file, when done + +_____ + ### Create the Module 1. It is important that you already have the SpacetimeDB CLI tool [installed](/install). diff --git a/docs/unity/img.png b/docs/unity/img.png new file mode 100644 index 0000000000000000000000000000000000000000..83d34e00047ec4a055d1581b1830b9b890730fb2 GIT binary patch literal 19806 zcmb@ubx>SU*Y25wgg}Bza7g3YXmAPcZb2G?yLaP~1cJMyarfX5q_HGuaCZqVjk_}^ z?>F_;RNcAv&P>f86vgRdea_lzKkN6bO_-XBEaofHS5KZi!IYPi(s=UZDFXPhMtcTq zsW2F`gXXVltKFP;*L6R^B`llm%rTVQ&PqQUnPtNn4o&8oWAy{`{d z>C>ar0Pd#ul%ZQiE6Hs`mQ{Ji-ttcGK0O=FU59Fi;~n|j!o|kMNXr-Jb7)ZP)UU&d zr>=Ia1}ia)t@DKrEA+MA`Vw*xl8_}{a;!zg%7s!E3n3Rlj9Od?eZuw#+J-RQ=xx8Q zEf1mVOvfXfbG)F~H$jLNk+Nm$6YV9odX$i}T?;f2TTl58m&*rpr9?qbhS0ERaxS^L zU!$fA!}!>~dVp40q|!L2RGJ z5Ks}m!E+rG^Yq&>eWgU?#|k@D_YB;@0e(p3h8M=nlM##cRITCe$m$9zTd5-0w(Buh zZRIAZjmZkdT+{KQw$prLSGG|+&KTZRK|j@27?NFq6@hQ&K!Il?uyQ=6!Nb5QLFjPi}4oVmrLFjLg>?A=D?&eLr`4L7+b;$kPUakfWZA4I| zEOuK-zW}~rQ3Op0f~|?EZ1-{SJE6Ttll|Hy_EyB&R2AeNT=NLZkM7+d=Gbe<9!pmIGrS+aj@yl zJNaWNfmsGCELw?-LQn9id_jw*mMna_G%1gog&tv)3i@IY^c?bCqf@xZ@=rE`IED-_ zg=9-#2y>@&rZYEd05^k)<|~+{i|*MppEe(ZF{ie^hB;hQINS-G)`0ZgFSPB~y*R1Q z|8m$HJU7}PG5H!_!rv{mQc+G}YHlgg-MA+Ryv%qcg%=#2#?8L9WN*NY`=;Qs>s9sj z0*{G|G33F|BqI-NV%OD75K0oJ!#`9Tl-pk-5h{__TpKs+4Gqf^%_0-92`V19NEM); zg0&V7t=yngmR>V#mQ?)=EJ=g2o}l(w*) zxpdG$x#BnPow1Cu@QD25>dfBDcX2eP!ZaI9i(kO_c)4{~DxHSk+e9^Xb{K-o7x?_jw3{ts~2v;!3L+ zj|@f)D7xbBq*Z!6}6zU+K!`)q@^IScdVy^DlC zaXXY`ry^jbF+n=_gZnZE?t6IY@J9r@XgH}rk-L&kdgHO5HN6%cm;%c+V3W~3)@~ZA z=kYom&+yiD&_3pipjGlDke|_q@!`(73DfilKcvoYA z^d^ViRepa<7$yxeO^$jxwX zoQ!ehyQRCSjYI;YX4?F(Ibf&HjPsLJsSgVU9;XIywOVf&aPil$9WgN64mtGDW^0#! zUDV&Oj9E8p|6gxuVuOv7J0QDqk<{j4$Mp|4%EW)-x47Tn_sYn-@06FSaQTEU{}t5# zHts-0YfG~#|B~AJZ=d|kc}kxk|DO>Uf@sm+i+;P0zBxPmvUizE_H@y}iA=C8%UfPT zPVp!EP^-3tq~z(V=3Co?`nn`L@0TxLl)!0&>Yh(*IAnVqF5P1B=Fv{isH9WQG#F$5 zSH5P4=woEMZ*fR%V|c@=2*=w4rai$E!f1 zj>>k=iHMjUTPLQ`g4M1l8Lq4)>xAEZr@q-0w!2-9`{s=$m&e1EVQl5qoi$NAX8o+t z*Px(cI4G!&+<#Xi3y1Eu`-}Pz^4-l!G(yrbNvKqDe2;Ug+-BFhRjL>z3WuD0q4`oX zB%0?i5ZQ+L&eM8Zv&CFq&TdTyhCVP%tJk0;*`q~n<;N+B8YP`q-YmVWB{)^XH0fVD za_vw6vqF^64jpe8<`)!}J{K7CDp?SAYU$N(FXv%Te;XJM9#q8RY`=%bog!b&r+MYd z^`QG2=EnT!kDH0O9u2D^bqH}KC+h1AI_>Fiy|@gIY@(qs9)S7lI_QZJ;+kdpoaz($ zds}|sQZMtbUOlzWFFYcS4qGA72|sPY?4 zhS*T8FyD)eP{xS0JUq!h90&xm%D5-7eBoM;VT#weu`*+q%rCbml8m_@?nOO#RAN$^ z{1S223k}8Wo``YZ*|mgsr_kg`7weS$u#FcE*d9ASedgzzZx#01;)dTP+dmP%0{%;8 zd$!g}POgWd`Sxb6DlWKg!gPP8+1iOW}bwR4tL~xd%L&G{1iR>g5q~-y8fppYhS35QXesGFSXu& zdWtgf5??}}q(K~BKkcTRv<5Y*8E-n3+DA;p94EFihOiC465uUS+el3gb7a3f;+;U3wl?w$laso|O5P>6{Y9 zZuy!;;&9w*-kxg>r;?N``5KB0n*n0Wbb&FKoPlIct?5tT>e%}X3EHl0WKUzd?)TW& zW-a$ppPOVBKAlV!crAHOoD?!2SG-4S5<8ZKPqn6G*27Y(KPFy+%~q`E`)9vx6^5D` zE`dYV7ARc)R$xfOr$kc1!I%lA_8~RpFAK@=x|GQ>6yOKGnp>F)Mze_@j8n0US<(=5 zEKjEPZGyE_{i3u9Ao|7wv51MVb)G-&90nJ0Oa{EFfy;3e{b*1oNF~k+M_2@^$63NP zAaJ9^5G}1}m`!4BHin%SsG5 z5DHpSZcW3HIFP;@Ex6ya@7ZuI*l`Umm7T~n<}sqMrN5CzH_^fmxp7mpk z7M~mcYOka+S&?8{2^xtdI#*B%wu*-#tD7M+pJOsL1Ntj$&i2HOt5OUXzsV0$O7j|B zx+wuhwo7R%KS+U``ow=1PX&X*B0(9IW;0TTJ~aBgyyXb0D!QEkK>$J=Z0w2uKCXLG zz6N_Zm<}<*dH9EeHRY2^mG(>N;OIBp+|9rgfp<4ia4wZO_9fDU3rl_Xl}xg+lCF$5 za_o9dxPVg)sO&t_L_%x{kx@Ss?oFIkAP%Q=ggL~E^OP)E5!J_xe= zN>;EU(Ev-CuOcnz894%j>#I;(g&Ha&@Cssee(z5d?01+#F4U0KhIC`G;Cq6o_D3IYu;ZUpCmu^Mk9_Tj1^3yt^vtKZ*n?yjFImw*K(VdQKFRVm%VKAa6si zr;BTGz~tnnumK#WPWp`@xLfF4+pL`7v;chm8c!`bgiczTAO`BI39|DY-y~S8{j-uH znP`R%%9lQpg>Mp#w;FWrjpoplZ!YeLZ#wT`hH-|qiT8r-P4m5+g4^hg$bYaXr+^r# zCmP-3uRnfkb1iGYjMFkrKK{xC>R`=`L*T4+mlS63#n1tj&o3$Csb(Duqm(krxv^wjRac|R`gVi$yS6z5EY;iNwdq8&ztKk{esPNS z7X$Cm3u`=KiLuQI^CgPn4#B$Y?NMNd%Awd_q2g~`O27Pb>9|2b_Ji%9dPSC+jM6u`6iFqBjXOWdKPrrap@`R$Q}-f2)gYtIM_jrX;$y z_FQsbumL=H+TGtWtVPK#%Z%Wm#j42yyBadO`3lY_zr(=ouT5Yh3wkad)IlqiM8fyOGHLsfw zob4`C|B{_6I}U>rUd$qbVVvKM*3!)g+|b4YA$hB^iF~!imvmB$R&*=xj4~|Pmdm5_AI z^mrZFBhwc;1mWaw!+WXEr&`;tOY5-{BDCkU5fO4>ky6r`=aY_+`Qdm1RN^7F1?a7- z_BS1(Eg7%q^9zo2;R&W^Yq{p!N))+c1tzO+Kgf&y&T+f#I`99ThA2~sMKKJ>V7JnQ zhQSM3&TQmJgIozIM`K)9&*?+ND}f2q)Ts}r<&HWxh;Hz-M=^#cUoqclHE?c)8?T3I%l4JdsS@dKjJ+Z>7?;gBBTMCYxaBH&a#K0_V}XE z4LMe0S(9!UNphwPn#EjNBzS3w)+uAvwiK`Je0d(;c zgoNuE#FTJWmO2uuaq-mLT!}1P7W4cg{zbJg!a#!0mhe-@GIk3zxm^o z?z#^dpl33gDkw4uMQmEg^PgYo3ana}ayi;_RX&m9BO*h8m=Ib>v<&_g!1Zab_?gll z7h98_j-b87^WUvGHZ^PsS}2K?GIt9H+p%k!;yFR64a7M^Bjs&s?)>(_Bi{g9>C%YcPlorv1M9?SZlLKxMx}hGN6lK8a#WXQNDJ}s6DU0_ENJLh z9zD}`V!AiZp>iuXyhq~XeU75XYMC3u#0XuDO-tT{*WLkxLV+Uf@4hsuOupf+*sa23 z_(Ea^zqvi_(#%o7mPigk?uf-c3IPJzj#PuM8?8of<1xj+8W@};x>q2PeEEv%2=7Jn zpM`s&6Fd5Z;f}O+T+ie|bXqbp{A4Ze=!6oIxgi*@6`K=8tCRTU+!Lh+l0_E$5jYgL zJJcIfv78=3``b#;P7xdrSXlx(JO;c%vF=?{iBH712DJ?AU2DKmlx}*#urhghshs!9fX2G^gXk)IG>y7p9;_z zjVbTqQ4su{8XP|wd#BCtwp`Q5$FoaNlyxcwS9cJ~wR2lIrbQVO$MGUw8G?d{tV;Wr z7tWu$&0=l5HRgU3`FlNBo>R~Rj7}Bibzv@HzIv2G<6>G><{*9&cy@RM_ z_s8^{O7K%x5#$ArFyZE8g1u9Cf8uIA#_mSy^RlmQfi2L3*Oe7nn=4Nx`c^BZ&q*cY z`SD$zy(~0?%=A3CKV72i#A8W_^OrbSMIEgMgk#*9K&4NG)7uc(earhM4E;(l$TTj< z664)w6rhk3rg>UmCGq_6jxH9n_X0Y%w_Uk9#VSAb!pf%n_BCjSZflMjmFUdU#fB1o z-N2eYK6?l}GG|V}cfDMV0hjx5Ya8u;h)oxhl9JS^dY(IupJ7Z zJp82Gf2z4bfq`yhzr+^-JNQCiCPnh$79S0FCr66=OP=cnF}CM%+s6og@obDoT3{sdiDr=}p_VCl+FJPHKUvHJV(6bOFH zg|CRO=l5}3;cmS>LwKdeOGTKrIP<*uLGkCjE&4Mn-nWo1$8r|4V9R83np&|8xzDo9 zmxa&+F9OKwXGKW3$*RpC0&HfB-odzV@NvM(c~C^}=bg+>9jebB^y%%r*>F@S~YMWgF5oJR^dLHFM1kl)xgK=V%QgqgN!xJeT+(d6Wq zZL1?I-x`VxDo;d9i+hX-+CC@-MD@DS|4Z>BhQJv6S}e&$Or9UT^z0Yd&pBV5;vB?3 zS8S0?HF3*fK=w-=mFKV*;xZSs5OtNOcw1DfY@n$vzV(B-qmNd5L`R{}tiJh~JGI6=IXW*@MI+H}-6A-rB(X+O z9~0p~HBjSc_(f%#xEVt~db2dd`!h@%C^M`(0kZ9HghNbPCI#gVZt~ajG=P$rzw+>f zc{LoK z&Yu2?{NuHv4nA+wwByJ=TOp%9Pf?8?K2j>zutf zn<+<4e>}%VT#U-k{E6bQmkr=I;niG{d{hI!Yd?pc@B21YnCRuYz6D9q09DS{s$Ag~ z>wV+IUAxr+@K;e$W~}<1k!jr?McJRS3Kv$syt7Ao~fl5rT5g?l=?66S2qYQwFp5h z+X~s3f(!7UEi%~nG;s7Xnulnw`viNqli0^N`|Gm(h}I&Fk?Zb)GYt{b->wc?4q4BJ zxEIi2_S3)j{U8Xf>#6!|guqDSLc>;T)7>GWO*Za}M@fd@t@8Qra`E>)c^wq(W;)*J z81FyoDGeWym6|2C;Bft!jTSfecKkDG=SpvBaJVk3(Tp<-z4&Xg!+7+!V;xmYqkSt+ z1Y_|&gO!DZXS1xP`5A;GPCvikAf)~khkAQG_8wHGmAE_AaPn+X;w%&Yn)ECx8?1XJ zXAU11`GQ4al2o)(u1M#FQNJCF+9yiRpvl{l^vbIeT?ISs?}>1FK4XdJHeP*v$X}-T znu{q5DZO;9rsvpFqTh$8Y!kCRYhA=}t|&-y@vte7??vq%r3{-BvDHQkfZrm0-f(+#zy`&#tg!K4pwH1n|Dx+HBFG^Vm6w%hLIPWg%I! zDr^TsZ~pW2mkvev!Fz2^|1q=TSmL}~MXkQxtKXWae|)*s%HGXiuAo5>Zo}_Wt}C>W zV^!uY9yYA;NCFqTegpW=V`aJ}``9{Wn?k;$k~IyzY&dxEWl=~GwhroF@jt#<2-;4k zyJ?GTA-AC4123g=inj5(%1jCx8{cfVscf|vvE8o5Z}>=eV`?T6wRi;-66j>lYu1=f zTJ$i_rgyrU%Se^*cN)Nx2Yp90Kca@WurFMX3(xYEDU9hxN^eAne#5u=U=YqsMD_W4 zV<#M(ge)~l$SXe#olKjci`S$5T}WY*Y%C-7P2UHJeu%?$2_U9oqziow)pnaX376Ei zQh}m*+)ZEgYKA;Rp|tnG{Sm-9hNz?;inlckkDZM%(V)wrvS8L*pmgZl=i-5NPeYVA^l3E91|p@S1CK_!yTLI&l7`!r zL60+8@67HHQe2~m4D(G)mn&0Kkc?K4k1(A&cA#6w+pf<%wWW(!XnIXTrxpG5CY*B$ z-zyyKl1a=2?rk&9XCVe*+ZzHr%s=|X5@%cGI937a8XT+~-kp9zYTXK8g`gZktYKp6 zHBWuDDl^ddH?7l53=*v!E@&cVq2>iDqB1DeqaZ9KOQ6%0U)J&_9Bg7vn~(U7Ym$(A zJ3j&k1iy$p-3iUw&7YY97Sa-Ox}MN9kkL=v zZlXfd{L&{qilndbFKumwE6@`WXNZB9XRq;DE6pQrS{sKXLW3~em)~LG`K4^5|NatX z7as>lcPm%gylQPjX52iGkQo=DB5* zm=uu@z>CiaDNbLZfXNIT*Gh^1dp`C*zzlyE@PFfts{+Lcaf;EVz#BB!h!_W9YSRJp zIrty`BK$8%fsBS~k9d{$0fid#_DcZ&1F}${QY+NGb*|;(mq;`JMTZ(rP|(%qECdwL zepVwaxGRmuZuYLjQ`- ze-{wU>(yW*(_kG|CZs{`;>nV@1tV=?VL$eYUY)yuU9QUb=SNd=H3g?`#-IXA2)=PG zP>a1kJ-TZdrsbbpN?ZU^v|uQp9ixcBN@9M@4i9kAV*W1NxGn)`10h7eL4{kJjxn(7 z^1AUnC>>0#&p&b8Wq5DRAXvZV<@_B00;BX@{Guu42?*hMJEM4OBQ9J#saHEGJ>Q9A zoRgi=?Ck;qzRmFNLIN@>xkDF+kdwIXc^@_sW`oy>1)4a`7^lXXU^b~D-Q2H1K|C)X zVPUy5H>Rk(8OvHSjRSlzua5{$r-{K|#!1o`)jRpxL@J-&>>m>JK`t&M%5Q60(o}X& zU?u%hYi0Zvlri8a@5H4l+HW$yw@2yTEM{3SM{{q+)KCmuM7A3mS)H_+x&knEmG1%I z_e11>(c{4WT9OGNmE+(w<%Y}s+eQMxaC>t+DksO5rlGuX%w-C=AOhYpGAR~1#mwD= z`QWIpd|$1Tg+Gkah5N1N*Gmd?+p9OB$jx%Vs`ikwf-rjZ6HR~%w>QxN4^K(b)+YH4 z5Ed3vk&}RpWHUreFb4ppbqfV_W;-Cysi9{85&OQ;bp1Pbtp9H6SOLfdXH2`yS}y8G zYh2N`1_n?FmL@8%4aD)QnjOe#tpXThAOar-jGXm1K!O4Ixd!|ui1ExqZM%lIM2gqS z3)F>Rz#r3!jyE$`FZ(DLUhSAke=s~zm_AYfYbZX}4*j|?4OZWw`BitI#HSQmUzV1O zQ6RClF&$?HAyb800+eiVf=4_>W*j4=X>a;8iMd8gAK}^ zwj$VFg!veR&741$g9n?(i`!!UDig?Ddpa4p!x{ONd>Gh=Q7|XZVd(IC z*9yMj5S86;Jesb*HT=jlZfOptKJU`EGxWB;_jfLZ%1pVM@1`1Em<7dgbEeFieyNBE z0%BNVZ4WKD2;+`kq6{<+zqpQu(;GQV?WC*PMx8g zF=w|ce@_bo#70ihj)6WXc0Mu-p%w1(0&>7{8nIJ@-E+OGVYtzv19S6TI{Ns%!U&T4 zgin-jmfL=lqon_>Tcay3{zkxu+}A0E=hhXGK$)Xkpo#`+$e1tek0nk(!oT)%O4~MZ z59-1{ANahRA^b*c^MPy0yXI}x=RGc8@3QlZFC)hlM|H#i`!7-K7(?HL*GS-}AC;Yk zkj~(Xn`Z-0iZcw*(}2|;w4i=BN>?Li3_(S3ixUrC*>goQIrXDKD$>?PP|t-*Ej#-1 z-%5VSHULQe*AFO*h?bPBAxZ}?ww|ls6GwHwl1K1aBRZa3@j)ZEUOgFMCv{qan;2#g z0L73v5dQl>^$7gJ&iRk|^FK^(|0}2HZq@L=y`wA06HpM&{{wsJ|KSyi^ZHjhLSiUI z=7e(N78VvtOPOJbfC&Y1+^zAuy?DINdb~zfwqNYePusn}Cm2<5qm&qZW-xCYN$v&W%;+vdmoS$iyWbn)jD z&5H+qp@Wu^BSLrt1QX>34Np-}8x6t4Q$9qN-FNr5mzP+05^~>H3&EQ#=5f>O4&wu!xk%4s;_gJ1_6| z(o&$K!9qsB>%^R{SxSb4oSgh`L4P!b(8JYQOp$6S(39 zx;o(Tc6m5mAdvqi6rPxPwliLYK#-z1XB(yh*CM2ZfDP&~=Dy&s{CIpgd|YbtYmRI` z?k3wB&eD~UbRUb+fSZ_@a8+!(E;PD?()>zvv6=7fl`B!CE#+zhx+uu_9G^GZxp%6j z^Q)_=edJ16H(;YSxWGk|h@KQ29gNt`*UVQQS%8vsc|i z6<}EEzS!bl7G=WA! zLt$|ONx&YT7?yKx)q?bokg8Lumi+oQExwip1%@I?Un|H8HX-wrD& zO@!~&ub1<}Wk4AB-pk@6FlGe=R-^3yGpxi&$2U4C=H@DNl81(D$rhwrV}PN>^0>9%>^8B7S^U(ht91*j+K35>!`9YV?V(q;pXS=H>xS6kAIyUHa<*Fbd|)5y z>itP{a4FVnSziZ}sn<;P<^!!CCM)-U4sEJ-EkF78Gg9QF%NPwcqepf- z+FbNZitOxmh-&4k1T~}nbA~b%q3Bid6;{}1yKa#TjV+|x;mhpa7ZJ^VXL^65vLyZH zn<}5d%lAZG&k}Y!fY~(-6?Q%INoDGFK}}_VkIZeae+@Q7bnH(8-3uqh17Q`0+Zd?V zf`99)4B2MUm8^urGe1@l4a88Fr}^5G)F$6LhxD<2}-H zl%uhh)T)UtPLN;@reRGY)qiP z_bX4afkLBCIAgZO#_%PZy!9P|)1O;-Bj~>VUvm4OTqgA$2nMhuY z@DZEHtu)x7wj-RyB9Jr13~$U9xF1q4;as0efk2D)*6&%@>05zPm*^Xl5Wggk2#1c% z%zs_8<$(rcbXx_%s(=4IE#_N*p_^zr=n2Ehq((~zDL zNl_j}xU$ZwdZ3^jLo|uO*1>h{@M25ij!$A~4kZ=GUz_u}pxAGDggdU|5tVexgK96< z7?ha72F0&<_u zZ>{&bTw$^iaQ&bnz7w=Qu~kn#)Asnmc5~ot?^%AhwE~D7U&wTS(=XY!3$f2@mB`i3 zHnBd^8B@xt`POh^0_KSJ&)r)qSaOWce`Ai zB|CdHOPhX=3|5vem!Gz=wTI3EjcK9X(Aj2>EC_p6tZ835r=7kIr6Ao##79Z!&BcC$ z?>;Zu_wNR9#jHh)gy7(v3o0+wsfn2%uJLc37tmoJhHhhh~X%>$W0Z%(Bz5jn}EI%3Sy>GEOrq0P+KK#vY4FUOM0 z33*oh+{R4Kx)E>odMr)E+taZ(GFmm=ptTIR;{=Twa~xg_%;_9_V)8FaI2oMi^4E0Q zI?al_#eLTG(p{aKgQc;UH%R^O{f>(W6LbN>d z>vGN9wtq^aYL@fCIS+ex0^3TNydv$58qMGY2EJxuu~V1qc-`MgLYF-EOw_aCR<*UY`iOGf!zG~>!#5(H#LTl5ju84H^S-yJvcyV|Rw(*LjuE)r zsFWu^Tqc3ACIRylT0Th^Kg;H0r2CCnNK|jPrp}gWVoyYV*r?Un{`!v4HJ_VVrWxGv zo5pCy1&w08J+{&vf<>{^BIdAj?{yBU9(3NoJnmm>;Z^}26*^>&JQEpNTp7{-wZ48& z0IzVo|E&i1dN3qyY-0r1Y?BSba{0wMLwv_4k6^7CtBnwE^N$s0?SzH5`{x$4w>$Hy zzO188kS|@|d8s=fwJuK6n+v<>*oS+_AHk`Y?Bd-~+RKbiHC1X1}64vC2Q*^HFd;8L3+(P!?=mo`ar%yr7iZgtM0 zJ2LlL%|J`(F6fU}SCR(&vpA?%u!o@_dmEe#p|d**hBlep_UEnP59$;(fcrN^5>f@Y z-LI^h1i&tREf&aiX^nlv-csJ_B$b5kWTCBY+iOa`w%XOclJU6=!ST+Jc|Wp=|Gvpj z(6W#2=jnZ2$N2P2ztqjThbuljk4s^7C;9UyD{tFXWdlzdzTN+^!Ev$3Vnf_Puc-?K zCD2^0E4`FX1OCz1m6?~@^_6TSS7yxs(u8%l(}x*>%H9j^Vbd+orh)R7hG$f2N2Kk48)$hOXf(np3*{>aYb? zw4cI%(t3$3Tvc}Kj8fgz_+FiQ+x79`e%Bf$o6@+d@fX>G?9z6nj+x)y((?S}hXNC) zo@k1HZOs3fL^3H8JH)UTmr5DAtJBg?hmI~k7fUsY>cVF{ZD?PVDBxAcP^t}*ID^*+YSwb0iVvQj60K z8|h$% zIH33~sLpZ%L0Y6PQbJ|N2v$x#-xY`w=TWK`-=a2aF>$S@E2k({NU!^17 zn4IRth^7MNrmEO|VtRVyQ^jwr;#K+i`7JHvI<5Zqg56>_+j*|~d#ymZ;IZrnPf76t zDgu)9w7USy&dCJt#~sxG3GYT1yTGiTvIY>_zS!U4fV(xS$#I}1oyryxU4QRlrf!Cq zpcwMCHRllAr@4J0D)ux(B~Jn>%SGkgf_Vc-a|}Z`&flqm>+w}y5ux6WeO(sBuyZa7 zir^#f<1bmtFWMbgg(5bGf&YYKmH8(FIM7jRihmF2Z_w4>JZ!PMb+anB`D`$CBX5pw zb=A7$K4lbt=KX}T3q5=LovX4kreIa(geg$c7ZS^DsuERE6#@3|MhlY zEy^44{+_c)WL@G5evrp&7*sFeMD?)Zp~tg`VYWmU{>vc#MvQyY1>&*6nD3Jz^HtPj zmnpT02(RNw1NfGWvLNy^n9A`nc`r?r$-C@OvnJ*()=qpeTvi+|r@sN-{;?cCT0?~p z(=N=KVzI!fF#K+i`s4A0U*P>S6iOj?`=h_Vu*F!p48s)QIXO9fAEn_nC}U$|y!JDn zhU_X^c}+XfNPw5$o~zjLDZt<9T&yDVxP7CLeT0f@aQX4!QjRxj^JT4-aEkGSkxl5& zqL~QJ#}oDzbd}ykDGJDD*W<&{??<893sF_T&hfnZwNwxYtF+Q%AN-1si{@iPD&yVo z=?2O)KKz6o610es{FPm7*EM5)?OkM zd*TIdVM5SCL2<-!+*H~$IFOeCCePLDzr93I(%*9fO^mBY5fTbW4Z+fAanz%%6}~!> z#P%R~VV{7oKivB+luAdFtQi3wt@ag=WjdmoULJ6!%UJAvC{Tpw)7_760BGdZYY{6h z^In`oQ{T;*LWn$C)1?}6>{dthlDudoBZUGLb6Xn^#l))u(v~?{%W)%baJb|avW;a$ zJqA9rF;9gskYv?e$aM2Lmv7BktRZ~!``0e)l2oJ6WH>1iR;4nK)x>}05kJp%NgSvf z`?XNlS5PQa!>HL10o?(JQ5<1mVWEVx!(1p~47q@Yni_}i)zPQTZH>z5M*uAVoJd+) zS~iCAe9EG=!5a;9DQpW#MWLE8b~z1{0)|o0UNUP1o+!7>?#aqU%y|hhX$xD^bowvG zfsHD66IID*#_!EmLgvpAdqT@@``91hxr%2NoJ%0fPsJr!S)YWfRnMz{$lTb^ds5P0 z08B;|RO~Qx!6@{=xR=6Y%Pyf?Y8+n@#AV%YVOWWcq1 z&!c|)=-=|l+vgEoUs^OS;!-k&Jg_pPZ6432?;odsQnpA-_TGo;!?PXI+9PY04zr$1 z*>8Q^y219`i{%v#h*5?we@FjFF@<5iv0Uj~y&F1B@|R1qOqen5;A(w)p^Pi&5#@uAEq$x>2KZPKzNU zy1-5#cKBgr-3L}+-!|8czU$L1*~lYb)pY>JrY0sne|!K2S05jj#2}UL(J#Jb-$1jC z24P-6cD+zXnGQSL)801VZc&xC|0F~c!Oy#RGD&{AMW4!|$?KbjpdhVYp8v$mJS4$dN-gV!1NO{FzM^^biA4Uh6LH$)r;ZSXS^06KK?&^5kI?mv852eq5J+Epq$5T^>@j~SU1cp%T&BW11ZMCOO z#5^x;Bmy){^r5qCn3sp(=+ z60txwIOpG4x^HVVi4=%&t{sOSJSBx*t2N6~OfOoUe|$nY9eBQIN*%NS=Gi$JNa2o) zi(B#mq-mDdW(tr~t%lpYAlMtVWj%f{5-O^8t6@rubwHGD5P3=do4mP-w_Q7eF^gwC z_GZ-e^sd){Q8G%mYf4bHgu9Kg0}{N4N6XMrw$PP$c$J--mbgQ5QDuLzq^=B zj6BJQZ4QTb7!P{p*Cs2AkJ6zsu6;h2V1lAsq=~U%`GVAKS`~tDzJh&14TawCYE;U@ z-+JEW4;`<}5H{vyzAglEPbfX)8yxO5Uq9$`2uNvB>)1xI*AM_N{`$xBrvvWcd$+q> ztgHjKsH1=fvbMVVY-hht`v;4|bW%W!u z{<=cY@swtd4dAL#uh5q`1)xr^Yy;@z(3w`oLuq6PSqWh&Mqaqe{YTv)3xRcCtZ6L2~ja*-WVS^QKhS8AQvU% z;dNTGzEy)AavT6P3?=z^SB2xf%Ws~&vM7w!kIYU(*pJr+?$4hTX$}DBG>9VE@)lue z6ekr&Sx1L%HAAN!^XkUMFXj^}5QU3xYV5-A_!Dy#Cw7SU?n#)+Oo1Yy2GAdp){}If z)?)D}g*`b$L?GxAaK-_3Jt`4z0+d8gXXm4-as%B8!!S`npsbxL)2r#!5rqV`hJ!(} zC5j;(pC5pAd4PXVZ#|kTOX>9-p~bZj3`LJs0g^1K_uEOK9UsGKzrS29Bv_o`I(1b<|c)rf&6yAOZ z$U89P|J;=2xr%qKpO~1a1UUQO{;}Y( zBw$$_uv4ilF0i}_`+s_^Sc3xSL2`I0e?f`IGRwd9P63~&FDXMGPHI-VhjZHY#~N_i zTk(Ea|6x2g@!$roo$pmF{t98#bS;bbKI95{#9H_Co@x2F5NSbf?VEvEA*IO>Cotu_Z z0?x077-Pz?f5qjyMF$&z74@fzinp30gMoI5iD5Ug;452|Ni2$ktZ$<~AZ);YnRsOh z^I2PevK-guaM>nz9$!xO?;O~2Z0x=i2T1-yRBq;6t!e=0EFYJ;@5^j2CVkX^=YLIk zXE!yJAq056H)D$IgIQw6tubOpgtYsM+yL5^^`@(B z;uBydmdi(7aCS~IHK8x>5qpq>ZJH}9&mFUEGoes!Pictd6?Y>t{AIVc>GJt}f!2~b zZ3#9rS`4v!Qns_W;}s!IAepT%dJ;p$OiX_SqQ6C4mUBIs{gJdQxD}CG+SHG(3bs_W5N^*wM@5MqLp`#s z_K=#Q9KvQxY|;Q%Q;dt52>ZLvPhsc5Y?+&`pD(tJ;a+xww1dl$H!v_Dxz(_1Q7K_N zE(_6|PCOi=prC+{G~$truF-GL1sA@-*w|v_Y@*v2#O-CqX{oejU_q3(3rn1;T|k(L zuP)LH0YaG>YcGb*lY@rtH^I$nJSjpYBVqKlHoQD~OG8WhOtPCkK{+#3SCV(91rCB?u)49BVhy$^pLbq0CdWTXKc@`T5;{JJq*Z_MB!KERe(b&MlmUZuKMR zqProUxqg0rYKa%OOM$B{Tc+-vAjLdrC@=p>s%qqVl{r`+kTXoZdq6dAM0H2*(a_O4 z_HFLiTinqP$k}-5P*cL2W}(`M>yd=)Xi@(bjGcS>fLBcQcI3M=CXL_T)uP%hfpSA& zRd|Qp3a?1qkN9OT9jF63c5oKogez56OP}IG1z*go*o(|c8W$E@Zr|e*$Zk|;8$h9# z_W1kzBbZ=|bOX2hnAAIb8- zh(CJ}s??4iqq1sI@^a%`Zol!@jrC@2kxs@nhO7*#CzZtkF}QnO+Xdw8*-GsL%ylEJ zegZ1sco_BW_c!&$#Da@IJ$Tc3%T!+r zhZLdwT+o@>1E4-&CCB^|4mG~er3COwPnkX7x`=R5r%_GO8vi-rU18o?N?&r|uF%MD z0N9_o-kPfVK3Hp{AyfupfTjWQP7F5KduS=+c+juIsit!Z#JHw@Ut09u@!ov8T~4ru z3*)Q_=>}+m@qH1mbut*<(fqOqb`%e&s0rNs>$np=uR$$A6DYn|ri6~!Mg?q!@PV?V zcf{QH-q)Cc*+(Gl?a5p@AM6t!XoxjDYzhAcP6e|520p9!;p;Ut>{L@1 zmlR2~Bg$MN+F+9#qj*$GvxwS!;YYx_OB&9(WFIEhZtTS{BF3?AhrW3AenVYA`?MEb zzET4i2I~v199C2OwI>91{$D3T2iSVSy=v*&`g)FnNv#wMRU|#Mjv&0lo_exmt*orv z%OJY~L@|A=5yK~&@*cxxRMCBN7R|B{aD~ujk%tC>hl~FuHd^GaGGOdhz0Jjhl?uQF zWdOnNIfVi+xpXFaWs)b;C`=~(1PLMsTYqYnHM8KopnS-20HFv+N%p7G^Yii=xD-7> zXq1j;MdxqiX&)60NcM(4feG|b}!lu3(Jf+IW@dCbIvXWe5OYXL1&wZO#892Z=ij$9+s~bY ztj{hNUeTajI5mi|h+o-}j?El_JZs^E+1M(wB3?%M>-F@E?v=Y--A|i|E`tFSv!qc@ zQ+3-EzB>OF)y;WMcq*AA4E1RZ1!ij(I6V8qTJA?ji|5`CoD>`j4*%@WANEL$v0nd= z$&Jb{?wZPMdU{bfmj*t-w*ZtR84n~vRODShD0nuEqpf8vw?QDaLsNUtW?s~Z9a!Iq zLKPVLpfVGr!L8V)t_0||_uTRe#1bNr*mzpj@{&@MWmHt*yD#l9)DQzhm((C>5YizXgKWARBn1IMYLJj2MLGlq5oYKXk#3PtKuQo1 zMM`NT&-Q=Uz3Y6q-_H3m>s@>5-Ou|xzuM0X^);xVEKmXh0xB&{H6sE7LTYf0hY*3U zh(~Lz1O%*qwbYc2gKf48=I$Gt7Y<%rD_eHuXdA3v_wK9judmYPGgrIgoCc7jshd0L zI4x_@(!NkbnNu(wp$A?yHj=!5Wu&A&ceU8*@dCDqVt8IIhgk1l% zFhBo1J$<}TeqnNwdmzX9ZD3P#^U=Wp0~~IjhU2?_JzvJZtf9gGc;}0;h@fD3b2H;K z3oC1j&vG@zom;m^u3h6bvaksGIaW|nA*5EWVr^~hS3^Zrwg2+w&6{UuXW(Yt z6*p>;l*Oe`et{mUgn0Ez3$x>En()@$Z~d5#j*gz5p6>2$Rw-{gILk%!HJXXa{k^?f zFF!vCATK|^(d>rX^74~?J!4}AkL2WJo3;S*g_1AhqoWgrv~0}GQI551du#9GOUue^ z-aI6|Dk4a;Z1L`9+qznNpEX1-j70MA@GKR+VyT3xwwh-A?z{>gOlFTK zAtUPuIYi#LL3wd{el7=vlzgUvK$Mz3*VqmY4t{Eet@b^mhuL|1Q$G+2-v99-F_Dtw z_i|^7H44^Cp@&i zkxsf!4w5QxcV&p_(OZ??rd)rrp;c!+?}P-_KjqeqXZsHl2-d#kFd zzA3+V=F+dPtu=fyI;WJ(A)lL-rQsaJNip^LGuZ1+uCAM=m6Tt|TUuIJ!{dW`C4o7Y zJ6VT`QWu9;U9j^|w_wHy2U>jduNE_w66c9)sgs=}DgD@pX?Z?_X>h_Oe1`p)5g)&N z5m9)~#=;^iF3z=6auH=mORcu-yaJOQPECc~lEhCV6@qUUBPU(dB`1)k_yMa%R z=3KvFXT0Sz%@@c|ski=qA5L!r7Fv8#o;{lk>sYX#xYXS;KAv zR@-*D#_5g_LR=bA@(S9ekue)9-M)%h!0{R@D}J7xm6nt=GBI(PDO_h^W9zd}OU<8% zJ~O5oChi|Exa)d#J+GzViVB*ok6Yp5;6C#1-HG*eH%rUBotsxA1x}A2%QUOp zj@S6P>S@VVQdUORJOBOrb;+)T1SnhBlPBjVyQ>sO__Umyxlf;X8NY z7gK~rWhTk5>iRC`=d&>>$GJL0^nieR|NedIn%8j+w}3zwrWv_Q0qqv;YB>ss|DSik z5C{0Qva@GNO>tx;RKD1GMpmRxM@Q#_#V3ucpzlMp7BL>#QQ1vmQavJ#2B;D&b`lbx zeDHWSwf!SIB#1pH+I)eV`nt(*e0pDTe;n4@6Qf!*oZE!*3kk^`am(_R78On2fYct% z);T&%Rt*xlExOIPY*UaYU@$`QWJzAXBz~Wq1cw+++fVf83<>DkHBKFxgGn#Pa&r0u}^s4y!H{C6u+YDp|1(|xJA;9pizmBN*rHU<*VhuZMDL=h>fVIM=&<3?cdthT(UL(Q z8~aryn4iT;jKD|`5q&gLl2;1*APYM4PRR_5rP~L|K!dpdeLM)fv_z!XGjZ zNlV{eqh?cT;o*;KE5h8YGbIh$4%6=v%;>RQwY@Oa^()X;ffqctkvKkfP`&WTk~WT^ zbI1+k=U79 zQc6~yZ%cn{`dat|_SU6}6~B2u$54;e$r2@X6LVc*TZfBm5%x-sP)Cc7sVUeuNa=1? zyfJ-pNMl?cevp#J{N_~K2Ld4%VSB4_d$aK3EeaPoJ3HIap|Ge)DYm`6%~NcI>OcSS zsL-8?QgV2B*db#_NkT%RT)f2ZXv;iKtRW%2Z(GA7nNca!x49~0|Mr6#XWmz|V(o3e z&04J<-oNv}i#^XU`RP5yJ9lC+u@N-vvTATPvVwwVJ5T-n{n=d_3dcVhE8CkUzY4ru zcB?zscpq6y`rF5@@tM45*csXTH>e1AOKg{V*3GC_XfJq#6H!Y^mPK%$LiFQFE1I0S7X4EIP&UO39vC*pY&Aw|JcEh;r;cGgMtH)PKn)}P=J~mew zUKUom-*!!7 z2|c~9C4MQW;!IKp(z7_8m={Rn?V&F~I$^BR?M+B`jAAM^sm#SA?NFI<_9nDDfO>Lk z^3B9JEDjo0OPU^h0i`mDdHfp%@YVQy+&Eu-E(-`wo;ns2^T8SoMiB%1D4>7Yv)Ri3 zl>3P}oJH!7Df7`Yt2iY?uS-8tj+{Uy6d zB=rhSv-odjaPNwu!|kq>%^N{drM5Ci3OtgMzGw_8|PfV$Pnij9Gxv}Ps##R~?{S1H<{oV=%@v58Ie z8^ZPV_y3aP&dtpYXr7vy@?2=9iuv+3NPv%T7@w(uLeb_XDYRJ~ZOvh6n4=_cQ!_L7 z)YJ@RL_|cy#C%pxouB9BVe*PoBnMUuGgjFLgSJyVWC9y&+iN$CnRv_&n};=V*ZKK( z7FrB!nkbvSzgXDX+M1eboLR{r$Pc-B#g*UVQ~vYkbh;1dfr z6G7GwkRV{hpWw7l$y@K;3XMXzo$~9!6J<)1DxsIXw|u&9qd6L{aR7@+?~v>QccH>i zY=dJ-J(dCqty|ZHcgA3$-fTPkdNQ!5i11uwYu*8r@btbD8zeS1_FEw49bjxs@}P(d z{o%uh0LWGpHOkB^o)N90sR^lw7lrSoYEI~${Cb0hMqS-AF=4C*)mnFx()X??;**F3 zsDm+Wm8}VB1+M(rm0}x}wetY`EdJxtq>Yt%zVmQ`49wETw7M|T{?DNGLUq(k;?frb zk9C4oVw1n_Volv7P>0H*T!N%H8!hP5VDCgWFc20q(5|r4LR96f;e?`` zp2~!dJY{EXO>P%Ff_NSSA6Oq>(MV;2Wu0lik>j)Rgz+1r2L_TqU;N15lB+0(%C_dm zoXl0IJWH>H?p5#Y?a@&&xZNNEJ6q7yV{5i98Z9IwR31A?K6v=)d!u5>O94DOjmacl z{d{o!QB?pHG&U{{v@d!2c(n2M;ILn8+vd4O z1=mPs7pfNJH?S&4TSn)p)4N@_Zv8SjIVs4c0rQ75;q2<8gm&`YT@oavE90*d z&Rr=Yw>MTMxhhHGR;dvv-sOUz)=;E+lb72?aEC-BAt7Na55m&7ZBII~o6w70nZ-zV zp5&eh>6PlIJQB(*g3s)S?k0{O%cQce?EEd9C|xI8xCqf^d`&r=WU$>HZx0XT4tvI= zOb@KGf4dA~TpX)!Ne&p^BlM@yenn!YHZeX~eaB}h(XlwGSO@+)s$`|uNB#EWU}dPP zSYtvwHv&5Xo%{e~2Mv^PdUDrld)%~v;=fJ*8RVPZg#g(vp_i}@*~)~L-jqNg(2D(E z*bKg_DKb-SeU>%qP>ZyolKVSd6MmStd_E9oPHEDQ?+)%M6zl;ZZ%Ft+1nq}T!>OsO zPsaV6*3#3<;PkU!lBh?Twq4OjJWJziQ%unD8u;1Lc~RBg>N!(o$pk`KN(geG{NbOk z|BSV+zW!4CV}BG_+myt&mu1`Pw`nyUtfQ0uEF&XHSl2wXGf66 zGC&l7H>n<(m`MHLf_(@I_-VW;27_TT*_8(Em9+E;=n%iHc17OHI6ptHuC6vvykJoM zFg3*x28r05sj_{t+h1}2^+Z7tOXX8e0^(DOAC;6_5@5K+jH$MDcl7thZ8XWNLRrq*BK;oeDd<_q1e}Nad>xW=SkMzI-ikOhno(cZ3P(=eLGmoX*Z78K5yODJhZnN!`3TQ2!9#>6ob6*bai&uYBY=op|xc-~FFy%JH(Ab>iAe@Pk5z?iEe= zt?1odKUDh3&fDJ#Q(b>}(9(uAPcO_4L?x8qjfa<);dSxcr2drYWg`a%jq%HEkHC}$ zRL7Z_jDM%a3-PTqakyD*iS~mB!aFP(epx?%{tRm*8MVovV!fl3SX(Qe5dHl5^OY5Q zPOoYnJo+d-HC4%(ZK#~7A}fv%7b$F7&88U6Cd7Y`ou6O&IRbUhAi5F0_f@tevFIj{ z+R2^P|6-t2UNcck-`!nkU=eIc=k~$@!TKI1j57c$Hg|Jy_y#&oFR!W;dVs)NG!Xri ziod9-OL*#_?mp=%4Rustj2zqc#_KS>IRr4VYguKID3ZO5@$pe!S$yN4on_nVTU=H3 zVR?BOETOD}YPJ%NzzRZHuRJexpGT^ne`p!TkipzrXR02)pLV$gDF0N)eZ3s{%?*-U zU_by@c3z$iEhJJMx$0Z8g?&dENf!O>uaDxoQExVFEjO{s*{|xDJaKAJf*!2ki``)(NvPFMc5TOA=)ra|}2Z#S4 zNdiYgLb9$ZJYcKJnGv3zVJ>ron-Exeq&)Ch5a>AhhAFrkRAOjkWMpk!W&7?17$umP zn!ct0#nEEhBONBEOi%)xa#<~mD_)(0Fk*rSJg$$@AS$(;o!2I@8i)8SQv4ouD!|Oa zAu1-OVgIR2uc4s(@j2P0Gf8D6@ot2ff^L+1po|I35E6T&e1B^^xmzDka z@ne2r0qm%W1C%Hd8S?fnm3c;TGWDX|*0pIfI592GbE*9=*tR9+#sC0jG0)ToKAQo$ z6dD;|UQ%4l4XQ3lG_1F@;^Eo|{PfVN@sd{~nJYUp(?u-Oko1fVfhbBzpYyzPmOENc zi|gy_v+4}DzJ&oL;_$Fm-Ox!JkOAhJ2L}Nm3JM@@#;ENh(p91@+l}>f^lYvIVcIoMn~hpJyqcnQ&KiL(xgB^&^fbh0e5;p zPEO81NkQQVP^y*Fad0HxGP{KVvqKD%Si*n45D*lEVb=8Z6%Sk4kTm}F`?r{oP%)P` zH{OA3uMq@A1wq01k%EGPY}W)Jv1SF=l|;cLI>EcA+2+06VQpww3x=9YOEwaw<~U?| z^u_ldKYHjrd%*owYWw@&6!r1+^b8Nb{PfzO#cP4TiGF|!7|WMY#Swt{QHL~-kWd8{ zOWdntZqBS;q^+YvkwMFZ8=_gH7uRkliuC~1Hi)i$sPznj0a$>KvIziidvPET@d*jf zR}*VcIB`6~SpxzMurgu?>Cn70{Guk};z6@IDd-WRL=cuUlAt5_Z)|$*`HrVD z!F2Wg_>6Eb$eZU5bs+1SHe@cNjRz3G;;!cf$c&E^XV#Lj!C*!KR>7zj3`UEJKzp%) z#nU?)`wcc6n&1zQJFnicz42W^pGYtcz?^`cpKcmEuJxjNuA zSFdjC*koUeBvY`Y0lXJmCue5(!CkDhYGr`O@#f8&p&?BRwe(<`!irl)aL1QbaY!)h z1GDAvp`jx1uw`XWKfcldyL$Gqsw#2SyTEM=FxI-54+EbBeErHblbn+BDJ3NO^fW}R z_6UTO^9PJiRe`>mNmmya2U=6$oaLfSFAo+)eDL4^WVJr)_>>f;yWGBFN81Ym8WWq7 z#}<@SRBPYA%Y__3q^L2{Bs-VyLXXQ{zvkuP`K~8N#S2I7ZNup|uTrhDh6NO<-hnA*ER8(sUBIfgNQWzZc8(qCFW z+AlXYHtaApWh0xPYmT?(R`GlmM=6YsRiY2UBn}KUZ1pn0tRF<~qQqeVCkiU6!|D8P z_9?G5R4Xb2u3%|^%WiaS*DsKDuGi_B$YGcE{kk!Y2U7`ty^Qj5L54qKr{cJ1jyf#1 zxAdY*s-DPC67i%jyq(z^$BfTV0A1i?0tpicAeezpke=!5dzyd* zli4GIUg`Uwa>+)6C(b0oT68;NZ&<)k{mDCuFW`@7zkDg>{@VxY+bwZ<;(?1_q%5?% z3Zt{Px8F12h+zfHR~ioMk%((wV-44a(*+Ps?(lmNaKQ79CCE6WlaD5c#W6TEcL6_~ zz+If}&orI*XWYTTfgOSt18V){@H3IiL+f&FWo1H-1dl?w(i4{IgsAbE_h79|A$E`o zNf{Y(0ei?s7Yq)FzwR66%IY$=w;1<_JD>n@)>2#SASoL=dw=i_z6i=+KFfNmx2pR( z2R&-{6=OY;K%Og?X=$%IEJVXVN@bwfcnc??a&7927>T&6y zZ?ugT?}V`v(Cj&fCX2pB*>Z+WQK0jq=|ce705qaj{oi=6r0C%OGqj%X{v(ya8UAll h`TtG%|Kj3`=Dfu?8LORJ0zL~Q&{Ee|t5&f?{};c3wIBch literal 0 HcmV?d00001 diff --git a/docs/unity/index.md b/docs/unity/index.md index ecdf6801..6c005e36 100644 --- a/docs/unity/index.md +++ b/docs/unity/index.md @@ -1,5 +1,8 @@ # Unity Tutorial Overview +> [!IMPORTANT] +> TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. + 💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! The objective of this progressive tutorial is to help you become acquainted with the basic features of SpacetimeDB. By the end, you should have a basic understanding of what SpacetimeDB offers for developers making multiplayer games. It assumes that you have a basic understanding of the Unity Editor, Git, using a commandline terminal and coding. @@ -11,9 +14,9 @@ Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll b ## Unity Tutorial - Basic Multiplayer Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](/docs/modules/rust) or [C#](/docs/modules/c-sharp): -- [Part 1 - Setup](/docs/unity/part-1.md) -- [Part 2 - Server (C#)](/docs/unity/part-2.md) ☼ -- [Part 3 - Client (Unity)](/docs/unity/part-3.md) +1. [Setup](/docs/unity/part-1.md) +2. [Server (C#)](/docs/unity/part-2.md) ☼ +3. [Client (Unity)](/docs/unity/part-3.md) ☼ While the tutorial uses C#, the repo cloned in [Part 1](/docs/unity/part-1.md) does include a legacy Rust example to optionally use, instead. diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index c2f913be..8eb3d397 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -1,17 +1,17 @@ # Unity Multiplayer Tutorial - Part 1 +> [!IMPORTANT] +> TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. + ## Project Setup This progressive tutorial will guide you to: 1. Quickly setup up a multiplayer game project demo, using Unity and SpacetimeDB. -2. Publish your demo SpacetimeDB C# server module to `testnet`. +1. Publish your demo SpacetimeDB C# server module to `testnet`. 💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! -> [!IMPORTANT] -> TODO: This draft may link to WIP repos or docs - be sure to replace with final links after prerequisite PRs are approved (that are not yet approved upon writing this) - ## 1. Clone the Project Let's name it `SpacetimeDBUnityTutorial` for reference: diff --git a/docs/unity/part-2.md b/docs/unity/part-2.md index 4f7fc496..db061cd6 100644 --- a/docs/unity/part-2.md +++ b/docs/unity/part-2.md @@ -1,5 +1,8 @@ # Unity Multiplayer Tutorial - Part 2 +> [!IMPORTANT] +> TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. + ## Analyzing the C# Server Module This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md). @@ -7,9 +10,9 @@ This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md). In this part of the tutorial, we will: 1. Learn core concepts of the C# server module. -2. Review limitations and common practices. -3. Breakdown high-level concepts like Types, Tables, and Reducers. -4. Breakdown the initialization reducer (entry point) and chat demo features. +1. Review limitations and common practices. +1. Breakdown high-level concepts like Types, Tables, and Reducers. +1. Breakdown the initialization reducer (entry point) and chat demo features. The server module will handle the game logic and data management for the game. @@ -66,7 +69,7 @@ Common `using` statements include: using SpacetimeDB; // Contains class|func|struct attributes like [Table], [Type], [Reducer] using static SpacetimeDB.Runtime; // Contains Identity DbEventArgs, Log() using SpacetimeDB.Module; // Contains prop attributes like [Column] -using Module.Utils; // Helper to workaround the `throw` and `DateTime` limitations noted above +using static Module.Utils; // Helper to workaround the `throw` and `DateTime` limitations noted above ``` - You will mostly see `SpacetimeDB.Module` in [Tables.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Tables.cs) for schema definitions diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index e75968ac..c58e1da7 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -1,58 +1,238 @@ # Unity Multiplayer Tutorial - Part 3 +> [!IMPORTANT] +> TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. + ## Prerequisites This progressive tutorial is continued from [Part 2](/docs/unity/part-2.md): 1. You have already [setup your project](/docs/unity/index.md). -2. You have already [published your server module](/docs/unity/part-2.md). +1. You have already [published your server module](/docs/unity/part-2.md). ## Analyzing the Unity Client Demo In this part of the tutorial, we will: -1. Explore the SpacetimeDB Unity SDK. -2. Setup your `GameManager` connection properties. -3. Inspect high-level client initialization. -4. Press Play -> Breakdown game features. - -## SpacetimeDB Unity SDK +1. Setup your `GameManager` connection properties. +1. Inspect high-level client initialization. +1. Press Play -> Guided breakdown of game features: + 1. Chat + 1. Resource Gathering + 1. Inventory + 1. Store + 1. Unlockables -TODO +Start by opening `Scenes/Main` in the Unity project from the repo `/Client` dir. ## GameManager Connection Setup -TODO +![GameManager Inspector (+host name variations)](https://i.imgur.com/sHxYyS7.png) + +Select the `GameManager` in the scene hierarchy: + +1. Set **Db Name Or Address** to: `unity-demo`. +2. Set the **Host Name** to: `testnet`. +3. Save your scene. ## High-Level Client Initialization -TODO +Open the **GameManager.cs** script we were just inspecting and jump to `Start()`: + +```csharp +/// Register callbacks -> Connect to SpacetimeDB +private void Start() +{ + Application.runInBackground = true; + + initSubscribeToEvents(); + connectToSpacetimeDb(); +} +``` + +1. Once connected, we subscribe to all tables, then unregister the callback: + +```csharp +/// When we connect to SpacetimeDB we send our subscription queries +/// to tell SpacetimeDB which tables we want to get updates for. +/// After called, we'll unsub from onConnect +private void initOnceOnConnect() +{ + SpacetimeDBClient.instance.onConnect += () => + { + Debug.Log("Connected."); + + SpacetimeDBClient.instance.Subscribe(new List + { + "SELECT * FROM *", + }); + + SpacetimeDBClient.instance.onConnect -= connectToSpacetimeDb; + }; +} +``` + +> [!TIP] +> In a production environment, you'd instead subscribe to limited, local scopes and resubscribe with different parameters as you move through different zones. + +2. We then subscribe to database callbacks such as connection, disconnection, errors, inventory, player, etc. + - This includes an **identity received** callback. + - This is important since your identity what we will 1st check for _nearly all_ callbacks to determine if it's the local player: + +```csharp +private void onIdentityReceived(string token, Identity identity, Address address) +{ + // Cache the "last connected" dbNameOrAddress to later associate with the cached AuthToken + // This is so that if we change servers, we know to clear the token to prevent mismatch + PlayerPrefs.SetString(DB_NAME_ADDRESS_KEY, dbNameOrAddress); + AuthToken.SaveToken(token); + + // Cache the player identity for later to compare against component ownerIds + // to see if it's the local player + _localIdentity = identity; +} +``` + +3. Finally, we connect via a token, host and database name: +```csharp +/// On success => +/// 1. initOnceOnConnect() -> Subscribe to tables +/// 2. onIdentityReceived() -> Cache identity, token +/// On fail => onConnectError() +private void connectToSpacetimeDb() +{ + string token = getConnectAuthToken(); + string normalizedHostName = getNormalizedHostName(); + + SpacetimeDBClient.instance.Connect( + token, + normalizedHostName, + dbNameOrAddress); +} +``` + +## Play the Demo Game +![Gameplay Actions<>UI GIF](https://i.imgur.com/e9uLx3a.gif) + +Notice at the bottom-right, you have some tips: + +1. **Enter** = Chat +2. **Tab** = Inventory +3. Collect resources +4. Spend at the shop -## Play the Game +✅ From here, you can either explore the game's features on your own or continue reading for a guided breakdown below. Try triggering in-game features, then noting the log callstacks for entry point and flow hints. -TODO +___ ## Features Breakdown ### Feature: Chat -TODO +![Chat<>Reducer Tool](https://i.imgur.com/Gm6YN1S.png) + +Note the message of the day, directing you to emulate a third-party with the _Reducers_ editor tool: + +> Try the 'Reducers' tool **SHIFT+ALT+D** + +💡 Alternately accessed from the top menu: `Window/SpacetimeDB/Reducers` + +1. CreatePlayer + +![Create Player via Reducer Tool](https://i.imgur.com/yl5WBXt.png) + +2. Repeat with `SendChatMessage` to see it appear in chat from your created "third-party" player. + +💡 Dive into `UIChatController.cs` to best further discover how client-side chat messages work. ### Feature: Resource Gathering -TODO +![Resource Gathering](https://i.imgur.com/McdvbHZ.png) + +Thanks to our scheduler set by the server via the `Init()`, resources will spawn every 5~10 seconds (with the specified max cap): + +1. Extract (harvest) `Iron` by left-clicking a nearby blue node. +1. Once you unlock `Strawberries` in the shop later, you can see and extract those, too. + - 💡`Strawberries` are likely already spawned on the server, but the unlockable requirement prevents you from seeing it (client-authoritative) or extracting it (server-authoritative). + +Extracting a resource will trigger the following flows: + +![initSubscribeToEvents-Resource-Extraction-Events](https://i.imgur.com/xqJQ3Xu.png) + +1. **[Client]** Call `Reducer.Extract()` from `PlayerInputReceiver.cs` via `OnActionButton()`. + + +2. **[Server]** `Extract()` from `Resource.cs` will: + 1. Validate player, identity, config, inventory resource, and animator (which handles extraction speed). + 1. If valid, validate unlockables (eg: for `Strawberries`), consider items like `Pickaxe` for increased extraction speed and finally extract: + 1. InventoryComponent will be updated to add the resource and lower the `ResourceNodeComponent` hp by 1. + 1. If 0 hp, it will be destroyed. + 1. The resource will be inserted into to the player's `InventoryComponent` + + +3. **[Client]** Remember when [GameManager at Start()](#high-level-client-initialization) declared `initSubscribeToEvents()`? + - **Inventory:** + 1. `onInventoryComponentUpdate()` -> `PlayerInventoryController.Local.InventoryUpdate(newValue)` chain will be called. + 1. `InventoryUpdate()` will clear `PlayerInventoryController.Local._pockets` and resync with the server's `newValue`. + - **Resource:** `onResourceNodeComponentDelete` will `GameResources.RemoveAll()` those matching `oldValue.EntityId`. + - **Animation:** `OnAnimationComponentUpdate` will show animations from a RemotePlayer extracting a resource. + +💡 Dive into `GameResource.cs` to best further discover how client-side extractions work. ### Feature: Inventory -TODO +![Player Inventory](https://i.imgur.com/sBkgW48.png) -### Feature: Store +- On server [Player.cs::CreatePlayer()](./part-2.md#db-initialization), this chained to `createPlayerInventory()`, creating the default inventory `Pockets` and an empty set of `UnlockIds`. + - When the `Pocket` was created, we started the new Player with 5x `Iron`. + - Since the `Strawberry` unlock in the store only requires 5x `Iron`, we may _unlock_ it right away to see and extract `Strawberries`. + +See the [Store](#feature-store) section below for example items being consumed and replaced for a store purchase. -TODO +💡 Dive into `UIInventoryWindow.cs` to best further discover how client-side inventory works. ### Feature: Unlockables -TODO +![Unlockables](https://i.imgur.com/ShDOq4t.png) + +See the [Store](#feature-store) section below for an example unlock store purchase. + +💡 Dive into `UIUnlocks.cs` to best further discover how client-side unlocks work. + +### Feature: Store + +![Store Purchase (+Logs)](https://i.imgur.com/tZmR0uE.gif) + +Open **ShopRow.cs** to find the `Purchase()` function: + +```csharp +/// Success will trigger: OnInventoryComponentUpdate +public void Purchase() +{ + Debug.Log($"Purchasing shopId:{_shopId}, shopSaleIdx:{_shopSaleIdx}"); + Reducer.Purchase( + LocalPlayer.instance.EntityId, + _shopId, + (uint)_shopSaleIdx); + + tooltip.Clear(); +} +``` + +Purchasing from the store will trigger the following flows: + +1. **[Server]** `Purchase()` from `Shop.cs` will: + 1. Validate player, shop, sale, inventory, and required unlocks. + 1. If valid, process the sale: + 1. **Delete** required items from the player's `InventoryComponent`. + 1. **Update** `InventoryComponent` for qualifying `.GiveItems` and/or `.GivePlayerUnlocks` + 1. If any `InventoryComponent.GiveGlobalUnlocks`, **Update** `Config` by adding to its `GlobalUnlockIds`. + +2. **[Client]** Remember when [GameManager at Start()](#high-level-client-initialization) declared `initSubscribeToEvents()`? + - **Inventory:** + 1. `onInventoryComponentUpdate()` -> `PlayerInventoryController.Local.InventoryUpdate(newValue)` chain will be called. + 1. `InventoryUpdate()` will clear `PlayerInventoryController.Local._pockets` and resync with the server's `newValue`. + - **Config:** `onConfigComponentUpdate()` -> `PlayerInventoryController.Local.ConfigUpdate(newValue)` chain will be called. ______________________ # OLD >> TODO: MV || DELETE From f2d7c73645e2d39eee53c11f68f14d750a12fd70 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 28 Mar 2024 17:35:47 +0800 Subject: [PATCH 19/24] chore: Cleanup --- docs/unity/part-3.md | 414 +------------------------------------------ 1 file changed, 3 insertions(+), 411 deletions(-) diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index c58e1da7..53a31ac4 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -234,418 +234,10 @@ Purchasing from the store will trigger the following flows: 1. `InventoryUpdate()` will clear `PlayerInventoryController.Local._pockets` and resync with the server's `newValue`. - **Config:** `onConfigComponentUpdate()` -> `PlayerInventoryController.Local.ConfigUpdate(newValue)` chain will be called. -______________________ -# OLD >> TODO: MV || DELETE +## Troubleshooting -```csharp -// Start is called before the first frame update -void Start() -{ - instance = this; - - SpacetimeDBClient.instance.onConnect += () => - { - Debug.Log("Connected."); - - // Request all tables - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM *", - }); - }; - - // Called when we have an error connecting to SpacetimeDB - SpacetimeDBClient.instance.onConnectError += (error, message) => - { - Debug.LogError($"Connection error: " + message); - }; - - // Called when we are disconnected from SpacetimeDB - SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => - { - Debug.Log("Disconnected."); - }; - - // Called when we receive the client identity from SpacetimeDB - SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { - AuthToken.SaveToken(token); - local_identity = identity; - }; - - // Called after our local cache is populated from a Subscribe call - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - - // Now that we’ve registered all our callbacks, lets connect to spacetimedb - SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); -} -``` - -In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. - -Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. - ---- - -**Local Client Cache** - -The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. - ---- - -Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. - -**Append after the Start() function in TutorialGameManager.cs** - -```csharp -void OnSubscriptionApplied() -{ - // If we don't have any data for our player, then we are creating a - // new one. Let's show the username dialog, which will then call the - // create player reducer - var player = PlayerComponent.FilterByOwnerId(local_identity); - if (player == null) - { - // Show username selection - UIUsernameChooser.instance.Show(); - } - - // Show the Message of the Day in our Config table of the Client Cache - UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); - - // Now that we've done this work we can unregister this callback - SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; -} -``` - -### Adding the Multiplayer Functionality - -Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. - -**Append to the top of UIUsernameChooser.cs** - -```csharp -using SpacetimeDB.Types; -``` - -Then we're doing a modification to the `ButtonPressed()` function: - -**Modify the ButtonPressed function in UIUsernameChooser.cs** - -```csharp -public void ButtonPressed() -{ - CameraController.RemoveDisabler(GetHashCode()); - _panel.SetActive(false); - - // Call the SpacetimeDB CreatePlayer reducer - Reducer.CreatePlayer(_usernameField.text); -} -``` - -We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. - -First append this using to the top of `RemotePlayer.cs` - -**Create file RemotePlayer.cs, then replace its contents:** - -```csharp -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using SpacetimeDB.Types; -using TMPro; - -public class RemotePlayer : MonoBehaviour -{ - public ulong EntityId; - - public TMP_Text UsernameElement; - - public string Username { set { UsernameElement.text = value; } } - - void Start() - { - // Initialize overhead name - UsernameElement = GetComponentInChildren(); - var canvas = GetComponentInChildren(); - canvas.worldCamera = Camera.main; - - // Get the username from the PlayerComponent for this object and set it in the UI - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - if (playerComp is null) - { - string inputUsername = UsernameElement.Text; - Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); - Reducer.CreatePlayer(inputUsername); - - // Try again, optimistically assuming success for simplicity - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - } - - Username = playerComp.Username; - - // Get the last location for this player and set the initial position - EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); - transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - - // Register for a callback that is called when the client gets an - // update for a row in the EntityComponent table - EntityComponent.OnUpdate += EntityComponent_OnUpdate; - } -} -``` - -We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. - -**Append to bottom of RemotePlayer class in RemotePlayer.cs:** - -```csharp -private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) -{ - // If the update was made to this object - if(obj.EntityId == EntityId) - { - var movementController = GetComponent(); - - // Update target position, rotation, etc. - movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); - movementController.RemoteTargetRotation = obj.Direction; - movementController.SetMoving(obj.Moving); - } -} -``` - -Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. - -**Append to bottom of Start() function in TutorialGameManager.cs:** - -```csharp -PlayerComponent.OnInsert += PlayerComponent_OnInsert; -``` - -Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. - -**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** - -```csharp -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } -} -``` - -Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. - -**Append to the top of LocalPlayer.cs** - -```csharp -using SpacetimeDB.Types; -using SpacetimeDB; -``` - -**Append to the bottom of LocalPlayer class in LocalPlayer.cs** - -```csharp -private float? lastUpdateTime; -private void FixedUpdate() -{ - float? deltaTime = Time.time - lastUpdateTime; - bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; - bool isConnected = SpacetimeDBClient.instance.IsConnected(); - - if (hasUpdatedRecently || !isConnected) - { - return; - } - - lastUpdateTime = Time.time; - var p = PlayerMovementController.Local.GetModelPosition(); - - Reducer.UpdatePlayerPosition(new StdbVector3 - { - X = p.x, - Y = p.y, - Z = p.z, - }, - PlayerMovementController.Local.GetModelRotation(), - PlayerMovementController.Local.IsMoving()); -} -``` - -Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. - -![GameManager-Inspector2](/images/unity-tutorial/GameManager-Inspector2.JPG) - -### Play the Game! - -Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. - -![Unity-AddOpenScenes](/images/unity-tutorial/Unity-AddOpenScenes.JPG) - -When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. - -### Implement Player Logout - -So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. - -**Append to the bottom of Start() function in TutorialGameManager.cs** -```csharp -PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; -``` - -We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. - -Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. - -**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** -```csharp -private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(newValue); -} - -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(obj); -} - -private void OnPlayerComponentChanged(PlayerComponent obj) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it - var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); - if (obj.LoggedIn) - { - // Only spawn remote players who aren't already spawned - if (existingPlayer == null) - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } - } - else - { - if (existingPlayer != null) - { - Destroy(existingPlayer.gameObject); - } - } - } -} -``` - -Now you when you play the game you should see remote players disappear when they log out. - -Before updating the client, let's generate the client files and update publish our module. - -**Execute commands in the server/ directory** -```bash -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -spacetime publish -c unity-tutorial -``` - -On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. - -**Append to the top of UIChatController.cs:** -```csharp -using SpacetimeDB.Types; -``` - -**REPLACE the OnChatButtonPress function in UIChatController.cs:** - -```csharp -public void OnChatButtonPress() -{ - Reducer.SendChatMessage(_chatInput.text); - _chatInput.text = ""; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: - -**Append to the bottom of the Start() function in TutorialGameManager.cs:** -```csharp -Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; -``` - -Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. - -**Append after the Start() function in TutorialGameManager.cs** -```csharp -private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) -{ - var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); - if (player != null) - { - UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); - } -} -``` - -Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. +TODO? ## Conclusion -This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: - -**Next Unity Tutorial:** [Resources & Scheduling](/docs/unity/part-4.md) - ---- - -### Troubleshooting - -- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` - -- If you get this exception when running the project: - -``` -NullReferenceException: Object reference not set to an instance of an object -TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) -``` - -Check to see if your GameManager object in the Scene has the NetworkManager component attached. - -- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. - -``` -Connection error: Unable to connect to the remote server -``` +TODO? \ No newline at end of file From 233379850726f034c1796e1f1492631da6bd359a Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Fri, 29 Mar 2024 13:00:08 +0800 Subject: [PATCH 20/24] chore: Add Publisher GIF, update nav, cleanup - This should be more or less ready --- docs/nav.js | 11 +- docs/sdks/c-sharp/index.md | 9 +- docs/unity/homeless.md | 844 ------------------------------------- docs/unity/img.png | Bin 19806 -> 0 bytes docs/unity/img_1.png | Bin 7218 -> 0 bytes docs/unity/index.md | 8 +- docs/unity/part-1.md | 14 +- docs/unity/part-2.md | 29 +- docs/unity/part-3.md | 17 +- docs/unity/part-4.md | 261 ------------ docs/unity/part-5.md | 108 ----- nav.ts | 9 +- 12 files changed, 45 insertions(+), 1265 deletions(-) delete mode 100644 docs/unity/homeless.md delete mode 100644 docs/unity/img.png delete mode 100644 docs/unity/img_1.png delete mode 100644 docs/unity/part-4.md delete mode 100644 docs/unity/part-5.md diff --git a/docs/nav.js b/docs/nav.js index 9efcd534..bdf49e5d 100644 --- a/docs/nav.js +++ b/docs/nav.js @@ -11,18 +11,19 @@ const nav = { 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"), + 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("Overview", "deploying", "deploying/index.md"), page("Hosted", "deploying/hosted", "deploying/hosted.md"), page("Self-Hosted", "deploying/hosted", "deploying/self-hosted.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-2", "unity/part-2a-c-sharp.md"), - page("3 - Client", "unity/part-3", "unity/part-3.md"), + page("1 - Project Setup", "unity/part-1", "unity/part-1.md"), + page("2 - Server (C# Module)", "unity/part-2", "unity/part-2a-rust.md"), + page("3 - Client (Unity)", "unity/part-2", "unity/part-2.md"), section("Unity Tutorial - Advanced"), - page("4 - Resources And Scheduling", "unity/part-4", "unity/part-4.md"), + page("4 - Resources & 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"), diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md index 7c920cf5..f02bc764 100644 --- a/docs/sdks/c-sharp/index.md +++ b/docs/sdks/c-sharp/index.md @@ -65,11 +65,12 @@ dotnet add package spacetimedbsdk ### Using Unity -To install the SpacetimeDB SDK into a Unity project, [download the SpacetimeDB SDK](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/releases/latest), packaged as a `.unitypackage`. - -In Unity navigate to the `Assets > Import Package > Custom Package` menu in the menu bar. Select your `SpacetimeDB.Unity.Comprehensive.Tutorial.unitypackage` file and leave all folders checked. +To install the SpacetimeDB SDK into a Unity project, simply add the following line to your `Packages/manifest.json`: +```json +"com.clockworklabs.spacetimedbsdk": "https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git" +``` -(See also the [Unity Tutorial](/docs/unity/part-1)) +💡 See also the [Unity Tutorial](/docs/unity/index.md) ## Generate module bindings diff --git a/docs/unity/homeless.md b/docs/unity/homeless.md deleted file mode 100644 index 7699dc4d..00000000 --- a/docs/unity/homeless.md +++ /dev/null @@ -1,844 +0,0 @@ -> [!IMPORTANT] -> TODO: Delete this file, when done - -_____ - -### Create the Module - -1. It is important that you already have the SpacetimeDB CLI tool [installed](/install). - -2. Run SpacetimeDB locally using the installed CLI. In a **new** terminal or command window, run the following command: - -```bash -spacetime start -``` - -💡 Standalone mode will run in the foreground. -💡 Below examples Rust language, [but you may also use C#](../modules/c-sharp/index.md). - -## Create a Server Module - -Run the following command to initialize the SpacetimeDB server module project with Rust as the language: - -```bash -spacetime init --lang=rust server -``` - -This command creates a new folder named "server" within your Unity project directory and sets up the SpacetimeDB server project with Rust as the programming language. - -### SpacetimeDB Tables - -In this section we'll be making some edits to the file `server/src/lib.cs`. We recommend you open up this file in an IDE like VSCode or RustRover. - -**Important: Open the `server/src/lib.cs` file and delete its contents. We will be writing it from scratch here.** - -First we need to add some imports at the top of the file. - -**Copy and paste into lib.cs:** - -```csharp -// using SpacetimeDB; // Uncomment to omit `SpacetimeDB` attribute prefixes -using SpacetimeDB.Module; -using static SpacetimeDB.Runtime; -``` - -Then we are going to start by adding the global `Config` table. Right now it only contains the "message of the day" but it can be extended to store other configuration variables. This also uses a couple of macros, like `#[spacetimedb(table)]` which you can learn more about in our [C# module reference](/docs/modules/c-sharp). Simply put, this just tells SpacetimeDB to create a table which uses this struct as the schema for the table. - -**Append to the bottom of lib.cs:** - -```csharp -/// We're using this table as a singleton, -/// so there should typically only be one element where the version is 0. -[SpacetimeDB.Table] -public partial class Config -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public Identity Version; - public string? MessageOfTheDay; -} -``` - -Next, we're going to define a new `SpacetimeType` called `StdbVector3` which we're going to use to store positions. The difference between a `[SpacetimeDB.Type]` and a `[SpacetimeDB.Table]` is that tables actually store data, whereas the deriving `SpacetimeType` just allows you to create a new column of that type in a SpacetimeDB table. Therefore, `StdbVector3` is not, itself, a table. - -**Append to the bottom of lib.cs:** - -```csharp -/// This allows us to store 3D points in tables. -[SpacetimeDB.Type] -public partial class StdbVector3 -{ - public float X; - public float Y; - public float Z; -} -``` - -Now we're going to create a table which actually uses the `StdbVector3` that we just defined. The `EntityComponent` is associated with all entities in the world, including players. - -```csharp -/// This stores information related to all entities in our game. In this tutorial -/// all entities must at least have an entity_id, a position, a direction and they -/// must specify whether or not they are moving. -[SpacetimeDB.Table] -public partial class EntityComponent -{ - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - public ulong EntityId; - public StdbVector3 Position; - public float Direction; - public bool Moving; -} -``` - -Next, we will define the `PlayerComponent` table. The `PlayerComponent` table is used to store information related to players. Each player will have a row in this table, and will also have a row in the `EntityComponent` table with a matching `EntityId`. You'll see how this works later in the `CreatePlayer` reducer. - -**Append to the bottom of lib.cs:** - -```csharp -/// All players have this component and it associates an entity with the user's -/// Identity. It also stores their username and whether or not they're logged in. -[SpacetimeDB.Table] -public partial class PlayerComponent -{ - // An EntityId that matches an EntityId in the `EntityComponent` table. - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] - public ulong EntityId; - - // The user's identity, which is unique to each player - [SpacetimeDB.Column(ColumnAttrs.Unique)] - public Identity Identity; - public string? Username; - public bool LoggedIn; -} -``` - -Next, we write our very first reducer, `CreatePlayer`. From the client we will call this reducer when we create a new player: - -**Append to the bottom of lib.cs:** - -```csharp -/// This reducer is called when the user logs in for the first time and -/// enters a username. -[SpacetimeDB.Reducer] -public static void CreatePlayer(DbEventArgs dbEvent, string username) -{ - // Get the Identity of the client who called this reducer - Identity sender = dbEvent.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"); - } - - // Create a new entity for this player - try - { - new EntityComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Position = new StdbVector3 { X = 0, Y = 0, Z = 0 }, - Direction = 0, - Moving = false, - }.Insert(); - } - catch - { - Log("Error: Failed to create a unique PlayerComponent", LogLevel.Error); - Throw; - } - - // The PlayerComponent uses the same entity_id and stores the identity of - // the owner, username, and whether or not they are logged in. - try - { - new PlayerComponent - { - // EntityId = 0, // 0 is the same as leaving null to get a new, unique Id - Identity = dbEvent.Sender, - Username = username, - LoggedIn = true, - }.Insert(); - } - catch - { - Log("Error: Failed to insert PlayerComponent", LogLevel.Error); - throw; - } - Log($"Player created: {username}"); -} -``` - ---- - -**SpacetimeDB Reducers** - -"Reducer" is a term coined by Clockwork Labs that refers to a function which when executed "reduces" into a list of inserts and deletes, which is then packed into a single database transaction. Reducers can be called remotely using the CLI, client SDK or can be scheduled to be called at some future time from another reducer call. - ---- - -SpacetimeDB gives you the ability to define custom reducers that automatically trigger when certain events occur. - -- `Init` - Called the first time you publish your module and anytime you clear the database. We'll learn about publishing later. -- `Connect` - Called when a user connects to the SpacetimeDB module. Their identity can be found in the `Sender` value of the `ReducerContext`. -- `Disconnect` - Called when a user disconnects from the SpacetimeDB module. - -Next, we are going to write a custom `Init` reducer that inserts the default message of the day into our `Config` table. The `Config` table only ever contains a single row with version 0, which we retrieve using `Config.FilterByVersion(0)`. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the module is initially published -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void OnInit() -{ - try - { - new Config - { - Version = 0, - MessageOfTheDay = "Hello, World!", - }.Insert(); - } - catch - { - Log("Error: Failed to insert Config", LogLevel.Error); - throw; - } -} -``` - -We use the `Connect` and `Disconnect` reducers to update the logged in state of the player. The `UpdatePlayerLoginState` helper function we are about to define looks up the `PlayerComponent` row using the user's identity and if it exists, it updates the `LoggedIn` variable and calls the auto-generated `Update` function on `PlayerComponent` to update the row. - -**Append to the bottom of lib.cs:** - -```csharp -/// Called when the client connects, we update the LoggedIn state to true -[SpacetimeDB.Reducer(ReducerKind.Init)] -public static void ClientConnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:true); -``` -```csharp -/// Called when the client disconnects, we update the logged_in state to false -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void ClientDisonnected(DbEventArgs dbEvent) => - UpdatePlayerLoginState(dbEvent, loggedIn:false); -``` -```csharp -/// This helper function gets the PlayerComponent, sets the LoggedIn -/// variable and updates the PlayerComponent table row. -private static void UpdatePlayerLoginState(DbEventArgs dbEvent, bool loggedIn) -{ - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - player.LoggedIn = loggedIn; - PlayerComponent.UpdateByIdentity(dbEvent.Sender, player); -} -``` - -Our final reducer handles player movement. In `UpdatePlayerPosition` we look up the `PlayerComponent` using the user's Identity. If we don't find one, we return an error because the client should not be sending moves without calling `CreatePlayer` first. - -Using the `EntityId` in the `PlayerComponent` we retrieved, we can lookup the `EntityComponent` that stores the entity's locations in the world. We update the values passed in from the client and call the auto-generated `Update` function. - -**Append to the bottom of lib.cs:** - -```csharp -/// Updates the position of a player. This is also called when the player stops moving. -[SpacetimeDB.Reducer] -private static void UpdatePlayerPosition( - DbEventArgs dbEvent, - StdbVector3 position, - float direction, - bool moving) -{ - // First, look up the player using the sender identity - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - // Use the Player's EntityId to retrieve and update the EntityComponent - ulong playerEntityId = player.EntityId; - EntityComponent? entity = EntityComponent.FindByEntityId(playerEntityId); - if (entity is null) - { - throw new ArgumentException($"Player Entity '{playerEntityId}' not found"); - } - - entity.Position = position; - entity.Direction = direction; - entity.Moving = moving; - EntityComponent.UpdateByEntityId(playerEntityId, entity); -} -``` - ---- - -**Server Validation** - -In a fully developed game, the server would typically perform server-side validation on player movements to ensure they comply with game boundaries, rules, and mechanics. This validation, which we omit for simplicity in this tutorial, is essential for maintaining game integrity, preventing cheating, and ensuring a fair gaming experience. Remember to incorporate appropriate server-side validation in your game's development to ensure a secure and fair gameplay environment. - ---- - -### Publishing a Module to SpacetimeDB - -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. - -```bash -cd server -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. - -### Finally, Add Chat Support - -The client project has a chat window, but so far, all it's used for is the message of the day. We are going to add the ability for players to send chat messages to each other. - -First lets add a new `ChatMessage` table to the SpacetimeDB module. Add the following code to ``lib.cs``. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -[SpacetimeDB.Table] -public partial class ChatMessage -{ - // The primary key for this table will be auto-incremented - [SpacetimeDB.Column(ColumnAttrs.PrimaryKeyAuto)] - - // The entity id of the player that sent the message - public ulong SenderId; - - // Message contents - public string? Text; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. - -**Append to the bottom of server/src/lib.cs:** - -```csharp -/// Adds a chat entry to the ChatMessage table -[SpacetimeDB.Reducer] -public static void SendChatMessage(DbEventArgs dbEvent, string text) -{ - // Get the player's entity id - PlayerComponent? player = PlayerComponent.FindByIdentity(dbEvent.Sender); - if (player is null) - { - throw new ArgumentException("Player not found"); - } - - - // Insert the chat message - new ChatMessage - { - SenderId = player.EntityId, - Text = text, - }.Insert(); -} -``` - -## Wrapping Up - -💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) - -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` - -If you get any errors from this command, double check that you correctly entered everything into `lib.cs`. You can also look at the [Client Troubleshooting](/docs/unity/part-3.md#Troubleshooting) section. - -From here, the tutorial continues with more-advanced topics. The [next tutorial](/docs/unity/part-4.md) introduces Resources & Scheduling. - - -___________________________ - -# Unity Tutorial - Basic Multiplayer - Part 3 - Client - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from one of the Part 2 tutorials: - -[//]: # (- [Rust Server Module](/docs/unity/part-2a-rust.md)) -- [C# Server Module](/docs/unity/part-2) - -## Updating our Unity Project Client to use SpacetimeDB - -Now we are ready to connect our _BitCraft Mini_ project to SpacetimeDB. - -### Import the SDK and Generate Module Files - -1. Add the SpacetimeDB Unity Package using the Package Manager. Open the Package Manager window by clicking on Window -> Package Manager. Click on the + button in the top left corner of the window and select "Add package from git URL". Enter the following URL and click Add. - -```bash -https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git -``` - -![Unity-PackageManager](/images/unity-tutorial/Unity-PackageManager.JPG) - -3. The next step is to generate the module specific client files using the SpacetimeDB CLI. The files created by this command provide an interface for retrieving values from the local client cache of the database and for registering for callbacks to events. In your terminal or command window, run the following commands. - -```bash -mkdir -p ../client/Assets/module_bindings -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -``` - -### Connect to Your SpacetimeDB Module - -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. - -![Unity-AddNetworkManager](/images/unity-tutorial/Unity-AddNetworkManager.JPG) - -Next we are going to connect to our SpacetimeDB module. Open `TutorialGameManager.cs` in your editor of choice and add the following code at the top of the file: - -**Append to the top of TutorialGameManager.cs** - -```csharp -using SpacetimeDB; -using SpacetimeDB.Types; -using System.Linq; -``` - -At the top of the class definition add the following members: - -**Append to the top of TutorialGameManager class inside of TutorialGameManager.cs** - -```csharp -// These are connection variables that are exposed on the GameManager -// inspector. -[SerializeField] private string moduleAddress = "unity-tutorial"; -[SerializeField] private string hostName = "localhost:3000"; - -// This is the identity for this player that is automatically generated -// the first time you log in. We set this variable when the -// onIdentityReceived callback is triggered by the SDK after connecting -private Identity local_identity; -``` - -The first three fields will appear in your Inspector so you can update your connection details without editing the code. The `moduleAddress` should be set to the domain you used in the publish command. You should not need to change `hostName` if you are using SpacetimeDB locally. - -Now add the following code to the `Start()` function. For clarity, replace your entire `Start()` function with the function below. - -**REPLACE the Start() function in TutorialGameManager.cs** - -```csharp -// Start is called before the first frame update -void Start() -{ - instance = this; - - SpacetimeDBClient.instance.onConnect += () => - { - Debug.Log("Connected."); - - // Request all tables - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM *", - }); - }; - - // Called when we have an error connecting to SpacetimeDB - SpacetimeDBClient.instance.onConnectError += (error, message) => - { - Debug.LogError($"Connection error: " + message); - }; - - // Called when we are disconnected from SpacetimeDB - SpacetimeDBClient.instance.onDisconnect += (closeStatus, error) => - { - Debug.Log("Disconnected."); - }; - - // Called when we receive the client identity from SpacetimeDB - SpacetimeDBClient.instance.onIdentityReceived += (token, identity, address) => { - AuthToken.SaveToken(token); - local_identity = identity; - }; - - // Called after our local cache is populated from a Subscribe call - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; - - // Now that we’ve registered all our callbacks, lets connect to spacetimedb - SpacetimeDBClient.instance.Connect(AuthToken.Token, hostName, moduleAddress); -} -``` - -In our `onConnect` callback we are calling `Subscribe` and subscribing to all data in the database. You can also subscribe to specific tables using SQL syntax like `SELECT * FROM MyTable`. Our SQL documentation enumerates the operations that are accepted in our SQL syntax. - -Subscribing to tables tells SpacetimeDB what rows we want in our local client cache. We will also not get row update callbacks or event callbacks for any reducer that does not modify a row that matches at least one of our queries. This means that events can happen on the server and the client won't be notified unless they are subscribed to at least 1 row in the change. - ---- - -**Local Client Cache** - -The "local client cache" is a client-side view of the database defined by the supplied queries to the `Subscribe` function. It contains the requested data which allows efficient access without unnecessary server queries. Accessing data from the client cache is done using the auto-generated iter and filter_by functions for each table, and it ensures that update and event callbacks are limited to the subscribed rows. - ---- - -Next we write the `OnSubscriptionApplied` callback. When this event occurs for the first time, it signifies that our local client cache is fully populated. At this point, we can verify if a player entity already exists for the corresponding user. If we do not have a player entity, we need to show the `UserNameChooser` dialog so the user can enter a username. We also put the message of the day into the chat window. Finally we unsubscribe from the callback since we only need to do this once. - -**Append after the Start() function in TutorialGameManager.cs** - -```csharp -void OnSubscriptionApplied() -{ - // If we don't have any data for our player, then we are creating a - // new one. Let's show the username dialog, which will then call the - // create player reducer - var player = PlayerComponent.FilterByOwnerId(local_identity); - if (player == null) - { - // Show username selection - UIUsernameChooser.instance.Show(); - } - - // Show the Message of the Day in our Config table of the Client Cache - UIChatController.instance.OnChatMessageReceived("Message of the Day: " + Config.FilterByVersion(0).MessageOfTheDay); - - // Now that we've done this work we can unregister this callback - SpacetimeDBClient.instance.onSubscriptionApplied -= OnSubscriptionApplied; -} -``` - -### Adding the Multiplayer Functionality - -Now we have to change what happens when you press the "Continue" button in the name dialog window. Instead of calling start game like we did in the single player version, we call the `create_player` reducer on the SpacetimeDB module using the auto-generated code. Open `UIUsernameChooser.cs`. - -**Append to the top of UIUsernameChooser.cs** - -```csharp -using SpacetimeDB.Types; -``` - -Then we're doing a modification to the `ButtonPressed()` function: - -**Modify the ButtonPressed function in UIUsernameChooser.cs** - -```csharp -public void ButtonPressed() -{ - CameraController.RemoveDisabler(GetHashCode()); - _panel.SetActive(false); - - // Call the SpacetimeDB CreatePlayer reducer - Reducer.CreatePlayer(_usernameField.text); -} -``` - -We need to create a `RemotePlayer` script that we attach to remote player objects. In the same folder as `LocalPlayer.cs`, create a new C# script called `RemotePlayer`. In the start function, we will register an OnUpdate callback for the `EntityComponent` and query the local cache to get the player’s initial position. **Make sure you include a `using SpacetimeDB.Types;`** at the top of the file. - -First append this using to the top of `RemotePlayer.cs` - -**Create file RemotePlayer.cs, then replace its contents:** - -```csharp -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using SpacetimeDB.Types; -using TMPro; - -public class RemotePlayer : MonoBehaviour -{ - public ulong EntityId; - - public TMP_Text UsernameElement; - - public string Username { set { UsernameElement.text = value; } } - - void Start() - { - // Initialize overhead name - UsernameElement = GetComponentInChildren(); - var canvas = GetComponentInChildren(); - canvas.worldCamera = Camera.main; - - // Get the username from the PlayerComponent for this object and set it in the UI - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - if (playerComp is null) - { - string inputUsername = UsernameElement.Text; - Debug.Log($"PlayerComponent not found - Creating a new player ({inputUsername})"); - Reducer.CreatePlayer(inputUsername); - - // Try again, optimistically assuming success for simplicity - PlayerComponent? playerComp = PlayerComponent.FilterByEntityId(EntityId).FirstOrDefault(); - } - - Username = playerComp.Username; - - // Get the last location for this player and set the initial position - EntityComponent entity = EntityComponent.FilterByEntityId(EntityId); - transform.position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - - // Register for a callback that is called when the client gets an - // update for a row in the EntityComponent table - EntityComponent.OnUpdate += EntityComponent_OnUpdate; - } -} -``` - -We now write the `EntityComponent_OnUpdate` callback which sets the movement direction in the `MovementController` for this player. We also set the target position to the current location in the latest update. - -**Append to bottom of RemotePlayer class in RemotePlayer.cs:** - -```csharp -private void EntityComponent_OnUpdate(EntityComponent oldObj, EntityComponent obj, ReducerEvent callInfo) -{ - // If the update was made to this object - if(obj.EntityId == EntityId) - { - var movementController = GetComponent(); - - // Update target position, rotation, etc. - movementController.RemoteTargetPosition = new Vector3(obj.Position.X, obj.Position.Y, obj.Position.Z); - movementController.RemoteTargetRotation = obj.Direction; - movementController.SetMoving(obj.Moving); - } -} -``` - -Next we need to handle what happens when a `PlayerComponent` is added to our local cache. We will handle it differently based on if it’s our local player entity or a remote player. We are going to register for the `OnInsert` event for our `PlayerComponent` table. Add the following code to the `Start` function in `TutorialGameManager`. - -**Append to bottom of Start() function in TutorialGameManager.cs:** - -```csharp -PlayerComponent.OnInsert += PlayerComponent_OnInsert; -``` - -Create the `PlayerComponent_OnInsert` function which does something different depending on if it's the component for the local player or a remote player. If it's the local player, we set the local player object's initial position and call `StartGame`. If it's a remote player, we instantiate a `PlayerPrefab` with the `RemotePlayer` component. The start function of `RemotePlayer` handles initializing the player position. - -**Append to bottom of TutorialGameManager class in TutorialGameManager.cs:** - -```csharp -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent callInfo) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } -} -``` - -Next, we will add a `FixedUpdate()` function to the `LocalPlayer` class so that we can send the local player's position to SpacetimeDB. We will do this by calling the auto-generated reducer function `Reducer.UpdatePlayerPosition(...)`. When we invoke this reducer from the client, a request is sent to SpacetimeDB and the reducer `update_player_position(...)` (Rust) or `UpdatePlayerPosition(...)` (C#) is executed on the server and a transaction is produced. All clients connected to SpacetimeDB will start receiving the results of these transactions. - -**Append to the top of LocalPlayer.cs** - -```csharp -using SpacetimeDB.Types; -using SpacetimeDB; -``` - -**Append to the bottom of LocalPlayer class in LocalPlayer.cs** - -```csharp -private float? lastUpdateTime; -private void FixedUpdate() -{ - float? deltaTime = Time.time - lastUpdateTime; - bool hasUpdatedRecently = deltaTime.HasValue && deltaTime.Value < 1.0f / movementUpdateSpeed; - bool isConnected = SpacetimeDBClient.instance.IsConnected(); - - if (hasUpdatedRecently || !isConnected) - { - return; - } - - lastUpdateTime = Time.time; - var p = PlayerMovementController.Local.GetModelPosition(); - - Reducer.UpdatePlayerPosition(new StdbVector3 - { - X = p.x, - Y = p.y, - Z = p.z, - }, - PlayerMovementController.Local.GetModelRotation(), - PlayerMovementController.Local.IsMoving()); -} -``` - -Finally, we need to update our connection settings in the inspector for our GameManager object in the scene. Click on the GameManager in the Hierarchy tab. The the inspector tab you should now see fields for `Module Address` and `Host Name`. Set the `Module Address` to the name you used when you ran `spacetime publish`. This is likely `unity-tutorial`. If you don't remember, you can go back to your terminal and run `spacetime publish` again from the `server` folder. - -![GameManager-Inspector2](/images/unity-tutorial/GameManager-Inspector2.JPG) - -### Play the Game! - -Go to File -> Build Settings... Replace the SampleScene with the Main scene we have been working in. - -![Unity-AddOpenScenes](/images/unity-tutorial/Unity-AddOpenScenes.JPG) - -When you hit the `Build` button, it will kick off a build of the game which will use a different identity than the Unity Editor. Create your character in the build and in the Unity Editor by entering a name and clicking `Continue`. Now you can see each other in game running around the map. - -### Implement Player Logout - -So far we have not handled the `logged_in` variable of the `PlayerComponent`. This means that remote players will not despawn on your screen when they disconnect. To fix this we need to handle the `OnUpdate` event for the `PlayerComponent` table in addition to `OnInsert`. We are going to use a common function that handles any time the `PlayerComponent` changes. - -**Append to the bottom of Start() function in TutorialGameManager.cs** -```csharp -PlayerComponent.OnUpdate += PlayerComponent_OnUpdate; -``` - -We are going to add a check to determine if the player is logged for remote players. If the player is not logged in, we search for the `RemotePlayer` object with the corresponding `EntityId` and destroy it. - -Next we'll be updating some of the code in `PlayerComponent_OnInsert`. For simplicity, just replace the entire function. - -**REPLACE PlayerComponent_OnInsert in TutorialGameManager.cs** -```csharp -private void PlayerComponent_OnUpdate(PlayerComponent oldValue, PlayerComponent newValue, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(newValue); -} - -private void PlayerComponent_OnInsert(PlayerComponent obj, ReducerEvent dbEvent) -{ - OnPlayerComponentChanged(obj); -} - -private void OnPlayerComponentChanged(PlayerComponent obj) -{ - // If the identity of the PlayerComponent matches our user identity then this is the local player - if(obj.OwnerId == local_identity) - { - // Now that we have our initial position we can start the game - StartGame(); - } - else - { - // otherwise we need to look for the remote player object in the scene (if it exists) and destroy it - var existingPlayer = FindObjectsOfType().FirstOrDefault(item => item.EntityId == obj.EntityId); - if (obj.LoggedIn) - { - // Only spawn remote players who aren't already spawned - if (existingPlayer == null) - { - // Spawn the player object and attach the RemotePlayer component - var remotePlayer = Instantiate(PlayerPrefab); - - // Lookup and apply the position for this new player - var entity = EntityComponent.FilterByEntityId(obj.EntityId); - var position = new Vector3(entity.Position.X, entity.Position.Y, entity.Position.Z); - remotePlayer.transform.position = position; - - var movementController = remotePlayer.GetComponent(); - movementController.RemoteTargetPosition = position; - movementController.RemoteTargetRotation = entity.Direction; - - remotePlayer.AddComponent().EntityId = obj.EntityId; - } - } - else - { - if (existingPlayer != null) - { - Destroy(existingPlayer.gameObject); - } - } - } -} -``` - -Now you when you play the game you should see remote players disappear when they log out. - -Before updating the client, let's generate the client files and update publish our module. - -**Execute commands in the server/ directory** -```bash -spacetime generate --out-dir ../client/Assets/module_bindings --lang=csharp -spacetime publish -c unity-tutorial -``` - -On the client, let's add code to send the message when the chat button or enter is pressed. Update the `OnChatButtonPress` function in `UIChatController.cs`. - -**Append to the top of UIChatController.cs:** -```csharp -using SpacetimeDB.Types; -``` - -**REPLACE the OnChatButtonPress function in UIChatController.cs:** - -```csharp -public void OnChatButtonPress() -{ - Reducer.SendChatMessage(_chatInput.text); - _chatInput.text = ""; -} -``` - -Now we need to add a reducer to handle inserting new chat messages. First register for the ChatMessage reducer in the `Start()` function using the auto-generated function: - -**Append to the bottom of the Start() function in TutorialGameManager.cs:** -```csharp -Reducer.OnSendChatMessageEvent += OnSendChatMessageEvent; -``` - -Now we write the `OnSendChatMessageEvent` function. We can find the `PlayerComponent` for the player who sent the message using the `Identity` of the sender. Then we get the `Username` and prepend it to the message before sending it to the chat window. - -**Append after the Start() function in TutorialGameManager.cs** -```csharp -private void OnSendChatMessageEvent(ReducerEvent dbEvent, string message) -{ - var player = PlayerComponent.FilterByOwnerId(dbEvent.Identity); - if (player != null) - { - UIChatController.instance.OnChatMessageReceived(player.Username + ": " + message); - } -} -``` - -Now when you run the game you should be able to send chat messages to other players. Be sure to make a new Unity client build and run it in a separate window so you can test chat between two clients. - -## Conclusion - -This concludes the SpacetimeDB basic multiplayer tutorial, where we learned how to create a multiplayer game. In the next Unity tutorial, we will add resource nodes to the game and learn about _scheduled_ reducers: - -**Next Unity Tutorial:** [Resources & Scheduling](/docs/unity/part-4.md) - ---- - -### Troubleshooting - -- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings` - -- If you get this exception when running the project: - -``` -NullReferenceException: Object reference not set to an instance of an object -TutorialGameManager.Start () (at Assets/_Project/Game/TutorialGameManager.cs:26) -``` - -Check to see if your GameManager object in the Scene has the NetworkManager component attached. - -- If you get an error in your Unity console when starting the game, double check your connection settings in the Inspector for the `GameManager` object in the scene. - -``` -Connection error: Unable to connect to the remote server -``` diff --git a/docs/unity/img.png b/docs/unity/img.png deleted file mode 100644 index 83d34e00047ec4a055d1581b1830b9b890730fb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19806 zcmb@ubx>SU*Y25wgg}Bza7g3YXmAPcZb2G?yLaP~1cJMyarfX5q_HGuaCZqVjk_}^ z?>F_;RNcAv&P>f86vgRdea_lzKkN6bO_-XBEaofHS5KZi!IYPi(s=UZDFXPhMtcTq zsW2F`gXXVltKFP;*L6R^B`llm%rTVQ&PqQUnPtNn4o&8oWAy{`{d z>C>ar0Pd#ul%ZQiE6Hs`mQ{Ji-ttcGK0O=FU59Fi;~n|j!o|kMNXr-Jb7)ZP)UU&d zr>=Ia1}ia)t@DKrEA+MA`Vw*xl8_}{a;!zg%7s!E3n3Rlj9Od?eZuw#+J-RQ=xx8Q zEf1mVOvfXfbG)F~H$jLNk+Nm$6YV9odX$i}T?;f2TTl58m&*rpr9?qbhS0ERaxS^L zU!$fA!}!>~dVp40q|!L2RGJ z5Ks}m!E+rG^Yq&>eWgU?#|k@D_YB;@0e(p3h8M=nlM##cRITCe$m$9zTd5-0w(Buh zZRIAZjmZkdT+{KQw$prLSGG|+&KTZRK|j@27?NFq6@hQ&K!Il?uyQ=6!Nb5QLFjPi}4oVmrLFjLg>?A=D?&eLr`4L7+b;$kPUakfWZA4I| zEOuK-zW}~rQ3Op0f~|?EZ1-{SJE6Ttll|Hy_EyB&R2AeNT=NLZkM7+d=Gbe<9!pmIGrS+aj@yl zJNaWNfmsGCELw?-LQn9id_jw*mMna_G%1gog&tv)3i@IY^c?bCqf@xZ@=rE`IED-_ zg=9-#2y>@&rZYEd05^k)<|~+{i|*MppEe(ZF{ie^hB;hQINS-G)`0ZgFSPB~y*R1Q z|8m$HJU7}PG5H!_!rv{mQc+G}YHlgg-MA+Ryv%qcg%=#2#?8L9WN*NY`=;Qs>s9sj z0*{G|G33F|BqI-NV%OD75K0oJ!#`9Tl-pk-5h{__TpKs+4Gqf^%_0-92`V19NEM); zg0&V7t=yngmR>V#mQ?)=EJ=g2o}l(w*) zxpdG$x#BnPow1Cu@QD25>dfBDcX2eP!ZaI9i(kO_c)4{~DxHSk+e9^Xb{K-o7x?_jw3{ts~2v;!3L+ zj|@f)D7xbBq*Z!6}6zU+K!`)q@^IScdVy^DlC zaXXY`ry^jbF+n=_gZnZE?t6IY@J9r@XgH}rk-L&kdgHO5HN6%cm;%c+V3W~3)@~ZA z=kYom&+yiD&_3pipjGlDke|_q@!`(73DfilKcvoYA z^d^ViRepa<7$yxeO^$jxwX zoQ!ehyQRCSjYI;YX4?F(Ibf&HjPsLJsSgVU9;XIywOVf&aPil$9WgN64mtGDW^0#! zUDV&Oj9E8p|6gxuVuOv7J0QDqk<{j4$Mp|4%EW)-x47Tn_sYn-@06FSaQTEU{}t5# zHts-0YfG~#|B~AJZ=d|kc}kxk|DO>Uf@sm+i+;P0zBxPmvUizE_H@y}iA=C8%UfPT zPVp!EP^-3tq~z(V=3Co?`nn`L@0TxLl)!0&>Yh(*IAnVqF5P1B=Fv{isH9WQG#F$5 zSH5P4=woEMZ*fR%V|c@=2*=w4rai$E!f1 zj>>k=iHMjUTPLQ`g4M1l8Lq4)>xAEZr@q-0w!2-9`{s=$m&e1EVQl5qoi$NAX8o+t z*Px(cI4G!&+<#Xi3y1Eu`-}Pz^4-l!G(yrbNvKqDe2;Ug+-BFhRjL>z3WuD0q4`oX zB%0?i5ZQ+L&eM8Zv&CFq&TdTyhCVP%tJk0;*`q~n<;N+B8YP`q-YmVWB{)^XH0fVD za_vw6vqF^64jpe8<`)!}J{K7CDp?SAYU$N(FXv%Te;XJM9#q8RY`=%bog!b&r+MYd z^`QG2=EnT!kDH0O9u2D^bqH}KC+h1AI_>Fiy|@gIY@(qs9)S7lI_QZJ;+kdpoaz($ zds}|sQZMtbUOlzWFFYcS4qGA72|sPY?4 zhS*T8FyD)eP{xS0JUq!h90&xm%D5-7eBoM;VT#weu`*+q%rCbml8m_@?nOO#RAN$^ z{1S223k}8Wo``YZ*|mgsr_kg`7weS$u#FcE*d9ASedgzzZx#01;)dTP+dmP%0{%;8 zd$!g}POgWd`Sxb6DlWKg!gPP8+1iOW}bwR4tL~xd%L&G{1iR>g5q~-y8fppYhS35QXesGFSXu& zdWtgf5??}}q(K~BKkcTRv<5Y*8E-n3+DA;p94EFihOiC465uUS+el3gb7a3f;+;U3wl?w$laso|O5P>6{Y9 zZuy!;;&9w*-kxg>r;?N``5KB0n*n0Wbb&FKoPlIct?5tT>e%}X3EHl0WKUzd?)TW& zW-a$ppPOVBKAlV!crAHOoD?!2SG-4S5<8ZKPqn6G*27Y(KPFy+%~q`E`)9vx6^5D` zE`dYV7ARc)R$xfOr$kc1!I%lA_8~RpFAK@=x|GQ>6yOKGnp>F)Mze_@j8n0US<(=5 zEKjEPZGyE_{i3u9Ao|7wv51MVb)G-&90nJ0Oa{EFfy;3e{b*1oNF~k+M_2@^$63NP zAaJ9^5G}1}m`!4BHin%SsG5 z5DHpSZcW3HIFP;@Ex6ya@7ZuI*l`Umm7T~n<}sqMrN5CzH_^fmxp7mpk z7M~mcYOka+S&?8{2^xtdI#*B%wu*-#tD7M+pJOsL1Ntj$&i2HOt5OUXzsV0$O7j|B zx+wuhwo7R%KS+U``ow=1PX&X*B0(9IW;0TTJ~aBgyyXb0D!QEkK>$J=Z0w2uKCXLG zz6N_Zm<}<*dH9EeHRY2^mG(>N;OIBp+|9rgfp<4ia4wZO_9fDU3rl_Xl}xg+lCF$5 za_o9dxPVg)sO&t_L_%x{kx@Ss?oFIkAP%Q=ggL~E^OP)E5!J_xe= zN>;EU(Ev-CuOcnz894%j>#I;(g&Ha&@Cssee(z5d?01+#F4U0KhIC`G;Cq6o_D3IYu;ZUpCmu^Mk9_Tj1^3yt^vtKZ*n?yjFImw*K(VdQKFRVm%VKAa6si zr;BTGz~tnnumK#WPWp`@xLfF4+pL`7v;chm8c!`bgiczTAO`BI39|DY-y~S8{j-uH znP`R%%9lQpg>Mp#w;FWrjpoplZ!YeLZ#wT`hH-|qiT8r-P4m5+g4^hg$bYaXr+^r# zCmP-3uRnfkb1iGYjMFkrKK{xC>R`=`L*T4+mlS63#n1tj&o3$Csb(Duqm(krxv^wjRac|R`gVi$yS6z5EY;iNwdq8&ztKk{esPNS z7X$Cm3u`=KiLuQI^CgPn4#B$Y?NMNd%Awd_q2g~`O27Pb>9|2b_Ji%9dPSC+jM6u`6iFqBjXOWdKPrrap@`R$Q}-f2)gYtIM_jrX;$y z_FQsbumL=H+TGtWtVPK#%Z%Wm#j42yyBadO`3lY_zr(=ouT5Yh3wkad)IlqiM8fyOGHLsfw zob4`C|B{_6I}U>rUd$qbVVvKM*3!)g+|b4YA$hB^iF~!imvmB$R&*=xj4~|Pmdm5_AI z^mrZFBhwc;1mWaw!+WXEr&`;tOY5-{BDCkU5fO4>ky6r`=aY_+`Qdm1RN^7F1?a7- z_BS1(Eg7%q^9zo2;R&W^Yq{p!N))+c1tzO+Kgf&y&T+f#I`99ThA2~sMKKJ>V7JnQ zhQSM3&TQmJgIozIM`K)9&*?+ND}f2q)Ts}r<&HWxh;Hz-M=^#cUoqclHE?c)8?T3I%l4JdsS@dKjJ+Z>7?;gBBTMCYxaBH&a#K0_V}XE z4LMe0S(9!UNphwPn#EjNBzS3w)+uAvwiK`Je0d(;c zgoNuE#FTJWmO2uuaq-mLT!}1P7W4cg{zbJg!a#!0mhe-@GIk3zxm^o z?z#^dpl33gDkw4uMQmEg^PgYo3ana}ayi;_RX&m9BO*h8m=Ib>v<&_g!1Zab_?gll z7h98_j-b87^WUvGHZ^PsS}2K?GIt9H+p%k!;yFR64a7M^Bjs&s?)>(_Bi{g9>C%YcPlorv1M9?SZlLKxMx}hGN6lK8a#WXQNDJ}s6DU0_ENJLh z9zD}`V!AiZp>iuXyhq~XeU75XYMC3u#0XuDO-tT{*WLkxLV+Uf@4hsuOupf+*sa23 z_(Ea^zqvi_(#%o7mPigk?uf-c3IPJzj#PuM8?8of<1xj+8W@};x>q2PeEEv%2=7Jn zpM`s&6Fd5Z;f}O+T+ie|bXqbp{A4Ze=!6oIxgi*@6`K=8tCRTU+!Lh+l0_E$5jYgL zJJcIfv78=3``b#;P7xdrSXlx(JO;c%vF=?{iBH712DJ?AU2DKmlx}*#urhghshs!9fX2G^gXk)IG>y7p9;_z zjVbTqQ4su{8XP|wd#BCtwp`Q5$FoaNlyxcwS9cJ~wR2lIrbQVO$MGUw8G?d{tV;Wr z7tWu$&0=l5HRgU3`FlNBo>R~Rj7}Bibzv@HzIv2G<6>G><{*9&cy@RM_ z_s8^{O7K%x5#$ArFyZE8g1u9Cf8uIA#_mSy^RlmQfi2L3*Oe7nn=4Nx`c^BZ&q*cY z`SD$zy(~0?%=A3CKV72i#A8W_^OrbSMIEgMgk#*9K&4NG)7uc(earhM4E;(l$TTj< z664)w6rhk3rg>UmCGq_6jxH9n_X0Y%w_Uk9#VSAb!pf%n_BCjSZflMjmFUdU#fB1o z-N2eYK6?l}GG|V}cfDMV0hjx5Ya8u;h)oxhl9JS^dY(IupJ7Z zJp82Gf2z4bfq`yhzr+^-JNQCiCPnh$79S0FCr66=OP=cnF}CM%+s6og@obDoT3{sdiDr=}p_VCl+FJPHKUvHJV(6bOFH zg|CRO=l5}3;cmS>LwKdeOGTKrIP<*uLGkCjE&4Mn-nWo1$8r|4V9R83np&|8xzDo9 zmxa&+F9OKwXGKW3$*RpC0&HfB-odzV@NvM(c~C^}=bg+>9jebB^y%%r*>F@S~YMWgF5oJR^dLHFM1kl)xgK=V%QgqgN!xJeT+(d6Wq zZL1?I-x`VxDo;d9i+hX-+CC@-MD@DS|4Z>BhQJv6S}e&$Or9UT^z0Yd&pBV5;vB?3 zS8S0?HF3*fK=w-=mFKV*;xZSs5OtNOcw1DfY@n$vzV(B-qmNd5L`R{}tiJh~JGI6=IXW*@MI+H}-6A-rB(X+O z9~0p~HBjSc_(f%#xEVt~db2dd`!h@%C^M`(0kZ9HghNbPCI#gVZt~ajG=P$rzw+>f zc{LoK z&Yu2?{NuHv4nA+wwByJ=TOp%9Pf?8?K2j>zutf zn<+<4e>}%VT#U-k{E6bQmkr=I;niG{d{hI!Yd?pc@B21YnCRuYz6D9q09DS{s$Ag~ z>wV+IUAxr+@K;e$W~}<1k!jr?McJRS3Kv$syt7Ao~fl5rT5g?l=?66S2qYQwFp5h z+X~s3f(!7UEi%~nG;s7Xnulnw`viNqli0^N`|Gm(h}I&Fk?Zb)GYt{b->wc?4q4BJ zxEIi2_S3)j{U8Xf>#6!|guqDSLc>;T)7>GWO*Za}M@fd@t@8Qra`E>)c^wq(W;)*J z81FyoDGeWym6|2C;Bft!jTSfecKkDG=SpvBaJVk3(Tp<-z4&Xg!+7+!V;xmYqkSt+ z1Y_|&gO!DZXS1xP`5A;GPCvikAf)~khkAQG_8wHGmAE_AaPn+X;w%&Yn)ECx8?1XJ zXAU11`GQ4al2o)(u1M#FQNJCF+9yiRpvl{l^vbIeT?ISs?}>1FK4XdJHeP*v$X}-T znu{q5DZO;9rsvpFqTh$8Y!kCRYhA=}t|&-y@vte7??vq%r3{-BvDHQkfZrm0-f(+#zy`&#tg!K4pwH1n|Dx+HBFG^Vm6w%hLIPWg%I! zDr^TsZ~pW2mkvev!Fz2^|1q=TSmL}~MXkQxtKXWae|)*s%HGXiuAo5>Zo}_Wt}C>W zV^!uY9yYA;NCFqTegpW=V`aJ}``9{Wn?k;$k~IyzY&dxEWl=~GwhroF@jt#<2-;4k zyJ?GTA-AC4123g=inj5(%1jCx8{cfVscf|vvE8o5Z}>=eV`?T6wRi;-66j>lYu1=f zTJ$i_rgyrU%Se^*cN)Nx2Yp90Kca@WurFMX3(xYEDU9hxN^eAne#5u=U=YqsMD_W4 zV<#M(ge)~l$SXe#olKjci`S$5T}WY*Y%C-7P2UHJeu%?$2_U9oqziow)pnaX376Ei zQh}m*+)ZEgYKA;Rp|tnG{Sm-9hNz?;inlckkDZM%(V)wrvS8L*pmgZl=i-5NPeYVA^l3E91|p@S1CK_!yTLI&l7`!r zL60+8@67HHQe2~m4D(G)mn&0Kkc?K4k1(A&cA#6w+pf<%wWW(!XnIXTrxpG5CY*B$ z-zyyKl1a=2?rk&9XCVe*+ZzHr%s=|X5@%cGI937a8XT+~-kp9zYTXK8g`gZktYKp6 zHBWuDDl^ddH?7l53=*v!E@&cVq2>iDqB1DeqaZ9KOQ6%0U)J&_9Bg7vn~(U7Ym$(A zJ3j&k1iy$p-3iUw&7YY97Sa-Ox}MN9kkL=v zZlXfd{L&{qilndbFKumwE6@`WXNZB9XRq;DE6pQrS{sKXLW3~em)~LG`K4^5|NatX z7as>lcPm%gylQPjX52iGkQo=DB5* zm=uu@z>CiaDNbLZfXNIT*Gh^1dp`C*zzlyE@PFfts{+Lcaf;EVz#BB!h!_W9YSRJp zIrty`BK$8%fsBS~k9d{$0fid#_DcZ&1F}${QY+NGb*|;(mq;`JMTZ(rP|(%qECdwL zepVwaxGRmuZuYLjQ`- ze-{wU>(yW*(_kG|CZs{`;>nV@1tV=?VL$eYUY)yuU9QUb=SNd=H3g?`#-IXA2)=PG zP>a1kJ-TZdrsbbpN?ZU^v|uQp9ixcBN@9M@4i9kAV*W1NxGn)`10h7eL4{kJjxn(7 z^1AUnC>>0#&p&b8Wq5DRAXvZV<@_B00;BX@{Guu42?*hMJEM4OBQ9J#saHEGJ>Q9A zoRgi=?Ck;qzRmFNLIN@>xkDF+kdwIXc^@_sW`oy>1)4a`7^lXXU^b~D-Q2H1K|C)X zVPUy5H>Rk(8OvHSjRSlzua5{$r-{K|#!1o`)jRpxL@J-&>>m>JK`t&M%5Q60(o}X& zU?u%hYi0Zvlri8a@5H4l+HW$yw@2yTEM{3SM{{q+)KCmuM7A3mS)H_+x&knEmG1%I z_e11>(c{4WT9OGNmE+(w<%Y}s+eQMxaC>t+DksO5rlGuX%w-C=AOhYpGAR~1#mwD= z`QWIpd|$1Tg+Gkah5N1N*Gmd?+p9OB$jx%Vs`ikwf-rjZ6HR~%w>QxN4^K(b)+YH4 z5Ed3vk&}RpWHUreFb4ppbqfV_W;-Cysi9{85&OQ;bp1Pbtp9H6SOLfdXH2`yS}y8G zYh2N`1_n?FmL@8%4aD)QnjOe#tpXThAOar-jGXm1K!O4Ixd!|ui1ExqZM%lIM2gqS z3)F>Rz#r3!jyE$`FZ(DLUhSAke=s~zm_AYfYbZX}4*j|?4OZWw`BitI#HSQmUzV1O zQ6RClF&$?HAyb800+eiVf=4_>W*j4=X>a;8iMd8gAK}^ zwj$VFg!veR&741$g9n?(i`!!UDig?Ddpa4p!x{ONd>Gh=Q7|XZVd(IC z*9yMj5S86;Jesb*HT=jlZfOptKJU`EGxWB;_jfLZ%1pVM@1`1Em<7dgbEeFieyNBE z0%BNVZ4WKD2;+`kq6{<+zqpQu(;GQV?WC*PMx8g zF=w|ce@_bo#70ihj)6WXc0Mu-p%w1(0&>7{8nIJ@-E+OGVYtzv19S6TI{Ns%!U&T4 zgin-jmfL=lqon_>Tcay3{zkxu+}A0E=hhXGK$)Xkpo#`+$e1tek0nk(!oT)%O4~MZ z59-1{ANahRA^b*c^MPy0yXI}x=RGc8@3QlZFC)hlM|H#i`!7-K7(?HL*GS-}AC;Yk zkj~(Xn`Z-0iZcw*(}2|;w4i=BN>?Li3_(S3ixUrC*>goQIrXDKD$>?PP|t-*Ej#-1 z-%5VSHULQe*AFO*h?bPBAxZ}?ww|ls6GwHwl1K1aBRZa3@j)ZEUOgFMCv{qan;2#g z0L73v5dQl>^$7gJ&iRk|^FK^(|0}2HZq@L=y`wA06HpM&{{wsJ|KSyi^ZHjhLSiUI z=7e(N78VvtOPOJbfC&Y1+^zAuy?DINdb~zfwqNYePusn}Cm2<5qm&qZW-xCYN$v&W%;+vdmoS$iyWbn)jD z&5H+qp@Wu^BSLrt1QX>34Np-}8x6t4Q$9qN-FNr5mzP+05^~>H3&EQ#=5f>O4&wu!xk%4s;_gJ1_6| z(o&$K!9qsB>%^R{SxSb4oSgh`L4P!b(8JYQOp$6S(39 zx;o(Tc6m5mAdvqi6rPxPwliLYK#-z1XB(yh*CM2ZfDP&~=Dy&s{CIpgd|YbtYmRI` z?k3wB&eD~UbRUb+fSZ_@a8+!(E;PD?()>zvv6=7fl`B!CE#+zhx+uu_9G^GZxp%6j z^Q)_=edJ16H(;YSxWGk|h@KQ29gNt`*UVQQS%8vsc|i z6<}EEzS!bl7G=WA! zLt$|ONx&YT7?yKx)q?bokg8Lumi+oQExwip1%@I?Un|H8HX-wrD& zO@!~&ub1<}Wk4AB-pk@6FlGe=R-^3yGpxi&$2U4C=H@DNl81(D$rhwrV}PN>^0>9%>^8B7S^U(ht91*j+K35>!`9YV?V(q;pXS=H>xS6kAIyUHa<*Fbd|)5y z>itP{a4FVnSziZ}sn<;P<^!!CCM)-U4sEJ-EkF78Gg9QF%NPwcqepf- z+FbNZitOxmh-&4k1T~}nbA~b%q3Bid6;{}1yKa#TjV+|x;mhpa7ZJ^VXL^65vLyZH zn<}5d%lAZG&k}Y!fY~(-6?Q%INoDGFK}}_VkIZeae+@Q7bnH(8-3uqh17Q`0+Zd?V zf`99)4B2MUm8^urGe1@l4a88Fr}^5G)F$6LhxD<2}-H zl%uhh)T)UtPLN;@reRGY)qiP z_bX4afkLBCIAgZO#_%PZy!9P|)1O;-Bj~>VUvm4OTqgA$2nMhuY z@DZEHtu)x7wj-RyB9Jr13~$U9xF1q4;as0efk2D)*6&%@>05zPm*^Xl5Wggk2#1c% z%zs_8<$(rcbXx_%s(=4IE#_N*p_^zr=n2Ehq((~zDL zNl_j}xU$ZwdZ3^jLo|uO*1>h{@M25ij!$A~4kZ=GUz_u}pxAGDggdU|5tVexgK96< z7?ha72F0&<_u zZ>{&bTw$^iaQ&bnz7w=Qu~kn#)Asnmc5~ot?^%AhwE~D7U&wTS(=XY!3$f2@mB`i3 zHnBd^8B@xt`POh^0_KSJ&)r)qSaOWce`Ai zB|CdHOPhX=3|5vem!Gz=wTI3EjcK9X(Aj2>EC_p6tZ835r=7kIr6Ao##79Z!&BcC$ z?>;Zu_wNR9#jHh)gy7(v3o0+wsfn2%uJLc37tmoJhHhhh~X%>$W0Z%(Bz5jn}EI%3Sy>GEOrq0P+KK#vY4FUOM0 z33*oh+{R4Kx)E>odMr)E+taZ(GFmm=ptTIR;{=Twa~xg_%;_9_V)8FaI2oMi^4E0Q zI?al_#eLTG(p{aKgQc;UH%R^O{f>(W6LbN>d z>vGN9wtq^aYL@fCIS+ex0^3TNydv$58qMGY2EJxuu~V1qc-`MgLYF-EOw_aCR<*UY`iOGf!zG~>!#5(H#LTl5ju84H^S-yJvcyV|Rw(*LjuE)r zsFWu^Tqc3ACIRylT0Th^Kg;H0r2CCnNK|jPrp}gWVoyYV*r?Un{`!v4HJ_VVrWxGv zo5pCy1&w08J+{&vf<>{^BIdAj?{yBU9(3NoJnmm>;Z^}26*^>&JQEpNTp7{-wZ48& z0IzVo|E&i1dN3qyY-0r1Y?BSba{0wMLwv_4k6^7CtBnwE^N$s0?SzH5`{x$4w>$Hy zzO188kS|@|d8s=fwJuK6n+v<>*oS+_AHk`Y?Bd-~+RKbiHC1X1}64vC2Q*^HFd;8L3+(P!?=mo`ar%yr7iZgtM0 zJ2LlL%|J`(F6fU}SCR(&vpA?%u!o@_dmEe#p|d**hBlep_UEnP59$;(fcrN^5>f@Y z-LI^h1i&tREf&aiX^nlv-csJ_B$b5kWTCBY+iOa`w%XOclJU6=!ST+Jc|Wp=|Gvpj z(6W#2=jnZ2$N2P2ztqjThbuljk4s^7C;9UyD{tFXWdlzdzTN+^!Ev$3Vnf_Puc-?K zCD2^0E4`FX1OCz1m6?~@^_6TSS7yxs(u8%l(}x*>%H9j^Vbd+orh)R7hG$f2N2Kk48)$hOXf(np3*{>aYb? zw4cI%(t3$3Tvc}Kj8fgz_+FiQ+x79`e%Bf$o6@+d@fX>G?9z6nj+x)y((?S}hXNC) zo@k1HZOs3fL^3H8JH)UTmr5DAtJBg?hmI~k7fUsY>cVF{ZD?PVDBxAcP^t}*ID^*+YSwb0iVvQj60K z8|h$% zIH33~sLpZ%L0Y6PQbJ|N2v$x#-xY`w=TWK`-=a2aF>$S@E2k({NU!^17 zn4IRth^7MNrmEO|VtRVyQ^jwr;#K+i`7JHvI<5Zqg56>_+j*|~d#ymZ;IZrnPf76t zDgu)9w7USy&dCJt#~sxG3GYT1yTGiTvIY>_zS!U4fV(xS$#I}1oyryxU4QRlrf!Cq zpcwMCHRllAr@4J0D)ux(B~Jn>%SGkgf_Vc-a|}Z`&flqm>+w}y5ux6WeO(sBuyZa7 zir^#f<1bmtFWMbgg(5bGf&YYKmH8(FIM7jRihmF2Z_w4>JZ!PMb+anB`D`$CBX5pw zb=A7$K4lbt=KX}T3q5=LovX4kreIa(geg$c7ZS^DsuERE6#@3|MhlY zEy^44{+_c)WL@G5evrp&7*sFeMD?)Zp~tg`VYWmU{>vc#MvQyY1>&*6nD3Jz^HtPj zmnpT02(RNw1NfGWvLNy^n9A`nc`r?r$-C@OvnJ*()=qpeTvi+|r@sN-{;?cCT0?~p z(=N=KVzI!fF#K+i`s4A0U*P>S6iOj?`=h_Vu*F!p48s)QIXO9fAEn_nC}U$|y!JDn zhU_X^c}+XfNPw5$o~zjLDZt<9T&yDVxP7CLeT0f@aQX4!QjRxj^JT4-aEkGSkxl5& zqL~QJ#}oDzbd}ykDGJDD*W<&{??<893sF_T&hfnZwNwxYtF+Q%AN-1si{@iPD&yVo z=?2O)KKz6o610es{FPm7*EM5)?OkM zd*TIdVM5SCL2<-!+*H~$IFOeCCePLDzr93I(%*9fO^mBY5fTbW4Z+fAanz%%6}~!> z#P%R~VV{7oKivB+luAdFtQi3wt@ag=WjdmoULJ6!%UJAvC{Tpw)7_760BGdZYY{6h z^In`oQ{T;*LWn$C)1?}6>{dthlDudoBZUGLb6Xn^#l))u(v~?{%W)%baJb|avW;a$ zJqA9rF;9gskYv?e$aM2Lmv7BktRZ~!``0e)l2oJ6WH>1iR;4nK)x>}05kJp%NgSvf z`?XNlS5PQa!>HL10o?(JQ5<1mVWEVx!(1p~47q@Yni_}i)zPQTZH>z5M*uAVoJd+) zS~iCAe9EG=!5a;9DQpW#MWLE8b~z1{0)|o0UNUP1o+!7>?#aqU%y|hhX$xD^bowvG zfsHD66IID*#_!EmLgvpAdqT@@``91hxr%2NoJ%0fPsJr!S)YWfRnMz{$lTb^ds5P0 z08B;|RO~Qx!6@{=xR=6Y%Pyf?Y8+n@#AV%YVOWWcq1 z&!c|)=-=|l+vgEoUs^OS;!-k&Jg_pPZ6432?;odsQnpA-_TGo;!?PXI+9PY04zr$1 z*>8Q^y219`i{%v#h*5?we@FjFF@<5iv0Uj~y&F1B@|R1qOqen5;A(w)p^Pi&5#@uAEq$x>2KZPKzNU zy1-5#cKBgr-3L}+-!|8czU$L1*~lYb)pY>JrY0sne|!K2S05jj#2}UL(J#Jb-$1jC z24P-6cD+zXnGQSL)801VZc&xC|0F~c!Oy#RGD&{AMW4!|$?KbjpdhVYp8v$mJS4$dN-gV!1NO{FzM^^biA4Uh6LH$)r;ZSXS^06KK?&^5kI?mv852eq5J+Epq$5T^>@j~SU1cp%T&BW11ZMCOO z#5^x;Bmy){^r5qCn3sp(=+ z60txwIOpG4x^HVVi4=%&t{sOSJSBx*t2N6~OfOoUe|$nY9eBQIN*%NS=Gi$JNa2o) zi(B#mq-mDdW(tr~t%lpYAlMtVWj%f{5-O^8t6@rubwHGD5P3=do4mP-w_Q7eF^gwC z_GZ-e^sd){Q8G%mYf4bHgu9Kg0}{N4N6XMrw$PP$c$J--mbgQ5QDuLzq^=B zj6BJQZ4QTb7!P{p*Cs2AkJ6zsu6;h2V1lAsq=~U%`GVAKS`~tDzJh&14TawCYE;U@ z-+JEW4;`<}5H{vyzAglEPbfX)8yxO5Uq9$`2uNvB>)1xI*AM_N{`$xBrvvWcd$+q> ztgHjKsH1=fvbMVVY-hht`v;4|bW%W!u z{<=cY@swtd4dAL#uh5q`1)xr^Yy;@z(3w`oLuq6PSqWh&Mqaqe{YTv)3xRcCtZ6L2~ja*-WVS^QKhS8AQvU% z;dNTGzEy)AavT6P3?=z^SB2xf%Ws~&vM7w!kIYU(*pJr+?$4hTX$}DBG>9VE@)lue z6ekr&Sx1L%HAAN!^XkUMFXj^}5QU3xYV5-A_!Dy#Cw7SU?n#)+Oo1Yy2GAdp){}If z)?)D}g*`b$L?GxAaK-_3Jt`4z0+d8gXXm4-as%B8!!S`npsbxL)2r#!5rqV`hJ!(} zC5j;(pC5pAd4PXVZ#|kTOX>9-p~bZj3`LJs0g^1K_uEOK9UsGKzrS29Bv_o`I(1b<|c)rf&6yAOZ z$U89P|J;=2xr%qKpO~1a1UUQO{;}Y( zBw$$_uv4ilF0i}_`+s_^Sc3xSL2`I0e?f`IGRwd9P63~&FDXMGPHI-VhjZHY#~N_i zTk(Ea|6x2g@!$roo$pmF{t98#bS;bbKI95{#9H_Co@x2F5NSbf?VEvEA*IO>Cotu_Z z0?x077-Pz?f5qjyMF$&z74@fzinp30gMoI5iD5Ug;452|Ni2$ktZ$<~AZ);YnRsOh z^I2PevK-guaM>nz9$!xO?;O~2Z0x=i2T1-yRBq;6t!e=0EFYJ;@5^j2CVkX^=YLIk zXE!yJAq056H)D$IgIQw6tubOpgtYsM+yL5^^`@(B z;uBydmdi(7aCS~IHK8x>5qpq>ZJH}9&mFUEGoes!Pictd6?Y>t{AIVc>GJt}f!2~b zZ3#9rS`4v!Qns_W;}s!IAepT%dJ;p$OiX_SqQ6C4mUBIs{gJdQxD}CG+SHG(3bs_W5N^*wM@5MqLp`#s z_K=#Q9KvQxY|;Q%Q;dt52>ZLvPhsc5Y?+&`pD(tJ;a+xww1dl$H!v_Dxz(_1Q7K_N zE(_6|PCOi=prC+{G~$truF-GL1sA@-*w|v_Y@*v2#O-CqX{oejU_q3(3rn1;T|k(L zuP)LH0YaG>YcGb*lY@rtH^I$nJSjpYBVqKlHoQD~OG8WhOtPCkK{+#3SCV(91rCB?u)49BVhy$^pLbq0CdWTXKc@`T5;{JJq*Z_MB!KERe(b&MlmUZuKMR zqProUxqg0rYKa%OOM$B{Tc+-vAjLdrC@=p>s%qqVl{r`+kTXoZdq6dAM0H2*(a_O4 z_HFLiTinqP$k}-5P*cL2W}(`M>yd=)Xi@(bjGcS>fLBcQcI3M=CXL_T)uP%hfpSA& zRd|Qp3a?1qkN9OT9jF63c5oKogez56OP}IG1z*go*o(|c8W$E@Zr|e*$Zk|;8$h9# z_W1kzBbZ=|bOX2hnAAIb8- zh(CJ}s??4iqq1sI@^a%`Zol!@jrC@2kxs@nhO7*#CzZtkF}QnO+Xdw8*-GsL%ylEJ zegZ1sco_BW_c!&$#Da@IJ$Tc3%T!+r zhZLdwT+o@>1E4-&CCB^|4mG~er3COwPnkX7x`=R5r%_GO8vi-rU18o?N?&r|uF%MD z0N9_o-kPfVK3Hp{AyfupfTjWQP7F5KduS=+c+juIsit!Z#JHw@Ut09u@!ov8T~4ru z3*)Q_=>}+m@qH1mbut*<(fqOqb`%e&s0rNs>$np=uR$$A6DYn|ri6~!Mg?q!@PV?V zcf{QH-q)Cc*+(Gl?a5p@AM6t!XoxjDYzhAcP6e|520p9!;p;Ut>{L@1 zmlR2~Bg$MN+F+9#qj*$GvxwS!;YYx_OB&9(WFIEhZtTS{BF3?AhrW3AenVYA`?MEb zzET4i2I~v199C2OwI>91{$D3T2iSVSy=v*&`g)FnNv#wMRU|#Mjv&0lo_exmt*orv z%OJY~L@|A=5yK~&@*cxxRMCBN7R|B{aD~ujk%tC>hl~FuHd^GaGGOdhz0Jjhl?uQF zWdOnNIfVi+xpXFaWs)b;C`=~(1PLMsTYqYnHM8KopnS-20HFv+N%p7G^Yii=xD-7> zXq1j;MdxqiX&)60NcM(4feG|b}!lu3(Jf+IW@dCbIvXWe5OYXL1&wZO#892Z=ij$9+s~bY ztj{hNUeTajI5mi|h+o-}j?El_JZs^E+1M(wB3?%M>-F@E?v=Y--A|i|E`tFSv!qc@ zQ+3-EzB>OF)y;WMcq*AA4E1RZ1!ij(I6V8qTJA?ji|5`CoD>`j4*%@WANEL$v0nd= z$&Jb{?wZPMdU{bfmj*t-w*ZtR84n~vRODShD0nuEqpf8vw?QDaLsNUtW?s~Z9a!Iq zLKPVLpfVGr!L8V)t_0||_uTRe#1bNr*mzpj@{&@MWmHt*yD#l9)DQzhm((C>5YizXgKWARBn1IMYLJj2MLGlq5oYKXk#3PtKuQo1 zMM`NT&-Q=Uz3Y6q-_H3m>s@>5-Ou|xzuM0X^);xVEKmXh0xB&{H6sE7LTYf0hY*3U zh(~Lz1O%*qwbYc2gKf48=I$Gt7Y<%rD_eHuXdA3v_wK9judmYPGgrIgoCc7jshd0L zI4x_@(!NkbnNu(wp$A?yHj=!5Wu&A&ceU8*@dCDqVt8IIhgk1l% zFhBo1J$<}TeqnNwdmzX9ZD3P#^U=Wp0~~IjhU2?_JzvJZtf9gGc;}0;h@fD3b2H;K z3oC1j&vG@zom;m^u3h6bvaksGIaW|nA*5EWVr^~hS3^Zrwg2+w&6{UuXW(Yt z6*p>;l*Oe`et{mUgn0Ez3$x>En()@$Z~d5#j*gz5p6>2$Rw-{gILk%!HJXXa{k^?f zFF!vCATK|^(d>rX^74~?J!4}AkL2WJo3;S*g_1AhqoWgrv~0}GQI551du#9GOUue^ z-aI6|Dk4a;Z1L`9+qznNpEX1-j70MA@GKR+VyT3xwwh-A?z{>gOlFTK zAtUPuIYi#LL3wd{el7=vlzgUvK$Mz3*VqmY4t{Eet@b^mhuL|1Q$G+2-v99-F_Dtw z_i|^7H44^Cp@&i zkxsf!4w5QxcV&p_(OZ??rd)rrp;c!+?}P-_KjqeqXZsHl2-d#kFd zzA3+V=F+dPtu=fyI;WJ(A)lL-rQsaJNip^LGuZ1+uCAM=m6Tt|TUuIJ!{dW`C4o7Y zJ6VT`QWu9;U9j^|w_wHy2U>jduNE_w66c9)sgs=}DgD@pX?Z?_X>h_Oe1`p)5g)&N z5m9)~#=;^iF3z=6auH=mORcu-yaJOQPECc~lEhCV6@qUUBPU(dB`1)k_yMa%R z=3KvFXT0Sz%@@c|ski=qA5L!r7Fv8#o;{lk>sYX#xYXS;KAv zR@-*D#_5g_LR=bA@(S9ekue)9-M)%h!0{R@D}J7xm6nt=GBI(PDO_h^W9zd}OU<8% zJ~O5oChi|Exa)d#J+GzViVB*ok6Yp5;6C#1-HG*eH%rUBotsxA1x}A2%QUOp zj@S6P>S@VVQdUORJOBOrb;+)T1SnhBlPBjVyQ>sO__Umyxlf;X8NY z7gK~rWhTk5>iRC`=d&>>$GJL0^nieR|NedIn%8j+w}3zwrWv_Q0qqv;YB>ss|DSik z5C{0Qva@GNO>tx;RKD1GMpmRxM@Q#_#V3ucpzlMp7BL>#QQ1vmQavJ#2B;D&b`lbx zeDHWSwf!SIB#1pH+I)eV`nt(*e0pDTe;n4@6Qf!*oZE!*3kk^`am(_R78On2fYct% z);T&%Rt*xlExOIPY*UaYU@$`QWJzAXBz~Wq1cw+++fVf83<>DkHBKFxgGn#Pa&r0u}^s4y!H{C6u+YDp|1(|xJA;9pizmBN*rHU<*VhuZMDL=h>fVIM=&<3?cdthT(UL(Q z8~aryn4iT;jKD|`5q&gLl2;1*APYM4PRR_5rP~L|K!dpdeLM)fv_z!XGjZ zNlV{eqh?cT;o*;KE5h8YGbIh$4%6=v%;>RQwY@Oa^()X;ffqctkvKkfP`&WTk~WT^ zbI1+k=U79 zQc6~yZ%cn{`dat|_SU6}6~B2u$54;e$r2@X6LVc*TZfBm5%x-sP)Cc7sVUeuNa=1? zyfJ-pNMl?cevp#J{N_~K2Ld4%VSB4_d$aK3EeaPoJ3HIap|Ge)DYm`6%~NcI>OcSS zsL-8?QgV2B*db#_NkT%RT)f2ZXv;iKtRW%2Z(GA7nNca!x49~0|Mr6#XWmz|V(o3e z&04J<-oNv}i#^XU`RP5yJ9lC+u@N-vvTATPvVwwVJ5T-n{n=d_3dcVhE8CkUzY4ru zcB?zscpq6y`rF5@@tM45*csXTH>e1AOKg{V*3GC_XfJq#6H!Y^mPK%$LiFQFE1I0S7X4EIP&UO39vC*pY&Aw|JcEh;r;cGgMtH)PKn)}P=J~mew zUKUom-*!!7 z2|c~9C4MQW;!IKp(z7_8m={Rn?V&F~I$^BR?M+B`jAAM^sm#SA?NFI<_9nDDfO>Lk z^3B9JEDjo0OPU^h0i`mDdHfp%@YVQy+&Eu-E(-`wo;ns2^T8SoMiB%1D4>7Yv)Ri3 zl>3P}oJH!7Df7`Yt2iY?uS-8tj+{Uy6d zB=rhSv-odjaPNwu!|kq>%^N{drM5Ci3OtgMzGw_8|PfV$Pnij9Gxv}Ps##R~?{S1H<{oV=%@v58Ie z8^ZPV_y3aP&dtpYXr7vy@?2=9iuv+3NPv%T7@w(uLeb_XDYRJ~ZOvh6n4=_cQ!_L7 z)YJ@RL_|cy#C%pxouB9BVe*PoBnMUuGgjFLgSJyVWC9y&+iN$CnRv_&n};=V*ZKK( z7FrB!nkbvSzgXDX+M1eboLR{r$Pc-B#g*UVQ~vYkbh;1dfr z6G7GwkRV{hpWw7l$y@K;3XMXzo$~9!6J<)1DxsIXw|u&9qd6L{aR7@+?~v>QccH>i zY=dJ-J(dCqty|ZHcgA3$-fTPkdNQ!5i11uwYu*8r@btbD8zeS1_FEw49bjxs@}P(d z{o%uh0LWGpHOkB^o)N90sR^lw7lrSoYEI~${Cb0hMqS-AF=4C*)mnFx()X??;**F3 zsDm+Wm8}VB1+M(rm0}x}wetY`EdJxtq>Yt%zVmQ`49wETw7M|T{?DNGLUq(k;?frb zk9C4oVw1n_Volv7P>0H*T!N%H8!hP5VDCgWFc20q(5|r4LR96f;e?`` zp2~!dJY{EXO>P%Ff_NSSA6Oq>(MV;2Wu0lik>j)Rgz+1r2L_TqU;N15lB+0(%C_dm zoXl0IJWH>H?p5#Y?a@&&xZNNEJ6q7yV{5i98Z9IwR31A?K6v=)d!u5>O94DOjmacl z{d{o!QB?pHG&U{{v@d!2c(n2M;ILn8+vd4O z1=mPs7pfNJH?S&4TSn)p)4N@_Zv8SjIVs4c0rQ75;q2<8gm&`YT@oavE90*d z&Rr=Yw>MTMxhhHGR;dvv-sOUz)=;E+lb72?aEC-BAt7Na55m&7ZBII~o6w70nZ-zV zp5&eh>6PlIJQB(*g3s)S?k0{O%cQce?EEd9C|xI8xCqf^d`&r=WU$>HZx0XT4tvI= zOb@KGf4dA~TpX)!Ne&p^BlM@yenn!YHZeX~eaB}h(XlwGSO@+)s$`|uNB#EWU}dPP zSYtvwHv&5Xo%{e~2Mv^PdUDrld)%~v;=fJ*8RVPZg#g(vp_i}@*~)~L-jqNg(2D(E z*bKg_DKb-SeU>%qP>ZyolKVSd6MmStd_E9oPHEDQ?+)%M6zl;ZZ%Ft+1nq}T!>OsO zPsaV6*3#3<;PkU!lBh?Twq4OjJWJziQ%unD8u;1Lc~RBg>N!(o$pk`KN(geG{NbOk z|BSV+zW!4CV}BG_+myt&mu1`Pw`nyUtfQ0uEF&XHSl2wXGf66 zGC&l7H>n<(m`MHLf_(@I_-VW;27_TT*_8(Em9+E;=n%iHc17OHI6ptHuC6vvykJoM zFg3*x28r05sj_{t+h1}2^+Z7tOXX8e0^(DOAC;6_5@5K+jH$MDcl7thZ8XWNLRrq*BK;oeDd<_q1e}Nad>xW=SkMzI-ikOhno(cZ3P(=eLGmoX*Z78K5yODJhZnN!`3TQ2!9#>6ob6*bai&uYBY=op|xc-~FFy%JH(Ab>iAe@Pk5z?iEe= zt?1odKUDh3&fDJ#Q(b>}(9(uAPcO_4L?x8qjfa<);dSxcr2drYWg`a%jq%HEkHC}$ zRL7Z_jDM%a3-PTqakyD*iS~mB!aFP(epx?%{tRm*8MVovV!fl3SX(Qe5dHl5^OY5Q zPOoYnJo+d-HC4%(ZK#~7A}fv%7b$F7&88U6Cd7Y`ou6O&IRbUhAi5F0_f@tevFIj{ z+R2^P|6-t2UNcck-`!nkU=eIc=k~$@!TKI1j57c$Hg|Jy_y#&oFR!W;dVs)NG!Xri ziod9-OL*#_?mp=%4Rustj2zqc#_KS>IRr4VYguKID3ZO5@$pe!S$yN4on_nVTU=H3 zVR?BOETOD}YPJ%NzzRZHuRJexpGT^ne`p!TkipzrXR02)pLV$gDF0N)eZ3s{%?*-U zU_by@c3z$iEhJJMx$0Z8g?&dENf!O>uaDxoQExVFEjO{s*{|xDJaKAJf*!2ki``)(NvPFMc5TOA=)ra|}2Z#S4 zNdiYgLb9$ZJYcKJnGv3zVJ>ron-Exeq&)Ch5a>AhhAFrkRAOjkWMpk!W&7?17$umP zn!ct0#nEEhBONBEOi%)xa#<~mD_)(0Fk*rSJg$$@AS$(;o!2I@8i)8SQv4ouD!|Oa zAu1-OVgIR2uc4s(@j2P0Gf8D6@ot2ff^L+1po|I35E6T&e1B^^xmzDka z@ne2r0qm%W1C%Hd8S?fnm3c;TGWDX|*0pIfI592GbE*9=*tR9+#sC0jG0)ToKAQo$ z6dD;|UQ%4l4XQ3lG_1F@;^Eo|{PfVN@sd{~nJYUp(?u-Oko1fVfhbBzpYyzPmOENc zi|gy_v+4}DzJ&oL;_$Fm-Ox!JkOAhJ2L}Nm3JM@@#;ENh(p91@+l}>f^lYvIVcIoMn~hpJyqcnQ&KiL(xgB^&^fbh0e5;p zPEO81NkQQVP^y*Fad0HxGP{KVvqKD%Si*n45D*lEVb=8Z6%Sk4kTm}F`?r{oP%)P` zH{OA3uMq@A1wq01k%EGPY}W)Jv1SF=l|;cLI>EcA+2+06VQpww3x=9YOEwaw<~U?| z^u_ldKYHjrd%*owYWw@&6!r1+^b8Nb{PfzO#cP4TiGF|!7|WMY#Swt{QHL~-kWd8{ zOWdntZqBS;q^+YvkwMFZ8=_gH7uRkliuC~1Hi)i$sPznj0a$>KvIziidvPET@d*jf zR}*VcIB`6~SpxzMurgu?>Cn70{Guk};z6@IDd-WRL=cuUlAt5_Z)|$*`HrVD z!F2Wg_>6Eb$eZU5bs+1SHe@cNjRz3G;;!cf$c&E^XV#Lj!C*!KR>7zj3`UEJKzp%) z#nU?)`wcc6n&1zQJFnicz42W^pGYtcz?^`cpKcmEuJxjNuA zSFdjC*koUeBvY`Y0lXJmCue5(!CkDhYGr`O@#f8&p&?BRwe(<`!irl)aL1QbaY!)h z1GDAvp`jx1uw`XWKfcldyL$Gqsw#2SyTEM=FxI-54+EbBeErHblbn+BDJ3NO^fW}R z_6UTO^9PJiRe`>mNmmya2U=6$oaLfSFAo+)eDL4^WVJr)_>>f;yWGBFN81Ym8WWq7 z#}<@SRBPYA%Y__3q^L2{Bs-VyLXXQ{zvkuP`K~8N#S2I7ZNup|uTrhDh6NO<-hnA*ER8(sUBIfgNQWzZc8(qCFW z+AlXYHtaApWh0xPYmT?(R`GlmM=6YsRiY2UBn}KUZ1pn0tRF<~qQqeVCkiU6!|D8P z_9?G5R4Xb2u3%|^%WiaS*DsKDuGi_B$YGcE{kk!Y2U7`ty^Qj5L54qKr{cJ1jyf#1 zxAdY*s-DPC67i%jyq(z^$BfTV0A1i?0tpicAeezpke=!5dzyd* zli4GIUg`Uwa>+)6C(b0oT68;NZ&<)k{mDCuFW`@7zkDg>{@VxY+bwZ<;(?1_q%5?% z3Zt{Px8F12h+zfHR~ioMk%((wV-44a(*+Ps?(lmNaKQ79CCE6WlaD5c#W6TEcL6_~ zz+If}&orI*XWYTTfgOSt18V){@H3IiL+f&FWo1H-1dl?w(i4{IgsAbE_h79|A$E`o zNf{Y(0ei?s7Yq)FzwR66%IY$=w;1<_JD>n@)>2#SASoL=dw=i_z6i=+KFfNmx2pR( z2R&-{6=OY;K%Og?X=$%IEJVXVN@bwfcnc??a&7927>T&6y zZ?ugT?}V`v(Cj&fCX2pB*>Z+WQK0jq=|ce705qaj{oi=6r0C%OGqj%X{v(ya8UAll h`TtG%|Kj3`=Dfu?8LORJ0zL~Q&{Ee|t5&f?{};c3wIBch diff --git a/docs/unity/index.md b/docs/unity/index.md index 6c005e36..28096827 100644 --- a/docs/unity/index.md +++ b/docs/unity/index.md @@ -14,14 +14,10 @@ Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll b ## Unity Tutorial - Basic Multiplayer Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](/docs/modules/rust) or [C#](/docs/modules/c-sharp): +![Core Architecture](https://i.imgur.com/Me0JlsK.png) + 1. [Setup](/docs/unity/part-1.md) 2. [Server (C#)](/docs/unity/part-2.md) ☼ 3. [Client (Unity)](/docs/unity/part-3.md) ☼ While the tutorial uses C#, the repo cloned in [Part 1](/docs/unity/part-1.md) does include a legacy Rust example to optionally use, instead. - -## Unity Tutorial - Advanced -By this point, you should already have a basic understanding of SpacetimeDB client, server and CLI: - -- [Part 4 - Resources & Scheduling](/docs/unity/part-4.md) -- [Part 5 - BitCraft Mini](/docs/unity/part-5.md) diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 8eb3d397..dd38dfda 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -3,7 +3,7 @@ > [!IMPORTANT] > TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. -## Project Setup +## Quickstart Project Setup This progressive tutorial will guide you to: @@ -29,7 +29,9 @@ This project repo is separated into two sub-projects: ## 2. Publishing the Project -From Unity, you don't need CLI commands for common functionality: +From Unity, you don't need CLI commands for common functionality: There's a Unity editor tool for that! + +![Unity Publisher Editor Tool GIF](https://i.imgur.com/Hbup2W9.gif) 1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) 1. Create an identity -> Select `testnet` for the server @@ -37,8 +39,6 @@ From Unity, you don't need CLI commands for common functionality: 💡For the next section, we'll use the selected `Server` and publish result `Host` -![Unity Publisher Tool](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-publisher.jpg) - ## 3. Connecting the Project 1. Open `Scenes/Main` in Unity -> select the `GameManager` GameObject in the inspector. @@ -51,9 +51,7 @@ From Unity, you don't need CLI commands for common functionality: With the same `Main` scene open, press play! -![Gameplay Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-action.jpg) - -![UI Screenshot](https://github.com/clockworklabs/zeke-demo-project/raw/dylan/feat/mini-upgrade/.doc/prev-ui.jpg) +![Gameplay Actions<>UI GIF](https://i.imgur.com/e9uLx3a.gif) You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. @@ -61,4 +59,4 @@ You should see your local player as a box in the scene: Notice some hints at the Congratulations! You have successfully set up your multiplayer game project. -In the next section, we will break down how Server Modules work and analyze the demo code. \ No newline at end of file +In the next section, we will break down how Server Modules work and analyze the demo code. In a later section, we'll also analyze the Unity client demo. \ No newline at end of file diff --git a/docs/unity/part-2.md b/docs/unity/part-2.md index db061cd6..314ae47c 100644 --- a/docs/unity/part-2.md +++ b/docs/unity/part-2.md @@ -3,6 +3,11 @@ > [!IMPORTANT] > TODO: This draft may link to WIP repos, docs or temporarily-hosted images - be sure to replace with final links/images after prerequisite PRs are approved (that are not yet approved upon writing this) -> then delete this memo. +### Prerequisites + +- This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md): +- Or [start from the beginning](/docs/unity/index.md). + ## Analyzing the C# Server Module This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md). @@ -18,17 +23,14 @@ The server module will handle the game logic and data management for the game. 💡 Need help? [Join our Discord server](https://discord.gg/spacetimedb)! -### Prerequisites - -This progressive tutorial is continued from [Part 1](/docs/unity/part-1.md): -* You have already [setup your project](/docs/unity/index.md). - ## The Entity Component Systems (ECS) Before we continue to creating the server module, it's important to understand the basics of the ECS. This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). +![ECS Flow, Wikipedia Creative Commons CC0 1.0](https://i.imgur.com/NlJTevf.png) + We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. @@ -267,20 +269,15 @@ While this is very high-level, **this is what's happening:** - `[Reducer(ReducerKind.Disconnect)]` - `[Reducer(ReducerKind.Update)]` - Not to be confused with Unity-style Update loops, this calls when a `[Table]` row is updated. +## Publishing the Module -## Wrapping Up - -💡View the [entire lib.cs file](https://gist.github.com/dylanh724/68067b4e843ea6e99fbd297fe1a87c49) +To deploy outside of Unity, we'd normally use the `spacetime publish` CLI command. -Now that we added chat support, let's publish the latest module version to SpacetimeDB, assuming we're still in the `server` dir: - -```bash -spacetime publish -c unity-tutorial -``` +However, Unity has an integrated **Publisher** editor tool that will be introduced in the next section! ## Conclusion -You have now learned the core concepts of the C# server module, reviewed limitations and common practices -and broke down high-level concepts like Types, Tables, and Reducers with real examples from the demo. +You have now learned the core concepts of the C# server module, reviewed limitations/common practices +and broke down high-level concepts like Types, Tables, and Reducers - with real examples from the demo. -In the next section, we will break down the client-side code and analyze the Unity demo code. \ No newline at end of file +In the next section, we'll publish our module, break down the **client-side** Unity code and analyze client-server flows. \ No newline at end of file diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index 53a31ac4..da097358 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -5,15 +5,15 @@ ## Prerequisites -This progressive tutorial is continued from [Part 2](/docs/unity/part-2.md): -1. You have already [setup your project](/docs/unity/index.md). -1. You have already [published your server module](/docs/unity/part-2.md). +- This progressive tutorial is continued from [Part 2](/docs/unity/part-2.md). +- Or [start from the beginning](/docs/unity/index.md). ## Analyzing the Unity Client Demo In this part of the tutorial, we will: 1. Setup your `GameManager` connection properties. +1. Use the _Publisher_ editor tool to deploy your module. 1. Inspect high-level client initialization. 1. Press Play -> Guided breakdown of game features: 1. Chat @@ -234,10 +234,11 @@ Purchasing from the store will trigger the following flows: 1. `InventoryUpdate()` will clear `PlayerInventoryController.Local._pockets` and resync with the server's `newValue`. - **Config:** `onConfigComponentUpdate()` -> `PlayerInventoryController.Local.ConfigUpdate(newValue)` chain will be called. -## Troubleshooting - -TODO? - ## Conclusion -TODO? \ No newline at end of file +✅ You've successfully finished the SpacetimeDB client-server multiplayer tutorial series! **From here:** + +- Check the [C# Reference Doc](../modules/c-sharp/index.md) +- Explore the CLI tool by opening a terminal and typing `spacetime`, which expands upon the _Publisher_ and _Reducer_ Unity editor tools. + - 💡Try starting with `spacetime sql -h` to learn how to query your database [via SQL queries](../sql/index.md)! +- Ask questions in our [Discord server](https://discord.gg/spacetimedb) diff --git a/docs/unity/part-4.md b/docs/unity/part-4.md deleted file mode 100644 index a87f27a2..00000000 --- a/docs/unity/part-4.md +++ /dev/null @@ -1,261 +0,0 @@ -# Unity Tutorial - Advanced - Part 4 - Resources and Scheduling - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 3](/docs/unity/part-3.md) Tutorial. - -**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** - -In this second part of the lesson, we'll add resource nodes to our project and learn about scheduled reducers. Then we will spawn the nodes on the client so they are visible to the player. - -## Add Resource Node Spawner - -In this section we will add functionality to our server to spawn the resource nodes. - -### Step 1: Add the SpacetimeDB Tables for Resource Nodes - -1. Before we start adding code to the server, we need to add the ability to use the rand crate in our SpacetimeDB module so we can generate random numbers. Open the `Cargo.toml` file in the `Server` directory and add the following line to the `[dependencies]` section. - -```toml -rand = "0.8.5" -``` - -We also need to add the `getrandom` feature to our SpacetimeDB crate. Update the `spacetimedb` line to: - -```toml -spacetimedb = { "0.5", features = ["getrandom"] } -``` - -2. The first entity component we are adding, `ResourceNodeComponent`, stores the resource type. We'll define an enum to describe a `ResourceNodeComponent`'s type. For now, we'll just have one resource type: Iron. In the future, though, we'll add more resources by adding variants to the `ResourceNodeType` enum. Since we are using a custom enum, we need to mark it with the `SpacetimeType` attribute. Add the following code to lib.rs. - -```rust -#[derive(SpacetimeType, Clone)] -pub enum ResourceNodeType { - Iron, -} - -#[spacetimedb(table)] -#[derive(Clone)] -pub struct ResourceNodeComponent { - #[primarykey] - pub entity_id: u64, - - // Resource type of this resource node - pub resource_type: ResourceNodeType, -} -``` - -Because resource nodes never move, the `MobileEntityComponent` is overkill. Instead, we will add a new entity component named `StaticLocationComponent` that only stores the position and rotation. - -```rust -#[spacetimedb(table)] -#[derive(Clone)] -pub struct StaticLocationComponent { - #[primarykey] - pub entity_id: u64, - - pub location: StdbVector2, - pub rotation: f32, -} -``` - -3. We are also going to add a couple of additional column to our Config table. `map_extents` let's our spawner know where it can spawn the nodes. `num_resource_nodes` is the maximum number of nodes to spawn on the map. Update the config table in lib.rs. - -```rust -#[spacetimedb(table)] -pub struct Config { - // Config is a global table with a single row. This table will be used to - // store configuration or global variables - - #[primarykey] - // always 0 - // having a table with a primarykey field which is always zero is a way to store singleton global state - pub version: u32, - - pub message_of_the_day: String, - - // new variables for resource node spawner - // X and Z range of the map (-map_extents to map_extents) - pub map_extents: u32, - // maximum number of resource nodes to spawn on the map - pub num_resource_nodes: u32, -} -``` - -4. In the `init` reducer, we need to set the initial values of our two new variables. Update the following code: - -```rust - Config::insert(Config { - version: 0, - message_of_the_day: "Hello, World!".to_string(), - - // new variables for resource node spawner - map_extents: 25, - num_resource_nodes: 10, - }) - .expect("Failed to insert config."); -``` - -### Step 2: Write our Resource Spawner Repeating Reducer - -1. Add the following code to lib.rs. We are using a special attribute argument called repeat which will automatically schedule the reducer to run every 1000ms. - -```rust -#[spacetimedb(reducer, repeat = 1000ms)] -pub fn resource_spawner_agent(_ctx: ReducerContext, _prev_time: Timestamp) -> Result<(), String> { - let config = Config::filter_by_version(&0).unwrap(); - - // Retrieve the maximum number of nodes we want to spawn from the Config table - let num_resource_nodes = config.num_resource_nodes as usize; - - // Count the number of nodes currently spawned and exit if we have reached num_resource_nodes - let num_resource_nodes_spawned = ResourceNodeComponent::iter().count(); - if num_resource_nodes_spawned >= num_resource_nodes { - log::info!("All resource nodes spawned. Skipping."); - return Ok(()); - } - - // Pick a random X and Z based off the map_extents - let mut rng = rand::thread_rng(); - let map_extents = config.map_extents as f32; - let location = StdbVector2 { - x: rng.gen_range(-map_extents..map_extents), - z: rng.gen_range(-map_extents..map_extents), - }; - // Pick a random Y rotation in degrees - let rotation = rng.gen_range(0.0..360.0); - - // Insert our SpawnableEntityComponent which assigns us our entity_id - let entity_id = SpawnableEntityComponent::insert(SpawnableEntityComponent { entity_id: 0 }) - .expect("Failed to create resource spawnable entity component.") - .entity_id; - - // Insert our static location with the random position and rotation we selected - StaticLocationComponent::insert(StaticLocationComponent { - entity_id, - location: location.clone(), - rotation, - }) - .expect("Failed to insert resource static location component."); - - // Insert our resource node component, so far we only have iron - ResourceNodeComponent::insert(ResourceNodeComponent { - entity_id, - resource_type: ResourceNodeType::Iron, - }) - .expect("Failed to insert resource node component."); - - // Log that we spawned a node with the entity_id and location - log::info!( - "Resource node spawned: {} at ({}, {})", - entity_id, - location.x, - location.z, - ); - - Ok(()) -} -``` - -2. Since this reducer uses `rand::Rng` we need add include it. Add this `use` statement to the top of lib.rs. - -```rust -use rand::Rng; -``` - -3. Even though our reducer is set to repeat, we still need to schedule it the first time. Add the following code to the end of the `init` reducer. You can use this `schedule!` macro to schedule any reducer to run in the future after a certain amount of time. - -```rust - // Start our resource spawner repeating reducer - spacetimedb::schedule!("1000ms", resource_spawner_agent(_, Timestamp::now())); -``` - -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: - -```bash -spacetime generate --out-dir ../Assets/autogen --lang=csharp - -spacetime publish -c yourname/bitcraftmini -``` - -Your resource node spawner will start as soon as you publish since we scheduled it to run in our init reducer. You can watch the log output by using the `--follow` flag on the logs CLI command. - -```bash -spacetime logs -f yourname/bitcraftmini -``` - -### Step 3: Spawn the Resource Nodes on the Client - -1. First we need to update the `GameResource` component in Unity to work for multiplayer. Open GameResource.cs and add `using SpacetimeDB.Types;` to the top of the file. Then change the variable `Type` to be of type `ResourceNodeType` instead of `int`. Also add a new variable called `EntityId` of type `ulong`. - -```csharp - public ulong EntityId; - - public ResourceNodeType Type = ResourceNodeType.Iron; -``` - -2. Now that we've changed the `Type` variable, we need to update the code in the `PlayerAnimator` component that references it. Open PlayerAnimator.cs and update the following section of code. We need to add `using SpacetimeDB.Types;` to this file as well. This fixes the compile errors that result from changing the type of the `Type` variable to our new server generated enum. - -```csharp - var resourceType = res?.Type ?? ResourceNodeType.Iron; - switch (resourceType) - { - case ResourceNodeType.Iron: - _animator.SetTrigger("Mine"); - Interacting = true; - break; - default: - Interacting = false; - break; - } - for (int i = 0; i < _tools.Length; i++) - { - _tools[i].SetActive(((int)resourceType) == i); - } - _target = res; -``` - -3. Now that our `GameResource` is ready to be spawned, lets update the `BitcraftMiniGameManager` component to actually create them. First, we need to add the new tables to our SpacetimeDB subscription. Open BitcraftMiniGameManager.cs and update the following code: - -```csharp - SpacetimeDBClient.instance.Subscribe(new List() - { - "SELECT * FROM Config", - "SELECT * FROM SpawnableEntityComponent", - "SELECT * FROM PlayerComponent", - "SELECT * FROM MobileEntityComponent", - // Our new tables for part 2 of the tutorial - "SELECT * FROM ResourceNodeComponent", - "SELECT * FROM StaticLocationComponent" - }); -``` - -4. Next let's add an `OnInsert` handler for the `ResourceNodeComponent`. Add the following line to the `Start` function. - -```csharp - ResourceNodeComponent.OnInsert += ResourceNodeComponent_OnInsert; -``` - -5. Finally we add the new function to handle the insert event. This function will be called whenever a new `ResourceNodeComponent` is inserted into our local client cache. We can use this to spawn the resource node in the world. Add the following code to the `BitcraftMiniGameManager` class. - -To get the position and the rotation of the node, we look up the `StaticLocationComponent` for this entity by using the EntityId. - -```csharp - private void ResourceNodeComponent_OnInsert(ResourceNodeComponent insertedValue, ReducerEvent callInfo) - { - switch(insertedValue.ResourceType) - { - case ResourceNodeType.Iron: - var iron = Instantiate(IronPrefab); - StaticLocationComponent loc = StaticLocationComponent.FilterByEntityId(insertedValue.EntityId); - Vector3 nodePos = new Vector3(loc.Location.X, 0.0f, loc.Location.Z); - iron.transform.position = new Vector3(nodePos.x, MathUtil.GetTerrainHeight(nodePos), nodePos.z); - iron.transform.rotation = Quaternion.Euler(0.0f, loc.Rotation, 0.0f); - break; - } - } -``` - -### Step 4: Play the Game! - -6. Hit Play in the Unity Editor and you should now see your resource nodes spawning in the world! diff --git a/docs/unity/part-5.md b/docs/unity/part-5.md deleted file mode 100644 index 6ebce1c0..00000000 --- a/docs/unity/part-5.md +++ /dev/null @@ -1,108 +0,0 @@ -# Unity Tutorial - Advanced - Part 5 - BitCraft Mini - -Need help with the tutorial? [Join our Discord server](https://discord.gg/spacetimedb)! - -This progressive tutorial is continued from the [Part 4](/docs/unity/part-3.md) Tutorial. - -**Oct 14th, 2023: This tutorial has not yet been updated for the recent 0.7.0 release, it will be updated asap!** - -BitCraft Mini is a game that we developed which extends the code you've already developed in this tutorial. It is inspired by our game [BitCraft](https://bitcraftonline.com) and illustrates how you could build a more complex game from just the components we've discussed. Right now you can walk around, mine ore, and manage your inventory. - -## 1. Download - -You can git-clone BitCraftMini from here: - -```plaintext -git clone ssh://git@github.com/clockworklabs/BitCraftMini -``` - -Once you have downloaded BitCraftMini, you will need to compile the spacetime module. - -## 2. Compile the Spacetime Module - -In order to compile the BitCraftMini module, you will need to install cargo. You can install cargo from here: - -> https://www.rust-lang.org/tools/install - -Once you have cargo installed, you can compile and publish the module with these commands: - -```bash -cd BitCraftMini/Server -spacetime publish -``` - -`spacetime publish` will output an address where your module has been deployed to. You will want to copy/save this address because you will need it in step 3. Here is an example of what it should look like: - -```plaintext -$ spacetime publish -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - Finished release [optimized] target(s) in 0.03s -Publish finished successfully. -Created new database with address: c91c17ecdcea8a05302be2bad9dd59b3 -``` - -Optionally, you can specify a name when you publish the module: - -```bash -spacetime publish "unique-module-name" -``` - -Currently, all the named modules exist in the same namespace so if you get a message saying that database is not owned by you, it means that someone else has already published a module with that name. You can either choose a different name or you can use the address instead. If you specify a name when you publish, you can use that name in place of the autogenerated address in both the CLI and in the Unity client. - -In the BitCraftMini module we have a function called `initialize()`. This function should be called immediately after publishing the module to spacetimedb. This function is in charge of generating some initial settings that are required for the server to operate. You can call this function like so: - -```bash -spacetime call "" "initialize" "[]" -``` - -Here we are telling spacetime to invoke the `initialize()` function on our module "bitcraftmini". If the function had some arguments, we would json encode them and put them into the "[]". Since `initialize()` requires no parameters, we just leave it empty. - -After you have called `initialize()` on the spacetime module you shouldgenerate the client files: - -```bash -spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs -``` - -Here is some sample output: - -```plaintext -$ spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang cs -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - Finished release [optimized] target(s) in 0.03s -compilation took 234.613518ms -Generate finished successfully. -``` - -If you've gotten this message then everything should be working properly so far. - -## 3. Replace address in BitCraftMiniGameManager - -The following settings are exposed in the `BitCraftMiniGameManager` inspector: Module Address, Host Name, and SSL Enabled. - -Open the Main scene in Unity and click on the `GameManager` object in the heirarchy. The inspector window will look like this: - -![GameManager-Inspector](/images/unity-tutorial/GameManager-Inspector.JPG) - -Update the module address with the address you got from the `spacetime publish` command. If you are using SpacetimeDB Cloud `testnet`, the host name should be `testnet.spacetimedb.com` and SSL Enabled should be checked. If you are running SpacetimeDB Standalone locally, the host name should be `localhost:3000` and SSL Enabled should be unchecked. For instructions on how to deploy to these environments, see the [Deployment Section](/docs/deploying/testnet) - -## 4. Play Mode - -You should now be able to enter play mode and walk around! You can mine some rocks, cut down some trees and if you connect more clients you can trade with other players. - -## 5. Editing the Module - -If you want to make further updates to the module, make sure to use this publish command instead: - -```bash -spacetime publish -``` - -Where `` is your own address. If you do this instead then you won't have to change the address inside of `BitCraftMiniGameManager.cs` - -When you change the server module you should also regenerate the client files as well: - -```bash -spacetime generate --out-dir ../Client/Assets/_Project/autogen --lang=cs -``` - -You may want to consider putting these 2 commands into a simple shell script to make the process a bit cleaner. diff --git a/nav.ts b/nav.ts index c613e090..8d3367fb 100644 --- a/nav.ts +++ b/nav.ts @@ -36,11 +36,10 @@ const nav: Nav = { page("Self-Hosted", "deploying/hosted", "deploying/self-hosted.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-2", "unity/part-2.md"), - page("3 - Client", "unity/part-3", "unity/part-3.md"), + page("Overview", "unity-tutorial", "unity/index.md"), + page("1 - Project Setup", "unity/part-1", "unity/part-1.md"), + page("2 - Server (C# Module)", "unity/part-2", "unity/part-2a-rust.md"), + page("3 - Client (Unity)", "unity/part-2", "unity/part-2.md"), section("Unity Tutorial - Advanced"), page("4 - Resources & Scheduling", "unity/part-4", "unity/part-4.md"), From fc5b554c75a1a2d22518a231ef4836bc35e99728 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Fri, 5 Apr 2024 20:05:47 +0800 Subject: [PATCH 21/24] chore!: Replaced imgur links with spacetime-web - (!) Be sure to approve the other spacetime-web PR (edited to OP) for images to show up: https://github.com/clockworklabs/spacetime-web/pull/392 --- docs/unity/index.md | 2 +- docs/unity/part-1.md | 4 ++-- docs/unity/part-2.md | 2 +- docs/unity/part-3.md | 20 ++++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/unity/index.md b/docs/unity/index.md index 28096827..6addcf11 100644 --- a/docs/unity/index.md +++ b/docs/unity/index.md @@ -14,7 +14,7 @@ Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll b ## Unity Tutorial - Basic Multiplayer Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](/docs/modules/rust) or [C#](/docs/modules/c-sharp): -![Core Architecture](https://i.imgur.com/Me0JlsK.png) +![Core Architecture](/images/unity-tutorial/overview/core-architecture.png) 1. [Setup](/docs/unity/part-1.md) 2. [Server (C#)](/docs/unity/part-2.md) ☼ diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index dd38dfda..ca1dd963 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -31,7 +31,7 @@ This project repo is separated into two sub-projects: From Unity, you don't need CLI commands for common functionality: There's a Unity editor tool for that! -![Unity Publisher Editor Tool GIF](https://i.imgur.com/Hbup2W9.gif) +![Unity Publisher Editor Tool GIF](/images/unity-tutorial/part-1/unity-publisher-editor-tool-animated.gif) 1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) 1. Create an identity -> Select `testnet` for the server @@ -51,7 +51,7 @@ From Unity, you don't need CLI commands for common functionality: There's a Unit With the same `Main` scene open, press play! -![Gameplay Actions<>UI GIF](https://i.imgur.com/e9uLx3a.gif) +![Gameplay Actions<>UI GIF](/images/unity-tutorial/part-3/action-ui-animation.gif) You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. diff --git a/docs/unity/part-2.md b/docs/unity/part-2.md index 314ae47c..23961da2 100644 --- a/docs/unity/part-2.md +++ b/docs/unity/part-2.md @@ -29,7 +29,7 @@ Before we continue to creating the server module, it's important to understand t This is a game development architecture that separates game objects into components for better flexibility and performance. You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). -![ECS Flow, Wikipedia Creative Commons CC0 1.0](https://i.imgur.com/NlJTevf.png) +![ECS Flow, Wikipedia Creative Commons CC0 1.0](/images/unity-tutorial/part-2/ecs-flow-wiki-creative-commons-cc0-v1.min.png) We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index da097358..757c1d00 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -26,7 +26,7 @@ Start by opening `Scenes/Main` in the Unity project from the repo `/Client` dir. ## GameManager Connection Setup -![GameManager Inspector (+host name variations)](https://i.imgur.com/sHxYyS7.png) +![GameManager Inspector (+host name variations)](/images/unity-tutorial/part-3/) Select the `GameManager` in the scene hierarchy: @@ -111,7 +111,7 @@ private void connectToSpacetimeDb() ``` ## Play the Demo Game -![Gameplay Actions<>UI GIF](https://i.imgur.com/e9uLx3a.gif) +![Gameplay Actions<>UI GIF](/images/unity-tutorial/part-3/action-ui-animation.gif) Notice at the bottom-right, you have some tips: @@ -128,7 +128,7 @@ ___ ### Feature: Chat -![Chat<>Reducer Tool](https://i.imgur.com/Gm6YN1S.png) +![Chat<>Reducer Tool](/images/unity-tutorial/part-3/chat-reducer-tool.min.png) Note the message of the day, directing you to emulate a third-party with the _Reducers_ editor tool: @@ -138,7 +138,7 @@ Note the message of the day, directing you to emulate a third-party with the _Re 1. CreatePlayer -![Create Player via Reducer Tool](https://i.imgur.com/yl5WBXt.png) +![Create Player via Reducer Tool](/images/unity-tutorial/part-3/create-player-via-reducer-tool.min.png) 2. Repeat with `SendChatMessage` to see it appear in chat from your created "third-party" player. @@ -146,7 +146,7 @@ Note the message of the day, directing you to emulate a third-party with the _Re ### Feature: Resource Gathering -![Resource Gathering](https://i.imgur.com/McdvbHZ.png) +![Resource Gathering](/images/unity-tutorial/part-3/resource-gathering.min.png) Thanks to our scheduler set by the server via the `Init()`, resources will spawn every 5~10 seconds (with the specified max cap): @@ -156,7 +156,7 @@ Thanks to our scheduler set by the server via the `Init()`, resources will spawn Extracting a resource will trigger the following flows: -![initSubscribeToEvents-Resource-Extraction-Events](https://i.imgur.com/xqJQ3Xu.png) +![initSubscribeToEvents-Resource-Extraction-Events](/images/unity-tutorial/part-3/initSubscribeToEvents-resource-extraction.min.png) 1. **[Client]** Call `Reducer.Extract()` from `PlayerInputReceiver.cs` via `OnActionButton()`. @@ -180,7 +180,7 @@ Extracting a resource will trigger the following flows: ### Feature: Inventory -![Player Inventory](https://i.imgur.com/sBkgW48.png) +![Player Inventory](/images/unity-tutorial/part-3/player-inventory.min.png) - On server [Player.cs::CreatePlayer()](./part-2.md#db-initialization), this chained to `createPlayerInventory()`, creating the default inventory `Pockets` and an empty set of `UnlockIds`. - When the `Pocket` was created, we started the new Player with 5x `Iron`. @@ -192,15 +192,15 @@ See the [Store](#feature-store) section below for example items being consumed a ### Feature: Unlockables -![Unlockables](https://i.imgur.com/ShDOq4t.png) +![Unlockables](/images/unity-tutorial/part-3/unlockables.min.png) See the [Store](#feature-store) section below for an example unlock store purchase. 💡 Dive into `UIUnlocks.cs` to best further discover how client-side unlocks work. -### Feature: Store +### Feature: Shop -![Store Purchase (+Logs)](https://i.imgur.com/tZmR0uE.gif) +![Store Purchase (+Logs)](/images/unity-tutorial/part-3/shop-purchase-wLogsanimated.gif) Open **ShopRow.cs** to find the `Purchase()` function: From 61e1dd242c593b91882e0cf4f9a1d9c98ce7faad Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 11 Apr 2024 15:22:49 +0800 Subject: [PATCH 22/24] doc: Restore hidden images: Uncomment to see - Since the static images are hosted on another repo, it's hard to preview --- docs/unity/index.md | 1 + docs/unity/part-1.md | 3 ++- docs/unity/part-2.md | 1 + docs/unity/part-3.md | 9 +++++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/unity/index.md b/docs/unity/index.md index 6addcf11..3ae82550 100644 --- a/docs/unity/index.md +++ b/docs/unity/index.md @@ -15,6 +15,7 @@ Tested with Unity `2022.3.20 LTS` (and may also work on newer versions). We'll b Get started with the core client-server setup. For part 2, you may choose your server module preference of [Rust](/docs/modules/rust) or [C#](/docs/modules/c-sharp): ![Core Architecture](/images/unity-tutorial/overview/core-architecture.png) + 1. [Setup](/docs/unity/part-1.md) 2. [Server (C#)](/docs/unity/part-2.md) ☼ diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index ca1dd963..fc95cdcf 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -32,6 +32,7 @@ This project repo is separated into two sub-projects: From Unity, you don't need CLI commands for common functionality: There's a Unity editor tool for that! ![Unity Publisher Editor Tool GIF](/images/unity-tutorial/part-1/unity-publisher-editor-tool-animated.gif) + 1. Open the _Publisher_ editor tool: `ALT+SHIFT+P` (or `Window/SpacetimeDB/Publisher` in the top menu) 1. Create an identity -> Select `testnet` for the server @@ -51,7 +52,7 @@ From Unity, you don't need CLI commands for common functionality: There's a Unit With the same `Main` scene open, press play! -![Gameplay Actions<>UI GIF](/images/unity-tutorial/part-3/action-ui-animation.gif) + You should see your local player as a box in the scene: Notice some hints at the bottom-right for things to do. diff --git a/docs/unity/part-2.md b/docs/unity/part-2.md index 23961da2..0a9e2350 100644 --- a/docs/unity/part-2.md +++ b/docs/unity/part-2.md @@ -30,6 +30,7 @@ This is a game development architecture that separates game objects into compone You can read more about the ECS design pattern [here](https://en.wikipedia.org/wiki/Entity_component_system). ![ECS Flow, Wikipedia Creative Commons CC0 1.0](/images/unity-tutorial/part-2/ecs-flow-wiki-creative-commons-cc0-v1.min.png) + We chose ECS for this example project because it promotes scalability, modularity, and efficient data management, making it ideal for building multiplayer games with SpacetimeDB. diff --git a/docs/unity/part-3.md b/docs/unity/part-3.md index 757c1d00..66918cac 100644 --- a/docs/unity/part-3.md +++ b/docs/unity/part-3.md @@ -27,6 +27,7 @@ Start by opening `Scenes/Main` in the Unity project from the repo `/Client` dir. ## GameManager Connection Setup ![GameManager Inspector (+host name variations)](/images/unity-tutorial/part-3/) + Select the `GameManager` in the scene hierarchy: @@ -112,6 +113,7 @@ private void connectToSpacetimeDb() ## Play the Demo Game ![Gameplay Actions<>UI GIF](/images/unity-tutorial/part-3/action-ui-animation.gif) + Notice at the bottom-right, you have some tips: @@ -129,6 +131,7 @@ ___ ### Feature: Chat ![Chat<>Reducer Tool](/images/unity-tutorial/part-3/chat-reducer-tool.min.png) + Note the message of the day, directing you to emulate a third-party with the _Reducers_ editor tool: @@ -139,6 +142,7 @@ Note the message of the day, directing you to emulate a third-party with the _Re 1. CreatePlayer ![Create Player via Reducer Tool](/images/unity-tutorial/part-3/create-player-via-reducer-tool.min.png) + 2. Repeat with `SendChatMessage` to see it appear in chat from your created "third-party" player. @@ -147,6 +151,7 @@ Note the message of the day, directing you to emulate a third-party with the _Re ### Feature: Resource Gathering ![Resource Gathering](/images/unity-tutorial/part-3/resource-gathering.min.png) + Thanks to our scheduler set by the server via the `Init()`, resources will spawn every 5~10 seconds (with the specified max cap): @@ -157,6 +162,7 @@ Thanks to our scheduler set by the server via the `Init()`, resources will spawn Extracting a resource will trigger the following flows: ![initSubscribeToEvents-Resource-Extraction-Events](/images/unity-tutorial/part-3/initSubscribeToEvents-resource-extraction.min.png) + 1. **[Client]** Call `Reducer.Extract()` from `PlayerInputReceiver.cs` via `OnActionButton()`. @@ -181,6 +187,7 @@ Extracting a resource will trigger the following flows: ### Feature: Inventory ![Player Inventory](/images/unity-tutorial/part-3/player-inventory.min.png) + - On server [Player.cs::CreatePlayer()](./part-2.md#db-initialization), this chained to `createPlayerInventory()`, creating the default inventory `Pockets` and an empty set of `UnlockIds`. - When the `Pocket` was created, we started the new Player with 5x `Iron`. @@ -193,6 +200,7 @@ See the [Store](#feature-store) section below for example items being consumed a ### Feature: Unlockables ![Unlockables](/images/unity-tutorial/part-3/unlockables.min.png) + See the [Store](#feature-store) section below for an example unlock store purchase. @@ -201,6 +209,7 @@ See the [Store](#feature-store) section below for an example unlock store purcha ### Feature: Shop ![Store Purchase (+Logs)](/images/unity-tutorial/part-3/shop-purchase-wLogsanimated.gif) + Open **ShopRow.cs** to find the `Purchase()` function: From 844f9a5197712b48dc7857ba20e5cdcca08fd7a9 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 25 Apr 2024 16:40:48 +0800 Subject: [PATCH 23/24] doc: Nuances + Limitations CC to modules; +use long --- docs/modules/c-sharp/index.md | 30 ++++++++++++++++++++++++++++++ docs/unity/part-2.md | 5 +++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/modules/c-sharp/index.md b/docs/modules/c-sharp/index.md index 31ebd1d4..fc641402 100644 --- a/docs/modules/c-sharp/index.md +++ b/docs/modules/c-sharp/index.md @@ -4,6 +4,36 @@ You can use the [C# SpacetimeDB library](https://github.com/clockworklabs/Spacet It uses [Roslyn incremental generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) to add extra static methods to types, tables and reducers marked with special attributes and registers them with the database runtime. +## C# Module Limitations & Nuances + +Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), it's important to be aware of the following: + +1. No DateTime-like types in Types or Tables: + - Use `long` for microsecond unix epoch timestamps + - See example usage and converts at the [_TimeConvert_ module demo class](https://github.com/clockworklabs/zeke-demo-project/blob/3fa1c94e75819a191bd785faa7a7d15ea4dc260c/Server-Csharp/src/Utils.cs#L19) + + +2. No Timers or async/await, such as those to create repeating loops: + - For repeating invokers, instead **re**schedule it from within a fired [Scheduler](https://spacetimedb.com/docs/modules/c-sharp#reducers) function. + + +3. Using `Debug` advanced option in the `Publisher` Unity editor tool will add callstack symbols for easier debugging: + - However, avoid using `Debug` mode when publishing outside a `localhost` server: + - Due to WASM buffer size limitations, this may cause publish failure. + + +4. If you `throw` a new `Exception`, no error logs will appear. Instead, use either: + 1. Use `Log(message, LogLevel.Error);` before you throw. + 2. Use the demo's static [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs) class to `Utils.Throw()` to wrap the error log before throwing. + + +5. `[AutoIncrement]` or `[PrimaryKeyAuto]` will never equal 0: + - Inserting a new row with an Auto key equaling 0 will always return a unique, non-0 value. + + +6. Enums cannot declare values out of the default order: + - For example, `{ Foo = 0, Bar = 3 }` will fail to compile. + ## Example Let's start with a heavily commented version of the default example from the landing page: diff --git a/docs/unity/part-2.md b/docs/unity/part-2.md index 0a9e2350..bbbaca0c 100644 --- a/docs/unity/part-2.md +++ b/docs/unity/part-2.md @@ -37,10 +37,11 @@ making it ideal for building multiplayer games with SpacetimeDB. ## C# Module Limitations & Nuances -Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), you may run into unexpected issues until aware of the following: +Since SpacetimeDB runs on [WebAssembly (WASM)](https://webassembly.org/), it's important to be aware of the following: 1. No DateTime-like types in Types or Tables: - - Use `string` for timestamps (exampled at [Utils.cs](https://github.com/clockworklabs/zeke-demo-project/tree/dylan/feat/mini-upgrade/Server-Csharp/src/Utils.cs)), or `long` for Unix Epoch time. + - Use `long` for microsecond unix epoch timestamps + - See example usage and converts at the [_TimeConvert_ module demo class](https://github.com/clockworklabs/zeke-demo-project/blob/3fa1c94e75819a191bd785faa7a7d15ea4dc260c/Server-Csharp/src/Utils.cs#L19) 2. No Timers or async/await, such as those to create repeating loops: From d5ff6131ce7283caf49ef16341d6cf349407eae8 Mon Sep 17 00:00:00 2001 From: Dylan Hunt Date: Thu, 25 Apr 2024 16:52:15 +0800 Subject: [PATCH 24/24] refactor: Timestamps use long + TimeConvert helper --- docs/unity/part-2.md | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/unity/part-2.md b/docs/unity/part-2.md index bbbaca0c..b51d7f4f 100644 --- a/docs/unity/part-2.md +++ b/docs/unity/part-2.md @@ -143,12 +143,22 @@ public partial class ChatMessage /// Message contents public string? ChatText; + + /// Microseconds Timestamp of when the message was sent + /// Convert between long and DateTimeOffset via Utils.TimeConvert + public long Timestamp; + + /// Microseconds DateTimeOffset representing Timestamp microseconds of when the action ended + /// Convert between long and DateTimeOffset via Utils.TimeConvert + public DateTimeOffset TimestampOffset => + TimeConvert.FromMicrosecondsTimestamp(Timestamp); } ``` - The `Id` vars are `ulong` types, commonly used for SpacetimeDB unique identifiers -- Notice how `Timestamp` is a `string` instead of DateTimeOffset (a limitation mentioned earlier). - - 💡 We'll demonstrate how to set a timestamp correctly in the next section. +- Notice how `Timestamp` field is a `long` type instead of `DateTimeOffset` (a [limitation](#limitations) mentioned earlier): + - Take note the `TimestampOffset` helper, currying the function to a `TimeConvert` utils class. + - This ensures we preserve the time to the microsecond via unix epoch timestamps. ```csharp /// This component will be created for all world objects that can move smoothly throughout the world, keeping track @@ -166,17 +176,19 @@ public partial class MobileEntityComponent /// Movement direction, {0,0} if not moving at all. public StdbVector2? Direction; - /// Timestamp when movement started. - /// This is a ISO 8601 format string; see Utils.GetTimestamp() - public string? MoveStartTimestamp; + /// Microseconds Timestamp of when the movement started + /// Convert between long and DateTimeOffset via Utils.TimeConvert + public long MoveStartTimestamp; + + /// Microseconds DateTimeOffset representing Timestamp microseconds of when the action ended + /// Convert between long and DateTimeOffset via Utils.TimeConvert + public DateTimeOffset MoveStartTimestampOffset => + TimeConvert.FromMicrosecondsTimestamp(MoveStartTimestamp); } ``` - `EntityId` is the unique identifier for the table, declared as a `ulong` - Location and Direction are both `StdbVector2` types discussed above -- `MoveStartTimestamp` is a stringified timestamp, as you cannot use `DateTime`-like types within Tables - - One of the [limitations](#limitations) mentioned earlier - ## Reducers @@ -210,7 +222,7 @@ public static void SendChatMessage(DbEventArgs dbEvent, string text) ChatEntityId = 0, // This column auto-increments, so we can set it to 0 SourceEntityId = player.EntityId, ChatText = text, - Timestamp = Utils.Timestamp(dbEvent.Time), // ISO 8601 format) + Timestamp = Utils.TimeConvert.ToMicrosecondsTimestamp(dbEvent.Time), }.Insert(); } ``` @@ -220,7 +232,7 @@ public static void SendChatMessage(DbEventArgs dbEvent, string text) - Contains the sender's `Identity`, `DateTimeOffset` sent, and a semi-anonymous `Address` to compare sender vs others. - The `PlayerComponent` was found by passing the sender's `Identity` to `PlayerComponent.FindByOwnerId()`. - `Throw()` is the helper function (workaround for one of the [limitations](#limitations) mentioned earlier) that logs an error before throwing. -- This timestamp utilized `Utils.Timestamp()`; an easier-to-remember alias than `dbEvent.Time.ToUniversalTime().ToString("o");`. +- This timestamp utilized `Utils.TimeConvert.ToMicrosecondsTimestamp()` for consistent time parsing. - Since `ChatEntityId` is tagged with the `[Column(ColumnAttrs.PrimaryKeyAuto)]` attribute, setting it to 0 will auto-increment. ### DB Initialization