Skip to content

Commit

Permalink
Add quickstart-chat example module and client (#116)
Browse files Browse the repository at this point in the history
This commit adds the `quickstart-chat` Rust module and client to the appropriate examples dirs.
  • Loading branch information
gefjon authored Jul 31, 2023
1 parent 333bbc3 commit 1e7cf1e
Show file tree
Hide file tree
Showing 14 changed files with 659 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ members = [
"modules/rust-wasm-test",
"modules/benchmarks",
"modules/spacetimedb-quickstart",
"modules/quickstart-chat",
]
default-members = ["crates/cli"]

Expand Down
4 changes: 4 additions & 0 deletions crates/sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ im = "15.1"
base64 = "0.21"
home = "0.5"
lazy_static = "1.4"

[dev-dependencies]
# for quickstart-chat example
hex = "0.4"
5 changes: 5 additions & 0 deletions crates/sdk/examples/quickstart-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# `quickstart-chat`

A simple command-line client for [the `quickstart-chat` module](/modules/quickstart-chat).

This client is described in-depth by [the SpacetimeDB Rust client quickstart](https://spacetimedb.com/docs/client-languages/rust/rust-sdk-quickstart-guide).
182 changes: 182 additions & 0 deletions crates/sdk/examples/quickstart-chat/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
mod module_bindings;
use module_bindings::*;

use spacetimedb_sdk::{
identity::{load_credentials, once_on_connect, save_credentials, Credentials, Identity},
on_subscription_applied,
reducer::Status,
subscribe,
table::{TableType, TableWithPrimaryKey},
};

// # Our main function

fn main() {
register_callbacks();
connect_to_db();
subscribe_to_tables();
user_input_loop();
}

// # Register callbacks

/// Register all the callbacks our app will use to respond to database events.
fn register_callbacks() {
// When we receive our `Credentials`, save them to a file.
once_on_connect(on_connected);

// When a new user joins, print a notification.
User::on_insert(on_user_inserted);

// When a user's status changes, print a notification.
User::on_update(on_user_updated);

// When a new message is received, print it.
Message::on_insert(on_message_inserted);

// When we receive the message backlog, print it in timestamp order.
on_subscription_applied(on_sub_applied);

// When we fail to set our name, print a warning.
on_set_name(on_name_set);

// When we fail to send a message, print a warning.
on_send_message(on_message_sent);
}

// ## Save credentials to a file

/// Our `on_connect` callback: save our credentials to a file.
fn on_connected(creds: &Credentials) {
if let Err(e) = save_credentials(CREDS_DIR, creds) {
eprintln!("Failed to save credentials: {:?}", e);
}
}

const CREDS_DIR: &str = ".spacetime_chat";

// ## Notify about new users

/// Our `User::on_insert` callback: if the user is online, print a notification.
fn on_user_inserted(user: &User, _: Option<&ReducerEvent>) {
if user.online {
println!("User {} connected.", user_name_or_identity(user));
}
}

fn user_name_or_identity(user: &User) -> String {
user.name
.clone()
.unwrap_or_else(|| identity_leading_hex(&user.identity))
}

fn identity_leading_hex(id: &Identity) -> String {
hex::encode(&id.bytes()[0..8])
}

// ## Notify about updated users

/// Our `User::on_update` callback:
/// print a notification about name and status changes.
fn on_user_updated(old: &User, new: &User, _: Option<&ReducerEvent>) {
if old.name != new.name {
println!(
"User {} renamed to {}.",
user_name_or_identity(old),
user_name_or_identity(new)
);
}
if old.online && !new.online {
println!("User {} disconnected.", user_name_or_identity(new));
}
if !old.online && new.online {
println!("User {} connected.", user_name_or_identity(new));
}
}

// ## Display incoming messages

/// Our `Message::on_insert` callback: print new messages.
fn on_message_inserted(message: &Message, reducer_event: Option<&ReducerEvent>) {
if reducer_event.is_some() {
print_message(message);
}
}

fn print_message(message: &Message) {
let sender = User::filter_by_identity(message.sender.clone())
.map(|u| user_name_or_identity(&u))
.unwrap_or_else(|| "unknown".to_string());
println!("{}: {}", sender, message.text);
}

// ## Print message backlog

/// Our `on_subscription_applied` callback:
/// sort all past messages and print them in timestamp order.
fn on_sub_applied() {
let mut messages = Message::iter().collect::<Vec<_>>();
messages.sort_by_key(|m| m.sent);
for message in messages {
print_message(&message);
}
}

// ## Warn if set_name failed

/// Our `on_set_name` callback: print a warning if the reducer failed.
fn on_name_set(_sender: &Identity, status: &Status, name: &String) {
if let Status::Failed(err) = status {
eprintln!("Failed to change name to {:?}: {}", name, err);
}
}

// ## Warn if a message was rejected

/// Our `on_send_message` callback: print a warning if the reducer failed.
fn on_message_sent(_sender: &Identity, status: &Status, text: &String) {
if let Status::Failed(err) = status {
eprintln!("Failed to send message {:?}: {}", text, err);
}
}

// # Connect to the database

/// The URL of the SpacetimeDB instance hosting our chat module.
const HOST: &str = "http://localhost:3000";

/// The module name we chose when we published our module.
const DB_NAME: &str = "chat";

/// Load credentials from a file and connect to the database.
fn connect_to_db() {
connect(
HOST,
DB_NAME,
load_credentials(CREDS_DIR).expect("Error reading stored credentials"),
)
.expect("Failed to connect");
}

// # Subscribe to queries

/// Register subscriptions for all rows of both tables.
fn subscribe_to_tables() {
subscribe(&["SELECT * FROM User;", "SELECT * FROM Message;"]).unwrap();
}

// # Handle user input

/// Read each line of standard input, and either set our name or send a message as appropriate.
fn user_input_loop() {
for line in std::io::stdin().lines() {
let Ok(line) = line else {
panic!("Failed to read from stdin.");
};
if let Some(name) = line.strip_prefix("/name ") {
set_name(name.to_string());
} else {
send_message(line);
}
}
}
39 changes: 39 additions & 0 deletions crates/sdk/examples/quickstart-chat/module_bindings/message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD.

#[allow(unused)]
use spacetimedb_sdk::{
anyhow::{anyhow, Result},
identity::Identity,
reducer::{Reducer, ReducerCallbackId, Status},
sats::{de::Deserialize, ser::Serialize},
spacetimedb_lib,
table::{TableIter, TableType, TableWithPrimaryKey},
};

#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct Message {
pub sender: Identity,
pub sent: u64,
pub text: String,
}

impl TableType for Message {
const TABLE_NAME: &'static str = "Message";
type ReducerEvent = super::ReducerEvent;
}

impl Message {
#[allow(unused)]
pub fn filter_by_sender(sender: Identity) -> TableIter<Self> {
Self::filter(|row| row.sender == sender)
}
#[allow(unused)]
pub fn filter_by_sent(sent: u64) -> TableIter<Self> {
Self::filter(|row| row.sent == sent)
}
#[allow(unused)]
pub fn filter_by_text(text: String) -> TableIter<Self> {
Self::filter(|row| row.text == text)
}
}
123 changes: 123 additions & 0 deletions crates/sdk/examples/quickstart-chat/module_bindings/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD.

use spacetimedb_sdk::callbacks::{DbCallbacks, ReducerCallbacks};
use spacetimedb_sdk::client_api_messages::{Event, TableUpdate};
use spacetimedb_sdk::client_cache::{ClientCache, RowCallbackReminders};
use spacetimedb_sdk::global_connection::with_connection_mut;
use spacetimedb_sdk::identity::Credentials;
use spacetimedb_sdk::reducer::AnyReducerEvent;
#[allow(unused)]
use spacetimedb_sdk::{
anyhow::{anyhow, Result},
identity::Identity,
reducer::{Reducer, ReducerCallbackId, Status},
sats::{de::Deserialize, ser::Serialize},
spacetimedb_lib,
table::{TableIter, TableType, TableWithPrimaryKey},
};
use std::sync::Arc;

pub mod message;
pub mod send_message_reducer;
pub mod set_name_reducer;
pub mod user;

pub use message::*;
pub use send_message_reducer::*;
pub use set_name_reducer::*;
pub use user::*;

#[allow(unused)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub enum ReducerEvent {
SendMessage(send_message_reducer::SendMessageArgs),
SetName(set_name_reducer::SetNameArgs),
}

