-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
quickstart-chat
example module and client (#116)
This commit adds the `quickstart-chat` Rust module and client to the appropriate examples dirs.
- Loading branch information
Showing
14 changed files
with
659 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
39
crates/sdk/examples/quickstart-chat/module_bindings/message.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
123
crates/sdk/examples/quickstart-chat/module_bindings/mod.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
}) | ||
} |
Oops, something went wrong.