mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-17 21:22:29 -04:00
ecc00cae40
# Description of Changes
Extends log records with a new field, `function`, which stores the name
of the reducer being executed when the log was produced. I have chosen
to name this field `function` rather than `reducer` because we will soon
be adding procedures, which are not reducers but will also be valid
values for this field.
While making this change, I noticed inconsistent values for injected
logs. Previously, we injected logs in three places, with different
values for the record fields:
1. `SystemLogger` (used when publishing and updating) set `filename:
Some("spacetimedb")` and `target: None`.
2. `log_reducer_error` (used for reducer error returns) set `filename:
None` and `target: Some(reducer)`, with `reducer` being the name of the
reducer.
3. `ModuleHost::inject_logs` (used for calls to nonexistent reducers and
calls with ill-typed arguments) set `filename: Some("external")` and
`target: None`.
With this change, I have decided that injected logs universally have
`filename: Some("__spacetimedb__")` and `target:
Some("__spacetimedb__")`. I have chosen the double-underscore convention
for reserved names to avoid confusion should a user name a source file
or reducer `spacetimedb`. I am not terribly attached to using
`spacetimedb` here, and would be happy to change to some other string,
like `internal` or `system`.
I have further decided that `log_reducer_error` and `inject_logs` both
have access to a sensible non-`None` value for `function` and so should
set it to the name of the reducer. The `target` field is not used for
this. Note that in cases where a client attempts to call a non-existent
reducer, the `function` field of the log record will be set to that
client-supplied non-existent reducer name.
# API and ABI breaking changes
Changes our log record format. This is at least backwards-compatible (as
in, newer consumers can read older logs) because the new field is
optional. I am unsure if it is forwards-compatible (as in, older
consumers reading newer logs) because I don't know if `serde_json` will
ignore unknown fields or will error.
# Expected complexity level and risk
1
# Testing
- [x] Manually ran `quickstart-chat` (modified to log messages and name
changes) and saw sensible output.
95 lines
2.8 KiB
Rust
95 lines
2.8 KiB
Rust
use spacetimedb::{Identity, ReducerContext, Table, Timestamp};
|
|
|
|
#[spacetimedb::table(name = user, public)]
|
|
pub struct User {
|
|
#[primary_key]
|
|
identity: Identity,
|
|
name: Option<String>,
|
|
online: bool,
|
|
}
|
|
|
|
#[spacetimedb::table(name = message, public)]
|
|
pub struct Message {
|
|
sender: Identity,
|
|
sent: Timestamp,
|
|
text: String,
|
|
}
|
|
|
|
fn validate_name(name: String) -> Result<String, String> {
|
|
if name.is_empty() {
|
|
Err("Names must not be empty".to_string())
|
|
} else {
|
|
Ok(name)
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::reducer]
|
|
pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
|
|
let name = validate_name(name)?;
|
|
if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
|
|
log::info!("User {} sets name to {name}", ctx.sender);
|
|
ctx.db.user().identity().update(User {
|
|
name: Some(name),
|
|
..user
|
|
});
|
|
Ok(())
|
|
} else {
|
|
Err("Cannot set name for unknown user".to_string())
|
|
}
|
|
}
|
|
|
|
fn validate_message(text: String) -> Result<String, String> {
|
|
if text.is_empty() {
|
|
Err("Messages must not be empty".to_string())
|
|
} else {
|
|
Ok(text)
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::reducer]
|
|
pub fn send_message(ctx: &ReducerContext, text: String) -> Result<(), String> {
|
|
// Things to consider:
|
|
// - Rate-limit messages per-user.
|
|
// - Reject messages from unnamed user.
|
|
let text = validate_message(text)?;
|
|
log::info!("User {}: {text}", ctx.sender);
|
|
ctx.db.message().insert(Message {
|
|
sender: ctx.sender,
|
|
text,
|
|
sent: ctx.timestamp,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
#[spacetimedb::reducer(init)]
|
|
// Called when the module is initially published
|
|
pub fn init(_ctx: &ReducerContext) {}
|
|
|
|
#[spacetimedb::reducer(client_connected)]
|
|
pub fn identity_connected(ctx: &ReducerContext) {
|
|
if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
|
|
// If this is a returning user, i.e. we already have a `User` with this `Identity`,
|
|
// set `online: true`, but leave `name` and `identity` unchanged.
|
|
ctx.db.user().identity().update(User { online: true, ..user });
|
|
} else {
|
|
// If this is a new user, create a `User` row for the `Identity`,
|
|
// which is online, but hasn't set a name.
|
|
ctx.db.user().insert(User {
|
|
name: None,
|
|
identity: ctx.sender,
|
|
online: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::reducer(client_disconnected)]
|
|
pub fn identity_disconnected(ctx: &ReducerContext) {
|
|
if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
|
|
ctx.db.user().identity().update(User { online: false, ..user });
|
|
} else {
|
|
// This branch should be unreachable,
|
|
// as it doesn't make sense for a client to disconnect without connecting first.
|
|
log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender);
|
|
}
|
|
}
|