#[allow(unused)]
fn handle_table_update(
table_update: TableUpdate,
client_cache: &mut ClientCache,
callbacks: &mut RowCallbackReminders,
) {
let table_name = &table_update.table_name[..];
match table_name {
"Message" => client_cache.handle_table_update_no_primary_key::<message::Message>(callbacks, table_update),
"User" => client_cache.handle_table_update_with_primary_key::<user::User>(callbacks, table_update),
_ => spacetimedb_sdk::log::error!("TableRowOperation on unknown table {:?}", table_name),
}
}

#[allow(unused)]
fn invoke_row_callbacks(
reminders: &mut RowCallbackReminders,
worker: &mut DbCallbacks,
reducer_event: Option<Arc<AnyReducerEvent>>,
state: &Arc<ClientCache>,
) {
reminders.invoke_callbacks::<message::Message>(worker, &reducer_event, state);
reminders.invoke_callbacks::<user::User>(worker, &reducer_event, state);
}

#[allow(unused)]
fn handle_resubscribe(new_subs: TableUpdate, client_cache: &mut ClientCache, callbacks: &mut RowCallbackReminders) {
let table_name = &new_subs.table_name[..];
match table_name {
"Message" => client_cache.handle_resubscribe_for_type::<message::Message>(callbacks, new_subs),
"User" => client_cache.handle_resubscribe_for_type::<user::User>(callbacks, new_subs),
_ => spacetimedb_sdk::log::error!("TableRowOperation on unknown table {:?}", table_name),
}
}

