From d8bf5beb8d286c89805205c8e914293fe7130f4f Mon Sep 17 00:00:00 2001 From: rekhoff Date: Tue, 28 Jan 2025 09:23:03 -0800 Subject: [PATCH 01/17] Initial code pass on updating server to 1.0.0 --- docs/modules/c-sharp/quickstart.md | 72 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index 571351c1..d940d493 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -60,9 +60,7 @@ spacetime init --lang csharp 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; +using SpacetimeDB; ``` - `SpacetimeDB.Module` contains the special attributes we'll use to define tables and reducers in our module. @@ -85,10 +83,10 @@ For each `User`, we'll store their `Identity`, an optional name they can set to In `server/Lib.cs`, add the definition of the table `User` to the `Module` class: ```csharp -[SpacetimeDB.Table(Public = true)] +[Table(Name = "User", Public = true)] public partial class User { - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + [PrimaryKey] public Identity Identity; public string? Name; public bool Online; @@ -100,7 +98,7 @@ For each `Message`, we'll store the `Identity` of the user who sent it, the `Tim In `server/Lib.cs`, add the definition of the table `Message` to the `Module` class: ```csharp -[SpacetimeDB.Table(Public = true)] +[Table(Name = "Message", Public = true)] public partial class Message { public Identity Sender; @@ -125,11 +123,11 @@ public static void SetName(ReducerContext ctx, string name) { name = ValidateName(name); - var user = User.FindByIdentity(ctx.Sender); + var user = ctx.Db.User.Identity.Find(ctx.CallerIdentity); if (user is not null) { user.Name = name; - User.UpdateByIdentity(ctx.Sender, user); + ctx.Db.User.Identity.Update(user); } } ``` @@ -146,7 +144,7 @@ In `server/Lib.cs`, add to the `Module` class: ```csharp /// Takes a name and checks if it's acceptable as a user's name. -public static string ValidateName(string name) +private static string ValidateName(string name) { if (string.IsNullOrEmpty(name)) { @@ -167,13 +165,15 @@ In `server/Lib.cs`, add to the `Module` class: public static void SendMessage(ReducerContext ctx, string text) { text = ValidateMessage(text); - Log(text); - new Message - { - Sender = ctx.Sender, - Text = text, - Sent = ctx.Time.ToUnixTimeMilliseconds(), - }.Insert(); + Log.Info(text); + ctx.Db.Message.Insert( + new Message + { + Sender = ctx.CallerIdentity, + Text = text, + Sent = ctx.Timestamp.ToUnixTimeMilliseconds(), + } + ); } ``` @@ -183,7 +183,7 @@ In `server/Lib.cs`, add to the `Module` class: ```csharp /// Takes a message's text and checks if it's acceptable to send. -public static string ValidateMessage(string text) +private static string ValidateMessage(string text) { if (string.IsNullOrEmpty(text)) { @@ -202,58 +202,60 @@ You could extend the validation in `ValidateMessage` in similar ways to `Validat In C# modules, you can register for `Connect` and `Disconnect` events by using a special `ReducerKind`. We'll use the `Connect` event to create a `User` record for the client if it doesn't yet exist, and to set its online status. -We'll use `User.FindByIdentity` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. +We'll use `reducerContext.Db.User.Identity.Find` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `reducerContext.Db.User.Identity.Update` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `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(ReducerContext ReducerContext) +[SpacetimeDB.Reducer(ReducerKind.ClientConnected)] +public static void ClientConnected(ReducerContext reducerContext) { - Log($"Connect {ReducerContext.Sender}"); - var user = User.FindByIdentity(ReducerContext.Sender); + Log.Info($"Connect {reducerContext.CallerIdentity}"); + var user = reducerContext.Db.User.Identity.Find(reducerContext.CallerIdentity); if (user is not null) { // If this is a returning user, i.e., we already have a `User` with this `Identity`, // set `Online: true`, but leave `Name` and `Identity` unchanged. user.Online = true; - User.UpdateByIdentity(ReducerContext.Sender, user); + reducerContext.Db.User.Identity.Update(user); } else { // If this is a new user, create a `User` object for the `Identity`, // which is online, but hasn't set a name. - new User - { - Name = null, - Identity = ReducerContext.Sender, - Online = true, - }.Insert(); + reducerContext.Db.User.Insert( + new User + { + Name = null, + Identity = reducerContext.CallerIdentity, + Online = true, + } + ); } } ``` -Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.Disconnect`. We'll use it to un-set the `Online` status of the `User` for the disconnected client. +Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.ClientDisconnected`. We'll use it to un-set the `Online` status of the `User` for the disconnected client. Add the following code after the `OnConnect` handler: ```csharp -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void OnDisconnect(ReducerContext ReducerContext) +[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)] +public static void ClientDisconnected(ReducerContext reducerContext) { - var user = User.FindByIdentity(ReducerContext.Sender); + var user = reducerContext.Db.User.Identity.Find(reducerContext.CallerIdentity); if (user is not null) { // This user should exist, so set `Online: false`. user.Online = false; - User.UpdateByIdentity(ReducerContext.Sender, user); + reducerContext.Db.User.Identity.Update(user); } else { // User does not exist, log warning - Log("Warning: No user found for disconnected client."); + Log.Warn("Warning: No user found for disconnected client."); } } ``` From 3bab3a6d04eef3a2d599e94a548b26a4aef75214 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Tue, 18 Feb 2025 16:11:38 -0800 Subject: [PATCH 02/17] Updated to work with current 1.0.0-rc4, master branches of SpacetimeDB and the CSharpSDK --- docs/modules/c-sharp/quickstart.md | 45 ++--- docs/sdks/c-sharp/quickstart.md | 268 +++++++++++++++++------------ 2 files changed, 178 insertions(+), 135 deletions(-) diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index d940d493..252ed640 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -63,13 +63,12 @@ To the top of `server/Lib.cs`, add some imports we'll be using: using SpacetimeDB; ``` -- `SpacetimeDB.Module` contains the special attributes we'll use to define tables and reducers in our module. -- `SpacetimeDB.Runtime` contains the raw API bindings SpacetimeDB uses to communicate with the database. +- `SpacetimeDB` contains the special attributes we'll use to define tables and reducers in our module and the raw API bindings SpacetimeDB uses to communicate with the database. We also need to create our static module class which all of the module code will live in. In `server/Lib.cs`, add: ```csharp -static partial class Module +public static partial class Module { } ``` @@ -111,19 +110,19 @@ public partial class Message We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `SetName` which clients can invoke to set their `User.Name`. It will validate the caller's chosen name, using a function `ValidateName` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail. -Each reducer may accept as its first argument a `ReducerContext`, which includes the `Identity` and `Address` of the client that called the reducer, and the `Timestamp` when it was invoked. For now, we only need the `Identity`, `ctx.Sender`. +Each reducer may accept as its first argument a `ReducerContext`, which includes contextual data such as the `Sender` which contains the Identity of the client that called the reducer, and the `Timestamp` when it was invoked. For now, we only need the `Sender`. It's also possible to call `SetName` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though. In `server/Lib.cs`, add to the `Module` class: ```csharp -[SpacetimeDB.Reducer] +[Reducer] public static void SetName(ReducerContext ctx, string name) { name = ValidateName(name); - var user = ctx.Db.User.Identity.Find(ctx.CallerIdentity); + var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { user.Name = name; @@ -161,7 +160,7 @@ We define a reducer `SendMessage`, which clients will call to send messages. It In `server/Lib.cs`, add to the `Module` class: ```csharp -[SpacetimeDB.Reducer] +[Reducer] public static void SendMessage(ReducerContext ctx, string text) { text = ValidateMessage(text); @@ -169,9 +168,9 @@ public static void SendMessage(ReducerContext ctx, string text) ctx.Db.Message.Insert( new Message { - Sender = ctx.CallerIdentity, + Sender = ctx.Sender, Text = text, - Sent = ctx.Timestamp.ToUnixTimeMilliseconds(), + Sent = ctx.Timestamp.MicrosecondsSinceUnixEpoch, } ); } @@ -202,33 +201,33 @@ 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 `reducerContext.Db.User.Identity.Find` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `reducerContext.Db.User.Identity.Update` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. +We'll use `reducerContext.Db.User.Identity.Find` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `reducerContext.Db.User.Identity.Update` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `User.Identity.Find` returns a nullable `User`, because the unique constraint from the `[PrimaryKey]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `User.Identity.Update`. In `server/Lib.cs`, add the definition of the connect reducer to the `Module` class: ```csharp -[SpacetimeDB.Reducer(ReducerKind.ClientConnected)] -public static void ClientConnected(ReducerContext reducerContext) +[Reducer(ReducerKind.ClientConnected)] +public static void ClientConnected(ReducerContext ctx) { - Log.Info($"Connect {reducerContext.CallerIdentity}"); - var user = reducerContext.Db.User.Identity.Find(reducerContext.CallerIdentity); + Log.Info($"Connect {ctx.Sender}"); + var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { // If this is a returning user, i.e., we already have a `User` with this `Identity`, // set `Online: true`, but leave `Name` and `Identity` unchanged. user.Online = true; - reducerContext.Db.User.Identity.Update(user); + ctx.Db.User.Identity.Update(user); } else { // If this is a new user, create a `User` object for the `Identity`, // which is online, but hasn't set a name. - reducerContext.Db.User.Insert( + ctx.Db.User.Insert( new User { Name = null, - Identity = reducerContext.CallerIdentity, + Identity = ctx.Sender, Online = true, } ); @@ -241,16 +240,16 @@ Similarly, whenever a client disconnects, the module will execute the `OnDisconn Add the following code after the `OnConnect` handler: ```csharp -[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)] -public static void ClientDisconnected(ReducerContext reducerContext) +[Reducer(ReducerKind.ClientDisconnected)] +public static void ClientDisconnected(ReducerContext ctx) { - var user = reducerContext.Db.User.Identity.Find(reducerContext.CallerIdentity); + var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { // This user should exist, so set `Online: false`. user.Online = false; - reducerContext.Db.User.Identity.Update(user); + ctx.Db.User.Identity.Update(user); } else { @@ -274,8 +273,10 @@ From the `quickstart-chat` directory, run: spacetime publish --project-path server ``` +Note: If `wasm-opt` is installed, `spacetime publish` will automatically optimize the Web Assembly output of the module. `wasm-opt` can be installed by running: + ```bash -npm i wasm-opt -g +cargo install wasm-opt ``` ## Call Reducers diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md index db06d9a4..c7d93aea 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -66,11 +66,8 @@ We will also need to create some global variables that will be explained when we // our local client SpacetimeDB identity Identity? local_identity = null; -// declare a thread safe queue to store commands in format (command, args) -ConcurrentQueue<(string,string)> input_queue = new ConcurrentQueue<(string, string)>(); - -// declare a threadsafe cancel token to cancel the process loop -CancellationTokenSource cancel_token = new CancellationTokenSource(); +// declare a thread safe queue to store commands +var input_queue = new ConcurrentQueue<(string Command, string Args)>(); ``` ## Define Main function @@ -89,47 +86,78 @@ void Main() { AuthToken.Init(".spacetime_csharp_quickstart"); - RegisterCallbacks(); - - // spawn a thread to call process updates and process commands - var thread = new Thread(ProcessThread); + // Builds and connects to the database + DbConnection? conn = null; + conn = ConnectToDB(); + // Registers callbacks for reducers + RegisterCallbacks(conn); + // Declare a threadsafe cancel token to cancel the process loop + var cancellationTokenSource = new CancellationTokenSource(); + // Spawn a thread to call process updates and process commands + var thread = new Thread(() => ProcessThread(conn, cancellationTokenSource.Token)); thread.Start(); - + // Handles CLI input InputLoop(); - - // this signals the ProcessThread to stop - cancel_token.Cancel(); + // This signals the ProcessThread to stop + cancellationTokenSource.Cancel(); thread.Join(); } ``` -## Register callbacks +## Connect to database -We need to handle several sorts of events: +Before we connect, we'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`. -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. +Next we build our connection to the database, and while we are doing so, we can register several of our callbacks. We'll provide the connection builder the following: +1. The URI of the server running the SpacetimeDB server module. If it's running on the same computer as, we can set `HOST` to be `"http://localhost:3000"` +2. The name of the module we are looking to communicate with on the server. Replace `` with the name you chose when publishing your module during the module quickstart. +3. The connection builder 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. +4. We register a callback for `OnConnect`, where we will call `Subscribe` to tell the module what tables we care about. +5. We register a callback for `OnConnectError`, which will be called if there are any errors during a connection attempt. +6. We register a callback for `OnDisconnect`, which will be called when the client disconnects from the server. +7. And finally we call `Build` to build our DbConnection. ```csharp -void RegisterCallbacks() +const string HOST = "http://localhost:3000"; +const string DBNAME = ""; + +/// Load credentials from a file and connect to the database. +DbConnection ConnectToDB() { - SpacetimeDBClient.instance.onConnect += OnConnect; - SpacetimeDBClient.instance.onIdentityReceived += OnIdentityReceived; - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + DbConnection? conn = null; + conn = DbConnection.Builder() + .WithUri(HOST) + .WithModuleName(DBNAME) + .WithToken(AuthToken.Token) + .OnConnect(OnConnected) + .OnConnectError(OnConnectError) + .OnDisconnect(OnDisconnect) + .Build(); + return conn; +} +``` + +## Register callbacks + +Now we need to handle several sorts of events with Tables and Reducers: - User.OnInsert += User_OnInsert; - User.OnUpdate += User_OnUpdate; +1. `User.OnInsert`: When a new user joins, we'll print a message introducing them. +2. `User.OnUpdate`: When a user is updated, we'll print their new name, or declare their new online status. +3. `Message.OnInsert`: When we receive a new message, we'll print it. +4. `Reducer.OnSetName`: If the server rejects our attempt to set our name, we'll print an error. +5. `Reducer.OnSendMessage`: If the server rejects a message we send, we'll print an error. + +```csharp +/// Register all the callbacks our app will use to respond to database events. +void RegisterCallbacks(DbConnection conn) +{ + conn.Db.User.OnInsert += User_OnInsert; + conn.Db.User.OnUpdate += User_OnUpdate; - Message.OnInsert += Message_OnInsert; + conn.Db.Message.OnInsert += Message_OnInsert; - Reducer.OnSetNameEvent += Reducer_OnSetNameEvent; - Reducer.OnSendMessageEvent += Reducer_OnSendMessageEvent; + conn.Reducers.OnSetName += Reducer_OnSetNameEvent; + conn.Reducers.OnSendMessage += Reducer_OnSendMessageEvent; } ``` @@ -144,14 +172,14 @@ These callbacks can fire in two contexts: This second case means that, even though the module only ever inserts online users, the client's `User.OnInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. -`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `ReducerEvent`. This will be `null` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument. +`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `EventContext`. This will be `null` for rows inserted when initializing the cache for a subscription. The `EventContext.Event` 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); +string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString();//[..8]; -void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) +void User_OnInsert(EventContext ctx, User insertedValue) { if (insertedValue.Online) { @@ -162,9 +190,9 @@ void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) ### Notify about updated users -Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User::update_by_identity` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column. +Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User.Identity.Update` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column. -`OnUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. +`OnUpdate` callbacks take three arguments: the old row, the new row, and a `EventContext`. In our module, users can be updated for three reasons: @@ -175,23 +203,22 @@ In our module, users can be updated for three reasons: We'll print an appropriate message in each of these cases. ```csharp -void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) +void User_OnUpdate(EventContext ctx, User oldValue, User newValue) { if (oldValue.Name != newValue.Name) { Console.WriteLine($"{UserNameOrIdentity(oldValue)} renamed to {newValue.Name}"); } - - if (oldValue.Online == newValue.Online) - return; - - if (newValue.Online) - { - Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); - } - else + if (oldValue.Online != newValue.Online) { - Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); + if (newValue.Online) + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); + } + else + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); + } } } ``` @@ -200,14 +227,14 @@ void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) When we receive a new message, we'll print it to standard output, along with the name of the user who sent it. Keep in mind that we only want to do this for new messages, i.e. those inserted by a `SendMessage` reducer invocation. We have to handle the backlog we receive when our subscription is initialized separately, to ensure they're printed in the correct order. To that effect, our `OnInsert` callback will check if its `ReducerEvent` argument is not `null`, and only print in that case. -To find the `User` based on the message's `Sender` identity, we'll use `User::FindByIdentity`, which behaves like the same function on the server. +To find the `User` based on the message's `Sender` identity, we'll use `User.Identity.Find`, which behaves like the same function on the server. We'll print the user's name or identity in the same way as we did when notifying about `User` table events, but here we have to handle the case where we don't find a matching `User` row. This can happen when the module owner sends a message using the CLI's `spacetime call`. In this case, we'll print `unknown`. ```csharp -void PrintMessage(Message message) +void PrintMessage(RemoteTables tables, Message message) { - var sender = User.FindByIdentity(message.Sender); + var sender = tables.User.Identity.Find(message.Sender); var senderName = "unknown"; if (sender != null) { @@ -217,11 +244,11 @@ void PrintMessage(Message message) Console.WriteLine($"{senderName}: {message.Text}"); } -void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent) +void Message_OnInsert(EventContext ctx, Message insertedValue) { - if (dbEvent != null) + if (ctx.Event is not Event.SubscribeApplied) { - PrintMessage(insertedValue); + PrintMessage(ctx.Db, insertedValue); } } ``` @@ -232,9 +259,9 @@ We can also register callbacks to run each time a reducer is invoked. We registe Each reducer callback takes one fixed argument: -The ReducerEvent that triggered the callback. It contains several fields. The ones we care about are: +The `ReducerEventContext` of the callback which contains an `Event` that contains several fields. The ones we care about are: -1. The `Identity` of the client that called the reducer. +1. The `CallerIdentity` is 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. @@ -252,15 +279,12 @@ We already handle successful `SetName` invocations using our `User.OnUpdate` cal We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. ```csharp -void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) +void Reducer_OnSetNameEvent(ReducerEventContext ctx, string name) { - bool localIdentityFailedToChangeName = - reducerEvent.Identity == local_identity && - reducerEvent.Status == ClientApi.Event.Types.Status.Failed; - - if (localIdentityFailedToChangeName) + var e = ctx.Event; + if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error)) { - Console.Write($"Failed to change name to {name}"); + Console.Write($"Failed to change name to {name}: {error}"); } } ``` @@ -270,42 +294,60 @@ void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different. ```csharp -void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) +void Reducer_OnSendMessageEvent(ReducerEventContext ctx, string text) { - bool localIdentityFailedToSendMessage = - reducerEvent.Identity == local_identity && - reducerEvent.Status == ClientApi.Event.Types.Status.Failed; - - if (localIdentityFailedToSendMessage) + var e = ctx.Event; + if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error)) { - Console.Write($"Failed to send message {text}"); + Console.Write($"Failed to send message {text}: {error}"); } } ``` ## Connect callback -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. +Once we are connected, we'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app. + +Then 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 using `SubscribeToAllTables`. ```csharp -void OnConnect() +void OnConnected(DbConnection conn, Identity identity, string authToken) { - SpacetimeDBClient.instance.Subscribe(new List - { - "SELECT * FROM User", "SELECT * FROM Message" - }); + local_identity = identity; + AuthToken.SaveToken(authToken); + + conn.SubscriptionBuilder() + .OnApplied(OnSubscriptionApplied) + .SubscribeToAllTables(); } ``` -## OnIdentityReceived callback +You can also subscribe to specific tables using SQL syntax, e.g. `SELECT * FROM my_table`. Our [SQL documentation](/docs/sql) enumerates the operations that are accepted in our SQL syntax. -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. +## Connect Error callback + +Should we get an error during connection, we'll be given an `Exception` which contains the details about the exception. To keep things simple, we'll just write the exception to the console. ```csharp -void OnIdentityReceived(string authToken, Identity identity, Address _address) +void OnConnectError(Exception e) { - local_identity = identity; - AuthToken.SaveToken(authToken); + Console.Write($"Error while connecting: {e}"); +} +``` + +## Disconnect callback + +When Disconnecting, the callback contains the connection details and if an error occurs, it will also contain an `Exception`. If we get an error, we'll write the error to the console, if not, we'll just write that we disconnected. + +```csharp +void OnDisconnect(DbConnection conn, Exception? e) +{ + if (e != null) + { + Console.Write($"Disconnected abnormally: {e}"); + } else { + Console.Write($"Disconnected normally."); + } } ``` @@ -314,54 +356,48 @@ void OnIdentityReceived(string authToken, Identity identity, Address _address) Once our subscription is applied, we'll print all the previously sent messages. We'll define a function `PrintMessagesInOrder` to do this. `PrintMessagesInOrder` calls the automatically generated `Iter` function on our `Message` table, which returns an iterator over all rows in the table. We'll use the `OrderBy` method on the iterator to sort the messages by their `Sent` timestamp. ```csharp -void PrintMessagesInOrder() +void PrintMessagesInOrder(RemoteTables tables) { - foreach (Message message in Message.Iter().OrderBy(item => item.Sent)) + foreach (Message message in tables.Message.Iter().OrderBy(item => item.Sent)) { - PrintMessage(message); + PrintMessage(tables, message); } } -void OnSubscriptionApplied() +void OnSubscriptionApplied(SubscriptionEventContext ctx) { Console.WriteLine("Connected"); - PrintMessagesInOrder(); + PrintMessagesInOrder(ctx.Db); } ``` - - ## 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. +1. 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. -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. +2. Finally, Close the connection to the module. ```csharp -const string HOST = "http://localhost:3000"; -const string DBNAME = "module"; - -void ProcessThread() +void ProcessThread(DbConnection conn, CancellationToken ct) { - SpacetimeDBClient.instance.Connect(AuthToken.Token, HOST, DBNAME); - - // loop until cancellation token - while (!cancel_token.IsCancellationRequested) + try { - SpacetimeDBClient.instance.Update(); + // loop until cancellation token + while (!ct.IsCancellationRequested) + { + conn.FrameTick(); - ProcessCommands(); + ProcessCommands(conn.Reducers); - Thread.Sleep(100); + Thread.Sleep(100); + } + } + finally + { + conn.Disconnect(); } - - SpacetimeDBClient.instance.Close(); } ``` @@ -388,7 +424,7 @@ void InputLoop() if (input.StartsWith("/name ")) { - input_queue.Enqueue(("name", input.Substring(6))); + input_queue.Enqueue(("name", input[6..])); continue; } else @@ -398,18 +434,18 @@ void InputLoop() } } -void ProcessCommands() +void ProcessCommands(RemoteReducers reducers) { // process input queue commands while (input_queue.TryDequeue(out var command)) { - switch (command.Item1) + switch (command.Command) { case "message": - Reducer.SendMessage(command.Item2); + reducers.SendMessage(command.Args); break; case "name": - Reducer.SetName(command.Item2); + reducers.SetName(command.Args); break; } } @@ -432,4 +468,10 @@ dotnet run --project client ## What's next? -Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example. +Congratulations! You've built a simple chat app using SpacetimeDB. + +You can find the full code for this client [in the C# SDK's examples](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/tree/master/examples~/quickstart/client). + +Check out the [C# SDK Reference](/docs/sdks/c-sharp) for a more comprehensive view of the SpacetimeDB Rust SDK. + +If you are interested in developing in the Unity game engine, check out our [Unity Comprehensive Tutorial](/docs/unity) and [Blackholio](https://github.com/ClockworkLabs/Blackholio) game example. From cedb90d0b434e1472a1b71628866bbc6efd11b2a Mon Sep 17 00:00:00 2001 From: rekhoff Date: Tue, 28 Jan 2025 09:23:03 -0800 Subject: [PATCH 03/17] Initial code pass on updating server to 1.0.0 --- docs/modules/c-sharp/quickstart.md | 72 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index 571351c1..d940d493 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -60,9 +60,7 @@ spacetime init --lang csharp 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; +using SpacetimeDB; ``` - `SpacetimeDB.Module` contains the special attributes we'll use to define tables and reducers in our module. @@ -85,10 +83,10 @@ For each `User`, we'll store their `Identity`, an optional name they can set to In `server/Lib.cs`, add the definition of the table `User` to the `Module` class: ```csharp -[SpacetimeDB.Table(Public = true)] +[Table(Name = "User", Public = true)] public partial class User { - [SpacetimeDB.Column(ColumnAttrs.PrimaryKey)] + [PrimaryKey] public Identity Identity; public string? Name; public bool Online; @@ -100,7 +98,7 @@ For each `Message`, we'll store the `Identity` of the user who sent it, the `Tim In `server/Lib.cs`, add the definition of the table `Message` to the `Module` class: ```csharp -[SpacetimeDB.Table(Public = true)] +[Table(Name = "Message", Public = true)] public partial class Message { public Identity Sender; @@ -125,11 +123,11 @@ public static void SetName(ReducerContext ctx, string name) { name = ValidateName(name); - var user = User.FindByIdentity(ctx.Sender); + var user = ctx.Db.User.Identity.Find(ctx.CallerIdentity); if (user is not null) { user.Name = name; - User.UpdateByIdentity(ctx.Sender, user); + ctx.Db.User.Identity.Update(user); } } ``` @@ -146,7 +144,7 @@ In `server/Lib.cs`, add to the `Module` class: ```csharp /// Takes a name and checks if it's acceptable as a user's name. -public static string ValidateName(string name) +private static string ValidateName(string name) { if (string.IsNullOrEmpty(name)) { @@ -167,13 +165,15 @@ In `server/Lib.cs`, add to the `Module` class: public static void SendMessage(ReducerContext ctx, string text) { text = ValidateMessage(text); - Log(text); - new Message - { - Sender = ctx.Sender, - Text = text, - Sent = ctx.Time.ToUnixTimeMilliseconds(), - }.Insert(); + Log.Info(text); + ctx.Db.Message.Insert( + new Message + { + Sender = ctx.CallerIdentity, + Text = text, + Sent = ctx.Timestamp.ToUnixTimeMilliseconds(), + } + ); } ``` @@ -183,7 +183,7 @@ In `server/Lib.cs`, add to the `Module` class: ```csharp /// Takes a message's text and checks if it's acceptable to send. -public static string ValidateMessage(string text) +private static string ValidateMessage(string text) { if (string.IsNullOrEmpty(text)) { @@ -202,58 +202,60 @@ You could extend the validation in `ValidateMessage` in similar ways to `Validat In C# modules, you can register for `Connect` and `Disconnect` events by using a special `ReducerKind`. We'll use the `Connect` event to create a `User` record for the client if it doesn't yet exist, and to set its online status. -We'll use `User.FindByIdentity` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `User.UpdateByIdentity` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. +We'll use `reducerContext.Db.User.Identity.Find` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `reducerContext.Db.User.Identity.Update` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `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(ReducerContext ReducerContext) +[SpacetimeDB.Reducer(ReducerKind.ClientConnected)] +public static void ClientConnected(ReducerContext reducerContext) { - Log($"Connect {ReducerContext.Sender}"); - var user = User.FindByIdentity(ReducerContext.Sender); + Log.Info($"Connect {reducerContext.CallerIdentity}"); + var user = reducerContext.Db.User.Identity.Find(reducerContext.CallerIdentity); if (user is not null) { // If this is a returning user, i.e., we already have a `User` with this `Identity`, // set `Online: true`, but leave `Name` and `Identity` unchanged. user.Online = true; - User.UpdateByIdentity(ReducerContext.Sender, user); + reducerContext.Db.User.Identity.Update(user); } else { // If this is a new user, create a `User` object for the `Identity`, // which is online, but hasn't set a name. - new User - { - Name = null, - Identity = ReducerContext.Sender, - Online = true, - }.Insert(); + reducerContext.Db.User.Insert( + new User + { + Name = null, + Identity = reducerContext.CallerIdentity, + Online = true, + } + ); } } ``` -Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.Disconnect`. We'll use it to un-set the `Online` status of the `User` for the disconnected client. +Similarly, whenever a client disconnects, the module will execute the `OnDisconnect` event if it's registered with `ReducerKind.ClientDisconnected`. We'll use it to un-set the `Online` status of the `User` for the disconnected client. Add the following code after the `OnConnect` handler: ```csharp -[SpacetimeDB.Reducer(ReducerKind.Disconnect)] -public static void OnDisconnect(ReducerContext ReducerContext) +[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)] +public static void ClientDisconnected(ReducerContext reducerContext) { - var user = User.FindByIdentity(ReducerContext.Sender); + var user = reducerContext.Db.User.Identity.Find(reducerContext.CallerIdentity); if (user is not null) { // This user should exist, so set `Online: false`. user.Online = false; - User.UpdateByIdentity(ReducerContext.Sender, user); + reducerContext.Db.User.Identity.Update(user); } else { // User does not exist, log warning - Log("Warning: No user found for disconnected client."); + Log.Warn("Warning: No user found for disconnected client."); } } ``` From 3cb446b208d99691819c68cd9d161c576472cd5c Mon Sep 17 00:00:00 2001 From: rekhoff Date: Tue, 18 Feb 2025 16:11:38 -0800 Subject: [PATCH 04/17] Updated to work with current 1.0.0-rc4, master branches of SpacetimeDB and the CSharpSDK --- docs/modules/c-sharp/quickstart.md | 45 ++--- docs/sdks/c-sharp/quickstart.md | 268 +++++++++++++++++------------ 2 files changed, 178 insertions(+), 135 deletions(-) diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index d940d493..252ed640 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -63,13 +63,12 @@ To the top of `server/Lib.cs`, add some imports we'll be using: using SpacetimeDB; ``` -- `SpacetimeDB.Module` contains the special attributes we'll use to define tables and reducers in our module. -- `SpacetimeDB.Runtime` contains the raw API bindings SpacetimeDB uses to communicate with the database. +- `SpacetimeDB` contains the special attributes we'll use to define tables and reducers in our module and the raw API bindings SpacetimeDB uses to communicate with the database. We also need to create our static module class which all of the module code will live in. In `server/Lib.cs`, add: ```csharp -static partial class Module +public static partial class Module { } ``` @@ -111,19 +110,19 @@ public partial class Message We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `SetName` which clients can invoke to set their `User.Name`. It will validate the caller's chosen name, using a function `ValidateName` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail. -Each reducer may accept as its first argument a `ReducerContext`, which includes the `Identity` and `Address` of the client that called the reducer, and the `Timestamp` when it was invoked. For now, we only need the `Identity`, `ctx.Sender`. +Each reducer may accept as its first argument a `ReducerContext`, which includes contextual data such as the `Sender` which contains the Identity of the client that called the reducer, and the `Timestamp` when it was invoked. For now, we only need the `Sender`. It's also possible to call `SetName` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though. In `server/Lib.cs`, add to the `Module` class: ```csharp -[SpacetimeDB.Reducer] +[Reducer] public static void SetName(ReducerContext ctx, string name) { name = ValidateName(name); - var user = ctx.Db.User.Identity.Find(ctx.CallerIdentity); + var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { user.Name = name; @@ -161,7 +160,7 @@ We define a reducer `SendMessage`, which clients will call to send messages. It In `server/Lib.cs`, add to the `Module` class: ```csharp -[SpacetimeDB.Reducer] +[Reducer] public static void SendMessage(ReducerContext ctx, string text) { text = ValidateMessage(text); @@ -169,9 +168,9 @@ public static void SendMessage(ReducerContext ctx, string text) ctx.Db.Message.Insert( new Message { - Sender = ctx.CallerIdentity, + Sender = ctx.Sender, Text = text, - Sent = ctx.Timestamp.ToUnixTimeMilliseconds(), + Sent = ctx.Timestamp.MicrosecondsSinceUnixEpoch, } ); } @@ -202,33 +201,33 @@ 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 `reducerContext.Db.User.Identity.Find` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `reducerContext.Db.User.Identity.Update` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `FindByIdentity` returns a nullable `User`, because the unique constraint from the `[SpacetimeDB.Column(ColumnAttrs.PrimaryKey)]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `UpdateByIdentity`. +We'll use `reducerContext.Db.User.Identity.Find` to look up a `User` row for `ctx.Sender`, if one exists. If we find one, we'll use `reducerContext.Db.User.Identity.Update` to overwrite it with a row that has `Online: true`. If not, we'll use `User.Insert` to insert a new row for our new user. All three of these methods are generated by the `[SpacetimeDB.Table]` attribute, with rows and behavior based on the row attributes. `User.Identity.Find` returns a nullable `User`, because the unique constraint from the `[PrimaryKey]` attribute means there will be either zero or one matching rows. `Insert` will throw an exception if the insert violates this constraint; if we want to overwrite a `User` row, we need to do so explicitly using `User.Identity.Update`. In `server/Lib.cs`, add the definition of the connect reducer to the `Module` class: ```csharp -[SpacetimeDB.Reducer(ReducerKind.ClientConnected)] -public static void ClientConnected(ReducerContext reducerContext) +[Reducer(ReducerKind.ClientConnected)] +public static void ClientConnected(ReducerContext ctx) { - Log.Info($"Connect {reducerContext.CallerIdentity}"); - var user = reducerContext.Db.User.Identity.Find(reducerContext.CallerIdentity); + Log.Info($"Connect {ctx.Sender}"); + var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { // If this is a returning user, i.e., we already have a `User` with this `Identity`, // set `Online: true`, but leave `Name` and `Identity` unchanged. user.Online = true; - reducerContext.Db.User.Identity.Update(user); + ctx.Db.User.Identity.Update(user); } else { // If this is a new user, create a `User` object for the `Identity`, // which is online, but hasn't set a name. - reducerContext.Db.User.Insert( + ctx.Db.User.Insert( new User { Name = null, - Identity = reducerContext.CallerIdentity, + Identity = ctx.Sender, Online = true, } ); @@ -241,16 +240,16 @@ Similarly, whenever a client disconnects, the module will execute the `OnDisconn Add the following code after the `OnConnect` handler: ```csharp -[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)] -public static void ClientDisconnected(ReducerContext reducerContext) +[Reducer(ReducerKind.ClientDisconnected)] +public static void ClientDisconnected(ReducerContext ctx) { - var user = reducerContext.Db.User.Identity.Find(reducerContext.CallerIdentity); + var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { // This user should exist, so set `Online: false`. user.Online = false; - reducerContext.Db.User.Identity.Update(user); + ctx.Db.User.Identity.Update(user); } else { @@ -274,8 +273,10 @@ From the `quickstart-chat` directory, run: spacetime publish --project-path server ``` +Note: If `wasm-opt` is installed, `spacetime publish` will automatically optimize the Web Assembly output of the module. `wasm-opt` can be installed by running: + ```bash -npm i wasm-opt -g +cargo install wasm-opt ``` ## Call Reducers diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md index db06d9a4..c7d93aea 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -66,11 +66,8 @@ We will also need to create some global variables that will be explained when we // our local client SpacetimeDB identity Identity? local_identity = null; -// declare a thread safe queue to store commands in format (command, args) -ConcurrentQueue<(string,string)> input_queue = new ConcurrentQueue<(string, string)>(); - -// declare a threadsafe cancel token to cancel the process loop -CancellationTokenSource cancel_token = new CancellationTokenSource(); +// declare a thread safe queue to store commands +var input_queue = new ConcurrentQueue<(string Command, string Args)>(); ``` ## Define Main function @@ -89,47 +86,78 @@ void Main() { AuthToken.Init(".spacetime_csharp_quickstart"); - RegisterCallbacks(); - - // spawn a thread to call process updates and process commands - var thread = new Thread(ProcessThread); + // Builds and connects to the database + DbConnection? conn = null; + conn = ConnectToDB(); + // Registers callbacks for reducers + RegisterCallbacks(conn); + // Declare a threadsafe cancel token to cancel the process loop + var cancellationTokenSource = new CancellationTokenSource(); + // Spawn a thread to call process updates and process commands + var thread = new Thread(() => ProcessThread(conn, cancellationTokenSource.Token)); thread.Start(); - + // Handles CLI input InputLoop(); - - // this signals the ProcessThread to stop - cancel_token.Cancel(); + // This signals the ProcessThread to stop + cancellationTokenSource.Cancel(); thread.Join(); } ``` -## Register callbacks +## Connect to database -We need to handle several sorts of events: +Before we connect, we'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`. -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. +Next we build our connection to the database, and while we are doing so, we can register several of our callbacks. We'll provide the connection builder the following: +1. The URI of the server running the SpacetimeDB server module. If it's running on the same computer as, we can set `HOST` to be `"http://localhost:3000"` +2. The name of the module we are looking to communicate with on the server. Replace `` with the name you chose when publishing your module during the module quickstart. +3. The connection builder 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. +4. We register a callback for `OnConnect`, where we will call `Subscribe` to tell the module what tables we care about. +5. We register a callback for `OnConnectError`, which will be called if there are any errors during a connection attempt. +6. We register a callback for `OnDisconnect`, which will be called when the client disconnects from the server. +7. And finally we call `Build` to build our DbConnection. ```csharp -void RegisterCallbacks() +const string HOST = "http://localhost:3000"; +const string DBNAME = ""; + +/// Load credentials from a file and connect to the database. +DbConnection ConnectToDB() { - SpacetimeDBClient.instance.onConnect += OnConnect; - SpacetimeDBClient.instance.onIdentityReceived += OnIdentityReceived; - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + DbConnection? conn = null; + conn = DbConnection.Builder() + .WithUri(HOST) + .WithModuleName(DBNAME) + .WithToken(AuthToken.Token) + .OnConnect(OnConnected) + .OnConnectError(OnConnectError) + .OnDisconnect(OnDisconnect) + .Build(); + return conn; +} +``` + +## Register callbacks + +Now we need to handle several sorts of events with Tables and Reducers: - User.OnInsert += User_OnInsert; - User.OnUpdate += User_OnUpdate; +1. `User.OnInsert`: When a new user joins, we'll print a message introducing them. +2. `User.OnUpdate`: When a user is updated, we'll print their new name, or declare their new online status. +3. `Message.OnInsert`: When we receive a new message, we'll print it. +4. `Reducer.OnSetName`: If the server rejects our attempt to set our name, we'll print an error. +5. `Reducer.OnSendMessage`: If the server rejects a message we send, we'll print an error. + +```csharp +/// Register all the callbacks our app will use to respond to database events. +void RegisterCallbacks(DbConnection conn) +{ + conn.Db.User.OnInsert += User_OnInsert; + conn.Db.User.OnUpdate += User_OnUpdate; - Message.OnInsert += Message_OnInsert; + conn.Db.Message.OnInsert += Message_OnInsert; - Reducer.OnSetNameEvent += Reducer_OnSetNameEvent; - Reducer.OnSendMessageEvent += Reducer_OnSendMessageEvent; + conn.Reducers.OnSetName += Reducer_OnSetNameEvent; + conn.Reducers.OnSendMessage += Reducer_OnSendMessageEvent; } ``` @@ -144,14 +172,14 @@ These callbacks can fire in two contexts: This second case means that, even though the module only ever inserts online users, the client's `User.OnInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. -`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `ReducerEvent`. This will be `null` for rows inserted when initializing the cache for a subscription. `ReducerEvent` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument. +`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `EventContext`. This will be `null` for rows inserted when initializing the cache for a subscription. The `EventContext.Event` 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); +string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString();//[..8]; -void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) +void User_OnInsert(EventContext ctx, User insertedValue) { if (insertedValue.Online) { @@ -162,9 +190,9 @@ void User_OnInsert(User insertedValue, ReducerEvent? dbEvent) ### Notify about updated users -Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User::update_by_identity` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column. +Because we declared a primary key column in our `User` table, we can also register on-update callbacks. These run whenever a row is replaced by a row with the same primary key, like our module's `User.Identity.Update` calls. We register these callbacks using the `OnUpdate` method, which is automatically implemented by `spacetime generate` for any table with a primary key column. -`OnUpdate` callbacks take three arguments: the old row, the new row, and a `ReducerEvent`. +`OnUpdate` callbacks take three arguments: the old row, the new row, and a `EventContext`. In our module, users can be updated for three reasons: @@ -175,23 +203,22 @@ In our module, users can be updated for three reasons: We'll print an appropriate message in each of these cases. ```csharp -void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) +void User_OnUpdate(EventContext ctx, User oldValue, User newValue) { if (oldValue.Name != newValue.Name) { Console.WriteLine($"{UserNameOrIdentity(oldValue)} renamed to {newValue.Name}"); } - - if (oldValue.Online == newValue.Online) - return; - - if (newValue.Online) - { - Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); - } - else + if (oldValue.Online != newValue.Online) { - Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); + if (newValue.Online) + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} connected."); + } + else + { + Console.WriteLine($"{UserNameOrIdentity(newValue)} disconnected."); + } } } ``` @@ -200,14 +227,14 @@ void User_OnUpdate(User oldValue, User newValue, ReducerEvent dbEvent) When we receive a new message, we'll print it to standard output, along with the name of the user who sent it. Keep in mind that we only want to do this for new messages, i.e. those inserted by a `SendMessage` reducer invocation. We have to handle the backlog we receive when our subscription is initialized separately, to ensure they're printed in the correct order. To that effect, our `OnInsert` callback will check if its `ReducerEvent` argument is not `null`, and only print in that case. -To find the `User` based on the message's `Sender` identity, we'll use `User::FindByIdentity`, which behaves like the same function on the server. +To find the `User` based on the message's `Sender` identity, we'll use `User.Identity.Find`, which behaves like the same function on the server. We'll print the user's name or identity in the same way as we did when notifying about `User` table events, but here we have to handle the case where we don't find a matching `User` row. This can happen when the module owner sends a message using the CLI's `spacetime call`. In this case, we'll print `unknown`. ```csharp -void PrintMessage(Message message) +void PrintMessage(RemoteTables tables, Message message) { - var sender = User.FindByIdentity(message.Sender); + var sender = tables.User.Identity.Find(message.Sender); var senderName = "unknown"; if (sender != null) { @@ -217,11 +244,11 @@ void PrintMessage(Message message) Console.WriteLine($"{senderName}: {message.Text}"); } -void Message_OnInsert(Message insertedValue, ReducerEvent? dbEvent) +void Message_OnInsert(EventContext ctx, Message insertedValue) { - if (dbEvent != null) + if (ctx.Event is not Event.SubscribeApplied) { - PrintMessage(insertedValue); + PrintMessage(ctx.Db, insertedValue); } } ``` @@ -232,9 +259,9 @@ We can also register callbacks to run each time a reducer is invoked. We registe Each reducer callback takes one fixed argument: -The ReducerEvent that triggered the callback. It contains several fields. The ones we care about are: +The `ReducerEventContext` of the callback which contains an `Event` that contains several fields. The ones we care about are: -1. The `Identity` of the client that called the reducer. +1. The `CallerIdentity` is 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. @@ -252,15 +279,12 @@ We already handle successful `SetName` invocations using our `User.OnUpdate` cal We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. ```csharp -void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) +void Reducer_OnSetNameEvent(ReducerEventContext ctx, string name) { - bool localIdentityFailedToChangeName = - reducerEvent.Identity == local_identity && - reducerEvent.Status == ClientApi.Event.Types.Status.Failed; - - if (localIdentityFailedToChangeName) + var e = ctx.Event; + if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error)) { - Console.Write($"Failed to change name to {name}"); + Console.Write($"Failed to change name to {name}: {error}"); } } ``` @@ -270,42 +294,60 @@ void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different. ```csharp -void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) +void Reducer_OnSendMessageEvent(ReducerEventContext ctx, string text) { - bool localIdentityFailedToSendMessage = - reducerEvent.Identity == local_identity && - reducerEvent.Status == ClientApi.Event.Types.Status.Failed; - - if (localIdentityFailedToSendMessage) + var e = ctx.Event; + if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error)) { - Console.Write($"Failed to send message {text}"); + Console.Write($"Failed to send message {text}: {error}"); } } ``` ## Connect callback -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. +Once we are connected, we'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app. + +Then 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 using `SubscribeToAllTables`. ```csharp -void OnConnect() +void OnConnected(DbConnection conn, Identity identity, string authToken) { - SpacetimeDBClient.instance.Subscribe(new List - { - "SELECT * FROM User", "SELECT * FROM Message" - }); + local_identity = identity; + AuthToken.SaveToken(authToken); + + conn.SubscriptionBuilder() + .OnApplied(OnSubscriptionApplied) + .SubscribeToAllTables(); } ``` -## OnIdentityReceived callback +You can also subscribe to specific tables using SQL syntax, e.g. `SELECT * FROM my_table`. Our [SQL documentation](/docs/sql) enumerates the operations that are accepted in our SQL syntax. -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. +## Connect Error callback + +Should we get an error during connection, we'll be given an `Exception` which contains the details about the exception. To keep things simple, we'll just write the exception to the console. ```csharp -void OnIdentityReceived(string authToken, Identity identity, Address _address) +void OnConnectError(Exception e) { - local_identity = identity; - AuthToken.SaveToken(authToken); + Console.Write($"Error while connecting: {e}"); +} +``` + +## Disconnect callback + +When Disconnecting, the callback contains the connection details and if an error occurs, it will also contain an `Exception`. If we get an error, we'll write the error to the console, if not, we'll just write that we disconnected. + +```csharp +void OnDisconnect(DbConnection conn, Exception? e) +{ + if (e != null) + { + Console.Write($"Disconnected abnormally: {e}"); + } else { + Console.Write($"Disconnected normally."); + } } ``` @@ -314,54 +356,48 @@ void OnIdentityReceived(string authToken, Identity identity, Address _address) Once our subscription is applied, we'll print all the previously sent messages. We'll define a function `PrintMessagesInOrder` to do this. `PrintMessagesInOrder` calls the automatically generated `Iter` function on our `Message` table, which returns an iterator over all rows in the table. We'll use the `OrderBy` method on the iterator to sort the messages by their `Sent` timestamp. ```csharp -void PrintMessagesInOrder() +void PrintMessagesInOrder(RemoteTables tables) { - foreach (Message message in Message.Iter().OrderBy(item => item.Sent)) + foreach (Message message in tables.Message.Iter().OrderBy(item => item.Sent)) { - PrintMessage(message); + PrintMessage(tables, message); } } -void OnSubscriptionApplied() +void OnSubscriptionApplied(SubscriptionEventContext ctx) { Console.WriteLine("Connected"); - PrintMessagesInOrder(); + PrintMessagesInOrder(ctx.Db); } ``` - - ## 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. +1. 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. -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. +2. Finally, Close the connection to the module. ```csharp -const string HOST = "http://localhost:3000"; -const string DBNAME = "module"; - -void ProcessThread() +void ProcessThread(DbConnection conn, CancellationToken ct) { - SpacetimeDBClient.instance.Connect(AuthToken.Token, HOST, DBNAME); - - // loop until cancellation token - while (!cancel_token.IsCancellationRequested) + try { - SpacetimeDBClient.instance.Update(); + // loop until cancellation token + while (!ct.IsCancellationRequested) + { + conn.FrameTick(); - ProcessCommands(); + ProcessCommands(conn.Reducers); - Thread.Sleep(100); + Thread.Sleep(100); + } + } + finally + { + conn.Disconnect(); } - - SpacetimeDBClient.instance.Close(); } ``` @@ -388,7 +424,7 @@ void InputLoop() if (input.StartsWith("/name ")) { - input_queue.Enqueue(("name", input.Substring(6))); + input_queue.Enqueue(("name", input[6..])); continue; } else @@ -398,18 +434,18 @@ void InputLoop() } } -void ProcessCommands() +void ProcessCommands(RemoteReducers reducers) { // process input queue commands while (input_queue.TryDequeue(out var command)) { - switch (command.Item1) + switch (command.Command) { case "message": - Reducer.SendMessage(command.Item2); + reducers.SendMessage(command.Args); break; case "name": - Reducer.SetName(command.Item2); + reducers.SetName(command.Args); break; } } @@ -432,4 +468,10 @@ dotnet run --project client ## What's next? -Congratulations! You've built a simple chat app using SpacetimeDB. You can look at the C# SDK Reference for more information about the client SDK. If you are interested in developing in the Unity game engine, check out our Unity3d Comprehensive Tutorial and BitcraftMini game example. +Congratulations! You've built a simple chat app using SpacetimeDB. + +You can find the full code for this client [in the C# SDK's examples](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/tree/master/examples~/quickstart/client). + +Check out the [C# SDK Reference](/docs/sdks/c-sharp) for a more comprehensive view of the SpacetimeDB Rust SDK. + +If you are interested in developing in the Unity game engine, check out our [Unity Comprehensive Tutorial](/docs/unity) and [Blackholio](https://github.com/ClockworkLabs/Blackholio) game example. From f964ab2fe90c5a4ceec6ec283ac976714056dc1c Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 19 Feb 2025 10:03:32 -0800 Subject: [PATCH 05/17] Minor edit for clarity --- 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 c7d93aea..2b0696c8 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -110,7 +110,7 @@ Before we connect, we'll store the SpacetimeDB host name and our module name in Next we build our connection to the database, and while we are doing so, we can register several of our callbacks. We'll provide the connection builder the following: 1. The URI of the server running the SpacetimeDB server module. If it's running on the same computer as, we can set `HOST` to be `"http://localhost:3000"` -2. The name of the module we are looking to communicate with on the server. Replace `` with the name you chose when publishing your module during the module quickstart. +2. The name of the module we are looking to communicate with on the server. Replace `` with `quickstart-chat` or the name you chose when publishing your module during the module quickstart. 3. The connection builder 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. 4. We register a callback for `OnConnect`, where we will call `Subscribe` to tell the module what tables we care about. 5. We register a callback for `OnConnectError`, which will be called if there are any errors during a connection attempt. From 9903f1e1ced93a6e6aabcc11643b502983543554 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 14:42:36 -0800 Subject: [PATCH 06/17] No longer optional, ReducerContext is always the first argument Co-authored-by: Phoebe Goldman --- 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 252ed640..6d354f2e 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -110,7 +110,7 @@ public partial class Message We want to allow users to set their names, because `Identity` is not a terribly user-friendly identifier. To that effect, we define a reducer `SetName` which clients can invoke to set their `User.Name`. It will validate the caller's chosen name, using a function `ValidateName` which we'll define next, then look up the `User` record for the caller and update it to store the validated name. If the name fails the validation, the reducer will fail. -Each reducer may accept as its first argument a `ReducerContext`, which includes contextual data such as the `Sender` which contains the Identity of the client that called the reducer, and the `Timestamp` when it was invoked. For now, we only need the `Sender`. +Each reducer must accept as its first argument a `ReducerContext`, which includes contextual data such as the `Sender` which contains the Identity of the client that called the reducer, and the `Timestamp` when it was invoked. For now, we only need the `Sender`. It's also possible to call `SetName` via the SpacetimeDB CLI's `spacetime call` command without a connection, in which case no `User` record will exist for the caller. We'll return an error in this case, but you could alter the reducer to insert a `User` row for the module owner. You'll have to decide whether the module owner is always online or always offline, though. From e074d5d6aed5f2ed8ef2d23207c8db457525f897 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 14:58:55 -0800 Subject: [PATCH 07/17] Improved description of OnInsert and OnDelete callbacks Co-authored-by: Phoebe Goldman --- 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 2b0696c8..4dcd7f43 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -172,7 +172,7 @@ These callbacks can fire in two contexts: This second case means that, even though the module only ever inserts online users, the client's `User.OnInsert` callbacks may be invoked with users who are offline. We'll only notify about online users. -`OnInsert` and `OnDelete` callbacks take two arguments: the altered row, and a `EventContext`. This will be `null` for rows inserted when initializing the cache for a subscription. The `EventContext.Event` is an enum autogenerated by `spacetime generate` with a variant for each reducer defined by the module. For now, we can ignore this argument. +`OnInsert` and `OnDelete` callbacks take two arguments: an `EventContext` and the altered row. The `EventContext.Event` is an enum which describes the event that caused the row to be inserted or deleted. All SpacetimeDB callbacks accept a context argument, which you can use in place of your top-level `DbConnection`. Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `UserNameOrIdentity` to handle this. From a794a50797d734d271d86cf5d829490e79c00633 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 15:04:11 -0800 Subject: [PATCH 08/17] Fixed capitalization. Co-authored-by: Phoebe Goldman --- 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 4dcd7f43..5ce511e7 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -337,7 +337,7 @@ void OnConnectError(Exception e) ## Disconnect callback -When Disconnecting, the callback contains the connection details and if an error occurs, it will also contain an `Exception`. If we get an error, we'll write the error to the console, if not, we'll just write that we disconnected. +When disconnecting, the callback contains the connection details and if an error occurs, it will also contain an `Exception`. If we get an error, we'll write the error to the console, if not, we'll just write that we disconnected. ```csharp void OnDisconnect(DbConnection conn, Exception? e) From 43a4499e9c851d814f4a6002fc7f5487684c7ee0 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 15:04:23 -0800 Subject: [PATCH 09/17] Fixed capitalization. Co-authored-by: Phoebe Goldman --- 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 5ce511e7..ef720575 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -377,7 +377,7 @@ Since the input loop will be blocking, we'll run our processing code in a separa 1. 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. -2. Finally, Close the connection to the module. +2. Finally, close the connection to the module. ```csharp void ProcessThread(DbConnection conn, CancellationToken ct) From 86e1b36300060d94ecd8a4d4840be3ce7449874a Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 15:05:11 -0800 Subject: [PATCH 10/17] SDK language corrected and clarified. Co-authored-by: Phoebe Goldman --- 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 ef720575..bd6610df 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -472,6 +472,6 @@ Congratulations! You've built a simple chat app using SpacetimeDB. You can find the full code for this client [in the C# SDK's examples](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/tree/master/examples~/quickstart/client). -Check out the [C# SDK Reference](/docs/sdks/c-sharp) for a more comprehensive view of the SpacetimeDB Rust SDK. +Check out the [C# client SDK Reference](/docs/sdks/c-sharp) for a more comprehensive view of the SpacetimeDB C# client SDK. If you are interested in developing in the Unity game engine, check out our [Unity Comprehensive Tutorial](/docs/unity) and [Blackholio](https://github.com/ClockworkLabs/Blackholio) game example. From ab36a00e7a4214a08d55d54b93be2141f05a5c88 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 15:06:27 -0800 Subject: [PATCH 11/17] Added that the example is for the C# client and does not include server examples. Co-authored-by: Phoebe Goldman --- 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 bd6610df..64b880be 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -470,7 +470,7 @@ dotnet run --project client Congratulations! You've built a simple chat app using SpacetimeDB. -You can find the full code for this client [in the C# SDK's examples](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/tree/master/examples~/quickstart/client). +You can find the full code for this client [in the C# client SDK's examples](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/tree/master/examples~/quickstart/client). Check out the [C# client SDK Reference](/docs/sdks/c-sharp) for a more comprehensive view of the SpacetimeDB C# client SDK. From efd8bf3ffd6b443d4759f050f989c1b6042736a2 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 15:07:37 -0800 Subject: [PATCH 12/17] Added comma for clarity Co-authored-by: Phoebe Goldman --- 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 64b880be..919865d9 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -259,7 +259,7 @@ We can also register callbacks to run each time a reducer is invoked. We registe Each reducer callback takes one fixed argument: -The `ReducerEventContext` of the callback which contains an `Event` that contains several fields. The ones we care about are: +The `ReducerEventContext` of the callback, which contains an `Event` that contains several fields. The ones we care about are: 1. The `CallerIdentity` is the `Identity` of the client that called the reducer. 2. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`. From 2433ee227f7b11524e21ec116618ac7a6f98917b Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 15:07:55 -0800 Subject: [PATCH 13/17] Added comma for clarity Co-authored-by: Phoebe Goldman --- 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 919865d9..ddf5cf36 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -261,7 +261,7 @@ Each reducer callback takes one fixed argument: The `ReducerEventContext` of the callback, which contains an `Event` that contains several fields. The ones we care about are: -1. The `CallerIdentity` is the `Identity` of the client that called the reducer. +1. The `CallerIdentity`, the `Identity` of the client that called the reducer. 2. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`. 3. The error message, if any, that the reducer returned. From 5470af9c5dd9a24c3c92efc5d9687081b60c566e Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 24 Feb 2025 15:23:01 -0800 Subject: [PATCH 14/17] Applied requested changes to improve clarity --- docs/modules/c-sharp/quickstart.md | 10 +++------- docs/sdks/c-sharp/quickstart.md | 10 +++++----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index 6d354f2e..41b289a8 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -57,14 +57,14 @@ spacetime init --lang csharp server 2. Open `server/Lib.cs`, a trivial module. 3. Clear it out, so we can write a new module that's still pretty simple: a bare-bones chat server. +To start, we'll need to add `SpacetimeDB` to our using statements. This will give us access to everything we need to author our SpacetimeDB server module. + To the top of `server/Lib.cs`, add some imports we'll be using: ```csharp using SpacetimeDB; ``` -- `SpacetimeDB` contains the special attributes we'll use to define tables and reducers in our module and 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 @@ -273,11 +273,7 @@ From the `quickstart-chat` directory, run: spacetime publish --project-path server ``` -Note: If `wasm-opt` is installed, `spacetime publish` will automatically optimize the Web Assembly output of the module. `wasm-opt` can be installed by running: - -```bash -cargo install wasm-opt -``` +Note: If the WebAssembly optimizer `wasm-opt` is installed, `spacetime publish` will automatically optimize the Web Assembly output of the published module. Instruction for installing the `wasm-opt` binary can be found in [Rust's wasm-opt documentation](https://docs.rs/wasm-opt/latest/wasm_opt/). ## Call Reducers diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md index ddf5cf36..177c27b6 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -177,7 +177,7 @@ This second case means that, even though the module only ever inserts online use 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();//[..8]; +string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()[..8]; void User_OnInsert(EventContext ctx, User insertedValue) { @@ -263,7 +263,7 @@ The `ReducerEventContext` of the callback, which contains an `Event` that contai 1. The `CallerIdentity`, the `Identity` of the client that called the reducer. 2. The `Status` of the reducer run, one of `Committed`, `Failed` or `OutOfEnergy`. -3. The error message, if any, that the reducer returned. +3. If we get a `Status.Failed`, an error message is nested inside that we'll want to write to the console. It also takes a variable amount of additional arguments that match the reducer's arguments. @@ -373,11 +373,11 @@ void OnSubscriptionApplied(SubscriptionEventContext ctx) ## Process thread -Since the input loop will be blocking, we'll run our processing code in a separate thread. This thread will: +Since the input loop will be blocking, we'll run our processing code in a separate thread. -1. 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. +This thread will loop until the thread is signaled to exit, calling the update function `FrameTick` on the `DbConnection` to process any updates received from the module, and `ProcessCommand` to process any commands received from the input loop. -2. Finally, close the connection to the module. +Afterward, close the connection to the module. ```csharp void ProcessThread(DbConnection conn, CancellationToken ct) From 7d1931d52a6fb3a5295f2f3e34200336147ed760 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Tue, 25 Feb 2025 14:29:07 -0800 Subject: [PATCH 15/17] Revised the SDK Client Quickstart to be more-in-line with the Rust Client Quickstart flow --- docs/sdks/c-sharp/quickstart.md | 169 ++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 53 deletions(-) diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md index 177c27b6..1fcc9dfc 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -28,6 +28,10 @@ Add the `SpacetimeDB.ClientSDK` [NuGet package](https://www.nuget.org/packages/s dotnet add package SpacetimeDB.ClientSDK ``` +## Clear `client/Program.cs` + +Clear out any data from `client/Program.cs` so we can write our chat client. + ## Generate your module types The `spacetime` CLI's `generate` command will generate client-side interfaces for the tables, reducers and types defined in your server module. @@ -39,15 +43,22 @@ mkdir -p client/module_bindings spacetime generate --lang csharp --out-dir client/module_bindings --project-path server ``` -Take a look inside `client/module_bindings`. The CLI should have generated five files: +Take a look inside `client/module_bindings`. The CLI should have generated three folders and nine files: ``` module_bindings -├── Message.cs -├── ReducerEvent.cs -├── SendMessageReducer.cs -├── SetNameReducer.cs -└── User.cs +├── Reducers +│ ├── ClientConnected.g.cs +│ ├── ClientDisconnected.g.cs +│ ├── SendMessage.g.cs +│ └── SetName.g.cs +├── Tables +│ ├── Message.g.cs +│ └── User.g.cs +├── Types +│ ├── Message.g.cs +│ └── User.g.cs +└── SpacetimeDBClient.g.cs ``` ## Add imports to Program.cs @@ -60,7 +71,9 @@ using SpacetimeDB.Types; using System.Collections.Concurrent; ``` -We will also need to create some global variables that will be explained when we use them later. Add the following to the top of `Program.cs`: +We will also need to create some global variables that will be explained when we use them later. + +To `Program.cs`, add: ```csharp // our local client SpacetimeDB identity @@ -75,12 +88,14 @@ var input_queue = new ConcurrentQueue<(string Command, string Args)>(); We'll work outside-in, first defining our `Main` function at a high level, then implementing each behavior it needs. We need `Main` to do several things: 1. Initialize the `AuthToken` module, which loads and stores our authentication token to/from local storage. -2. Create the `SpacetimeDBClient` instance. -3. Register callbacks on any events we want to handle. These will print to standard output messages received from the database and updates about users' names and online statuses. +2. Connect to the database. +3. Register a number of callbacks to run in response to various database events. 4. Start our processing thread which connects to the SpacetimeDB module, updates the SpacetimeDB client and processes commands that come in from the input loop running in the main thread. 5. Start the input loop, which reads commands from standard input and sends them to the processing thread. 6. When the input loop exits, stop the processing thread and wait for it to exit. +To `Program.cs`, add: + ```csharp void Main() { @@ -89,7 +104,7 @@ void Main() // Builds and connects to the database DbConnection? conn = null; conn = ConnectToDB(); - // Registers callbacks for reducers + // Registers to run in response to database events. RegisterCallbacks(conn); // Declare a threadsafe cancel token to cancel the process loop var cancellationTokenSource = new CancellationTokenSource(); @@ -108,18 +123,22 @@ void Main() Before we connect, we'll store the SpacetimeDB host name and our module name in constants `HOST` and `DB_NAME`. -Next we build our connection to the database, and while we are doing so, we can register several of our callbacks. We'll provide the connection builder the following: -1. The URI of the server running the SpacetimeDB server module. If it's running on the same computer as, we can set `HOST` to be `"http://localhost:3000"` -2. The name of the module we are looking to communicate with on the server. Replace `` with `quickstart-chat` or the name you chose when publishing your module during the module quickstart. -3. The connection builder 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. -4. We register a callback for `OnConnect`, where we will call `Subscribe` to tell the module what tables we care about. -5. We register a callback for `OnConnectError`, which will be called if there are any errors during a connection attempt. -6. We register a callback for `OnDisconnect`, which will be called when the client disconnects from the server. -7. And finally we call `Build` to build our DbConnection. +A connection to a SpacetimeDB database is represented by a `DbConnection`. We configure `DbConnection`s using the builder pattern, by calling `DbConnection.Builder()`, chaining method calls to set various connection parameters and register callbacks, then we cap it off with a call to `.Build()` to begin the connection. + +In our case, we'll supply the following options: + +1. A `WithUri` call, to specify the URI of the SpacetimeDB host where our module is running. +2. A `WithModuleName` call, to specify the name or `Identity` of our database. Make sure to pass the same name here as you supplied to `spacetime publish`. +3. A `WithToken` call, to supply a token to authenticate with. +4. An `OnConnect` callback, to run when the remote database acknowledges and accepts our connection. +5. An `OnConnectError` callback, to run if the remote database is unreachable or it rejects our connection. +6. An `OnDisconnect` callback, to run when our connection ends. + +To `Program.cs`, add: ```csharp const string HOST = "http://localhost:3000"; -const string DBNAME = ""; +const string DBNAME = "quickstart-chat"; /// Load credentials from a file and connect to the database. DbConnection ConnectToDB() @@ -137,6 +156,53 @@ DbConnection ConnectToDB() } ``` +### Save credentials + +SpacetimeDB will accept any [OpenID Connect](https://openid.net/developers/how-connect-works/) compliant [JSON Web Token](https://jwt.io/) and use it to compute an `Identity` for the user. More complex applications will generally authenticate their user somehow, generate or retrieve a token, and attach it to their connection via `WithToken`. In our case, though, we'll connect anonymously the first time, let SpacetimeDB generate a fresh `Identity` and corresponding JWT for us, and save that token locally to re-use the next time we connect. + +Once we are connected, we'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app. + +To `Program.cs`, add: + +```csharp +void OnConnected(DbConnection conn, Identity identity, string authToken) +{ + local_identity = identity; + AuthToken.SaveToken(authToken); +} +``` + +### Connect Error callback + +Should we get an error during connection, we'll be given an `Exception` which contains the details about the exception. To keep things simple, we'll just write the exception to the console. + +To `Program.cs`, add: + +```csharp +void OnConnectError(Exception e) +{ + Console.Write($"Error while connecting: {e}"); +} +``` + +### Disconnect callback + +When disconnecting, the callback contains the connection details and if an error occurs, it will also contain an `Exception`. If we get an error, we'll write the error to the console, if not, we'll just write that we disconnected. + +To `Program.cs`, add: + +```csharp +void OnDisconnect(DbConnection conn, Exception? e) +{ + if (e != null) + { + Console.Write($"Disconnected abnormally: {e}"); + } else { + Console.Write($"Disconnected normally."); + } +} +``` + ## Register callbacks Now we need to handle several sorts of events with Tables and Reducers: @@ -147,6 +213,8 @@ Now we need to handle several sorts of events with Tables and Reducers: 4. `Reducer.OnSetName`: If the server rejects our attempt to set our name, we'll print an error. 5. `Reducer.OnSendMessage`: If the server rejects a message we send, we'll print an error. +To `Program.cs`, add: + ```csharp /// Register all the callbacks our app will use to respond to database events. void RegisterCallbacks(DbConnection conn) @@ -176,6 +244,8 @@ This second case means that, even though the module only ever inserts online use Whenever we want to print a user, if they have set a name, we'll use that. If they haven't set a name, we'll instead print the first 8 bytes of their identity, encoded as hexadecimal. We'll define a function `UserNameOrIdentity` to handle this. +To `Program.cs`, add: + ```csharp string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()[..8]; @@ -202,6 +272,8 @@ In our module, users can be updated for three reasons: We'll print an appropriate message in each of these cases. +To `Program.cs`, add: + ```csharp void User_OnUpdate(EventContext ctx, User oldValue, User newValue) { @@ -231,6 +303,8 @@ To find the `User` based on the message's `Sender` identity, we'll use `User.Ide We'll print the user's name or identity in the same way as we did when notifying about `User` table events, but here we have to handle the case where we don't find a matching `User` row. This can happen when the module owner sends a message using the CLI's `spacetime call`. In this case, we'll print `unknown`. +To `Program.cs`, add: + ```csharp void PrintMessage(RemoteTables tables, Message message) { @@ -278,6 +352,8 @@ We already handle successful `SetName` invocations using our `User.OnUpdate` cal We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes. +To `Program.cs`, add: + ```csharp void Reducer_OnSetNameEvent(ReducerEventContext ctx, string name) { @@ -293,6 +369,8 @@ void Reducer_OnSetNameEvent(ReducerEventContext ctx, string name) We handle warnings on rejected messages the same way as rejected names, though the types and the error message are different. +To `Program.cs`, add: + ```csharp void Reducer_OnSendMessageEvent(ReducerEventContext ctx, string text) { @@ -304,11 +382,17 @@ void Reducer_OnSendMessageEvent(ReducerEventContext ctx, string text) } ``` -## Connect callback +## Subscribe to queries -Once we are connected, we'll use the `AuthToken` module to save our token to local storage, so that we can re-authenticate as the same user the next time we connect. We'll also store the identity in a global variable `local_identity` so that we can use it to check if we are the sender of a message or name change. This callback also notifies us of our client's `Address`, an opaque identifier SpacetimeDB modules can use to distinguish connections by the same `Identity`, but we won't use it in our app. +SpacetimeDB is set up so that each client subscribes via SQL queries to some subset of the database, and is notified about changes only to that subset. For complex apps with large databases, judicious subscriptions can save each client significant network bandwidth, memory and computation. For example, in [BitCraft](https://bitcraftonline.com), each player's client subscribes only to the entities in the "chunk" of the world where that player currently resides, rather than the entire game world. Our app is much simpler than BitCraft, so we'll just subscribe to the whole database using `SubscribeToAllTables`. + +You can also subscribe to specific tables using SQL syntax, e.g. `SELECT * FROM my_table`. Our [SQL documentation](/docs/sql) enumerates the operations that are accepted in our SQL syntax. -Then 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 using `SubscribeToAllTables`. +When we specify our subscriptions, we can supply an `OnApplied` callback. This will run when the subscription is applied and the matching rows become available in our client cache. We'll use this opportunity to print the message backlog in proper order. + +We can also provide an `OnError` callback. This will run if the subscription fails, usually due to an invalid or malformed SQL queries. We can't handle this case, so we'll just print out the error and exit the process. + +In `Program.cs`, update our `OnConnected` function to include `conn.SubscriptionBuilder().OnApplied(OnSubscriptionApplied).SubscribeToAllTables();` so that it reads: ```csharp void OnConnected(DbConnection conn, Identity identity, string authToken) @@ -322,39 +406,12 @@ void OnConnected(DbConnection conn, Identity identity, string authToken) } ``` -You can also subscribe to specific tables using SQL syntax, e.g. `SELECT * FROM my_table`. Our [SQL documentation](/docs/sql) enumerates the operations that are accepted in our SQL syntax. - -## Connect Error callback - -Should we get an error during connection, we'll be given an `Exception` which contains the details about the exception. To keep things simple, we'll just write the exception to the console. - -```csharp -void OnConnectError(Exception e) -{ - Console.Write($"Error while connecting: {e}"); -} -``` - -## Disconnect callback - -When disconnecting, the callback contains the connection details and if an error occurs, it will also contain an `Exception`. If we get an error, we'll write the error to the console, if not, we'll just write that we disconnected. - -```csharp -void OnDisconnect(DbConnection conn, Exception? e) -{ - if (e != null) - { - Console.Write($"Disconnected abnormally: {e}"); - } else { - Console.Write($"Disconnected normally."); - } -} -``` - ## 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. +To `Program.cs`, add: + ```csharp void PrintMessagesInOrder(RemoteTables tables) { @@ -379,6 +436,8 @@ This thread will loop until the thread is signaled to exit, calling the update f Afterward, close the connection to the module. +To `Program.cs`, add: + ```csharp void ProcessThread(DbConnection conn, CancellationToken ct) { @@ -401,7 +460,7 @@ void ProcessThread(DbConnection conn, CancellationToken ct) } ``` -## Input loop and ProcessCommands +## Handle user input The input loop will read commands from standard input and send them to the processing thread using the input queue. The `ProcessCommands` function is called every 100ms by the processing thread to process any pending commands. @@ -411,6 +470,8 @@ Supported Commands: 2. Set name: `name`, will send the new name to the module by calling `Reducer.SetName` which is automatically generated by `spacetime generate`. +To `Program.cs`, add: + ```csharp void InputLoop() { @@ -454,7 +515,9 @@ void ProcessCommands(RemoteReducers reducers) ## Run the client -Finally we just need to add a call to `Main` in `Program.cs`: +Finally, we just need to add a call to `Main`. + +To `Program.cs`, add: ```csharp Main(); From 549181f3d8aa074b2d9e44335bf6353b9988d0ef Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 26 Feb 2025 08:48:44 -0800 Subject: [PATCH 16/17] Added comments to code --- docs/sdks/c-sharp/quickstart.md | 48 ++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/docs/sdks/c-sharp/quickstart.md b/docs/sdks/c-sharp/quickstart.md index 1fcc9dfc..759accbe 100644 --- a/docs/sdks/c-sharp/quickstart.md +++ b/docs/sdks/c-sharp/quickstart.md @@ -99,8 +99,8 @@ To `Program.cs`, add: ```csharp void Main() { + // Initialize the `AuthToken` module AuthToken.Init(".spacetime_csharp_quickstart"); - // Builds and connects to the database DbConnection? conn = null; conn = ConnectToDB(); @@ -137,7 +137,10 @@ In our case, we'll supply the following options: To `Program.cs`, add: ```csharp +/// The URI of the SpacetimeDB instance hosting our chat module. const string HOST = "http://localhost:3000"; + +/// The module name we chose when we published our module. const string DBNAME = "quickstart-chat"; /// Load credentials from a file and connect to the database. @@ -165,6 +168,7 @@ Once we are connected, we'll use the `AuthToken` module to save our token to loc To `Program.cs`, add: ```csharp +/// Our `OnConnected` callback: save our credentials to a file. void OnConnected(DbConnection conn, Identity identity, string authToken) { local_identity = identity; @@ -179,6 +183,7 @@ Should we get an error during connection, we'll be given an `Exception` which co To `Program.cs`, add: ```csharp +/// Our `OnConnectError` callback: print the error, then exit the process. void OnConnectError(Exception e) { Console.Write($"Error while connecting: {e}"); @@ -192,6 +197,7 @@ When disconnecting, the callback contains the connection details and if an error To `Program.cs`, add: ```csharp +/// Our `OnDisconnect` callback: print a note, then exit the process. void OnDisconnect(DbConnection conn, Exception? e) { if (e != null) @@ -247,8 +253,10 @@ Whenever we want to print a user, if they have set a name, we'll use that. If th To `Program.cs`, add: ```csharp +/// If the user has no set name, use the first 8 characters from their identity. string UserNameOrIdentity(User user) => user.Name ?? user.Identity.ToString()[..8]; +/// Our `User.OnInsert` callback: if the user is online, print a notification. void User_OnInsert(EventContext ctx, User insertedValue) { if (insertedValue.Online) @@ -275,6 +283,8 @@ We'll print an appropriate message in each of these cases. To `Program.cs`, add: ```csharp +/// Our `User.OnUpdate` callback: +/// print a notification about name and status changes. void User_OnUpdate(EventContext ctx, User oldValue, User newValue) { if (oldValue.Name != newValue.Name) @@ -306,6 +316,15 @@ We'll print the user's name or identity in the same way as we did when notifying To `Program.cs`, add: ```csharp +/// Our `Message.OnInsert` callback: print new messages. +void Message_OnInsert(EventContext ctx, Message insertedValue) +{ + if (ctx.Event is not Event.SubscribeApplied) + { + PrintMessage(ctx.Db, insertedValue); + } +} + void PrintMessage(RemoteTables tables, Message message) { var sender = tables.User.Identity.Find(message.Sender); @@ -317,14 +336,6 @@ void PrintMessage(RemoteTables tables, Message message) Console.WriteLine($"{senderName}: {message.Text}"); } - -void Message_OnInsert(EventContext ctx, Message insertedValue) -{ - if (ctx.Event is not Event.SubscribeApplied) - { - PrintMessage(ctx.Db, insertedValue); - } -} ``` ### Warn if our name was rejected @@ -355,6 +366,7 @@ We'll test both that our identity matches the sender and that the status is `Fai To `Program.cs`, add: ```csharp +/// Our `OnSetNameEvent` callback: print a warning if the reducer failed. void Reducer_OnSetNameEvent(ReducerEventContext ctx, string name) { var e = ctx.Event; @@ -372,6 +384,7 @@ We handle warnings on rejected messages the same way as rejected names, though t To `Program.cs`, add: ```csharp +/// Our `OnSendMessageEvent` callback: print a warning if the reducer failed. void Reducer_OnSendMessageEvent(ReducerEventContext ctx, string text) { var e = ctx.Event; @@ -395,6 +408,7 @@ We can also provide an `OnError` callback. This will run if the subscription fai In `Program.cs`, update our `OnConnected` function to include `conn.SubscriptionBuilder().OnApplied(OnSubscriptionApplied).SubscribeToAllTables();` so that it reads: ```csharp +/// Our `OnConnect` callback: save our credentials to a file. void OnConnected(DbConnection conn, Identity identity, string authToken) { local_identity = identity; @@ -413,6 +427,14 @@ Once our subscription is applied, we'll print all the previously sent messages. To `Program.cs`, add: ```csharp +/// Our `OnSubscriptionApplied` callback: +/// sort all past messages and print them in timestamp order. +void OnSubscriptionApplied(SubscriptionEventContext ctx) +{ + Console.WriteLine("Connected"); + PrintMessagesInOrder(ctx.Db); +} + void PrintMessagesInOrder(RemoteTables tables) { foreach (Message message in tables.Message.Iter().OrderBy(item => item.Sent)) @@ -420,12 +442,6 @@ void PrintMessagesInOrder(RemoteTables tables) PrintMessage(tables, message); } } - -void OnSubscriptionApplied(SubscriptionEventContext ctx) -{ - Console.WriteLine("Connected"); - PrintMessagesInOrder(ctx.Db); -} ``` ## Process thread @@ -439,6 +455,7 @@ Afterward, close the connection to the module. To `Program.cs`, add: ```csharp +/// Our separate thread from main, where we can call process updates and process commands without blocking the main thread. void ProcessThread(DbConnection conn, CancellationToken ct) { try @@ -473,6 +490,7 @@ Supported Commands: To `Program.cs`, add: ```csharp +/// Read each line of standard input, and either set our name or send a message as appropriate. void InputLoop() { while (true) From cdbe1f32a6844185bcfc68d0592ae5a36e677905 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 26 Feb 2025 09:47:41 -0800 Subject: [PATCH 17/17] Replaced with quickstart-chat --- docs/modules/c-sharp/quickstart.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index 41b289a8..5dcb703a 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -265,12 +265,12 @@ If you haven't already started the SpacetimeDB server, run the `spacetime start` ## Publish the module -And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more pleasant. Come up with a unique name, and fill it in where we've written ``. +And that's all of our module code! We'll run `spacetime publish` to compile our module and publish it on SpacetimeDB. `spacetime publish` takes an optional name which will map to the database's unique address. Clients can connect either by name or by address, but names are much more pleasant. In this example, we'll be using `quickstart-chat`. Feel free to come up with a unique name, and in the CLI commands, replace where we've written `quickstart-chat` with the name you chose. From the `quickstart-chat` directory, run: ```bash -spacetime publish --project-path server +spacetime publish --project-path server quickstart-chat ``` Note: If the WebAssembly optimizer `wasm-opt` is installed, `spacetime publish` will automatically optimize the Web Assembly output of the published module. Instruction for installing the `wasm-opt` binary can be found in [Rust's wasm-opt documentation](https://docs.rs/wasm-opt/latest/wasm_opt/). @@ -280,13 +280,13 @@ Note: If the WebAssembly optimizer `wasm-opt` is installed, `spacetime publish` You can use the CLI (command line interface) to run reducers. The arguments to the reducer are passed in JSON format. ```bash -spacetime call SendMessage "Hello, World!" +spacetime call quickstart-chat SendMessage "Hello, World!" ``` Once we've called our `SendMessage` reducer, we can check to make sure it ran by running the `logs` command. ```bash -spacetime logs +spacetime logs quickstart-chat ``` You should now see the output that your module printed in the database. @@ -300,7 +300,7 @@ info: Hello, World! SpacetimeDB supports a subset of the SQL syntax so that you can easily query the data of your database. We can run a query using the `sql` command. ```bash -spacetime sql "SELECT * FROM Message" +spacetime sql quickstart-chat "SELECT * FROM Message" ``` ```bash