#[allow(unused)]
fn handle_event(
event: Event,
reducer_callbacks: &mut ReducerCallbacks,
state: Arc<ClientCache>,
) -> Option<Arc<AnyReducerEvent>> {
let Some(function_call) = &event.function_call else {
spacetimedb_sdk::log::warn!("Received Event with None function_call"); return None;
};
match &function_call.reducer[..] {
"send_message" => reducer_callbacks
.handle_event_of_type::<send_message_reducer::SendMessageArgs, ReducerEvent>(
event,
state,
ReducerEvent::SendMessage,
),
"set_name" => reducer_callbacks.handle_event_of_type::<set_name_reducer::SetNameArgs, ReducerEvent>(
event,
state,
ReducerEvent::SetName,
),
unknown => {
spacetimedb_sdk::log::error!("Event on an unknown reducer: {:?}", unknown);
None
}
}
}

/// Connect to a database named `db_name` accessible over the internet at the URI `spacetimedb_uri`.
///
/// If `credentials` are supplied, they will be passed to the new connection to
/// identify and authenticate the user. Otherwise, a set of `Credentials` will be
/// generated by the server.
pub fn connect<IntoUri>(spacetimedb_uri: IntoUri, db_name: &str, credentials: Option<Credentials>) -> Result<()>
where
IntoUri: TryInto<spacetimedb_sdk::http::Uri>,
<IntoUri as TryInto<spacetimedb_sdk::http::Uri>>::Error: std::error::Error + Send + Sync + 'static,
{
with_connection_mut(|connection| {
connection.connect(
spacetimedb_uri,
db_name,
credentials,
handle_table_update,
handle_resubscribe,
invoke_row_callbacks,
handle_event,
)?;
Ok(())
})
}
Loading

0 comments on commit 1e7cf1e

Please sign in to comment.