The Chat example:
use moon::*;
use shared::{UpMsg, DownMsg, Message};
async fn init() {}
async fn frontend() -> Frontend {
Frontend::new().title("Chat example")
}
async fn up_msg_handler(req: UpMsgRequest) {
if let UpMsg::SendMessage(message) = req.up_msg {
join_all(connected_client::by_id().iter().map(|(_, client)| {
client.send_down_msg(message, req.cor_id)
})).await
}
}
fn main() {
start!(init, frontend, up_msg_handler);
}
- The function
main
is invoked automatically when the Moon app is started on the server. - The function
init
is invoked when the Moon is ready to work. - The function
frontend
is invoked when the web browser wants to download and run the client (Zoon) app. - The function
up_msg_handler
handles requests from the Zoon.
- We need to find the needed actors. They are stored in indices.
connected_client::by_id
is a Moon's system index where each value is an actor representing a connected Zoon app. - All public actor functions are asynchronous so we have to
await
them an ideally call them all at once in this example to improve the performance a bit.- Note: The requested actor may live in another server or it doesn't live at all - then the Moon has to start it and load its state into the main memory before it can process your call. And all those operations and the business logic processing take some time, so asynchronicity allows you to spend the time in better ways than just waiting.
We'll use the Time Tracker example parts to demonstrate how to define an actor and create its instances.
-
Each actor should be placed in a standalone module / file - e.g.
backend/src/invoice.rs
. -
Let's add the "actor skeleton" into the file (we'll talk about individual parts later):
use shared::{InvoiceId, TimeBlockId}; actor!{ #[args] struct InvoiceArgs { time_block: TimeBlockId, id: InvoiceId, } // ------ Indices ------ // ------ PVars ------ // ------ Actor ------ #[actor] struct InvoiceActor; impl InvoiceActor { } }
-
We need to register the actor:
fn main() { start!(init, frontend, up_msg_handler, actors![ client, invoice, ... ]); }
-
And we can already create actor instances as needed:
use invoice::{self, InvoiceArgs}; ... async fn up_msg_handler(req: UpMsgRequest) { let down_msg = match req.up_msg { ... // ------ Invoice ------ UpMsg::AddInvoice(time_block, id) => { check_access!(req); new_actor(InvoiceArgs { time_block, id }).await; DownMsg::InvoiceAdded }, ... } }
-
new_actor
is the Moon's async function. In this case, it creates a newinvoice
actor and returnsInvoiceActor
. -
InvoiceActor
represents a copyable reference to theinvoice
actor instance. The instance is either active (loaded in the main memory) or only stored in a persistent storage.
-
-
Let's add
PVar
s to our actor:// ------ PVars ------ #[p_var] fn id() -> PVar<InvoiceId> { p_var("id", |_| args().map(|args| args.id)) } #[p_var] fn custom_id() -> PVar<String> { p_var("custom_id", |_| String::new()) } #[p_var] fn url() -> PVar<String> { p_var("url", |_| String::new()) } #[p_var] fn time_block() -> PVar<ClientId> { p_var("time_block", |_| args().map(|args| args.time_block)) }
-
PVar
is a Persistent Variable. It represents a copyable reference to the Moon's internal persistent storage. (The file system and PostgreSQL would be probably the first supported storages.) -
Most
PVar
's methods are async because they may need to communicate with the storage. Read operations are fast when the actor is active and the neededPVar
value has been cached in memory by Moon. -
The Moon's function
p_var
loads the value according to the identifier from the storage and deserializes it to the required Rust type. Or it creates a new record in the storage with the serialized default value provided by its second argument. -
The first
p_var
's parameter is the identifier - e.g."url"
. The identifier should be unique among other actor'sPVar
s. -
The second
p_var
's parameter is a callback that is invoked when:- A new record will be created. Then the callback's only argument is
None
. - The deserialization fails. Then the callback's argument contains
PVarError
with the serialized old value. It allows you to convert the data to a new type.
- A new record will be created. Then the callback's only argument is
-
The Moon's function
args
returns a wrapper for anInvoiceArgs
instance.
-
-
Now we can define accessors and other helpers for
InvoiceActor
:// ------ Actor ------ #[actor] struct InvoiceActor; impl InvoiceActor { async fn remove(&self) { self.remove_actor().await } async fn set_custom_id(&self, custom_id: String) { custom_id().set(custom_id).await } async fn set_url(&self, url: String) { url().set(url).await } async fn id(&self) -> InvoiceId { id().inner().await } async fn custom_id(&self) -> String { custom_id().inner().await } async fn url(&self) -> String { url().inner().await } }
- The
InvoiceActor
struct implements also some predefined methods. E.g.remove_actor()
allows you to remove the instance and delete the associated data.
- The
-
And the last part - indices:
// ------ Indices ------ #[index] fn by_id() -> Index<InvoiceId, InvoiceActor> { index("invoice_by_id", |_| id()) } #[index] fn by_time_block() -> Index<TimeBlockId, InvoiceActor> { index("invoice_by_time_block", |_| time_block()) }
-
Indices allow us to get actor instance references (e.g.
InvoiceActor
). They are basically key-value stores, where the value is an array of actor references. Example (an API draft):invoice::by_time_block().actors(time_block_id).first();
-
The Moon's function
index
returns the requested index referenceIndex
by the identifier. Or it creates a new index in the persistent storage with the serialized key provided by its second argument. -
The first
index
's parameter is identifier - e.g."invoice_by_id"
. It should be unique among all indices. -
The second
index
's parameter is a callback that is invoked when:- A new index will be created. Then the callback's only argument is
None
. - Serialized old keys have different type than the required one. Then the callback's argument contains
IndexError
. It allows you to convert the keys to a new type.
- A new index will be created. Then the callback's only argument is
-
The callback provided in the second
index
's argument has to returnPVar
. Index keys will be automatically synchronized with the associatedPVar
value.
-
Authentication and Authorization are:
- Probably the most reinvented wheels.
- Very difficult to implement without introducing security holes.
- Painful to integrate into your product because of regulations like GDPR or Cookie Law.
- Annoying for your users.
_
Defining the basic auth behavior is really a tough task:
-
Should we use LocalStorage or Cookies to store tokens? (I'm sure you can find many articles and people which claim that only LocalStorage or only Cookies is the right choice).
- I would generally prefer LocalStorage because it's much easier to work with and once the XSS attack is successful, not even correctly set Cookies can save you. But with MoonZoon, both client and server will be probably attached to the same domain so we can take full advantage of cookie security features. So it depends.
-
Can we assume all apps communicate over HTTPS?
-
There are also ways to register and log in without sending passwords to the server (see Seed's E2E encryption example for more info.) Do we need it?
-
Is E2E encryption required?
-
We need to take into account legal requirements:
- Do we need user's email? Is user's nick a real name? It has to be compatible with GDPR and we need to mention it in our Terms and Policies and other documents.
- The user has to agree with the usage of their data before the registration and he has to be able to delete the data.
- etc.
-
What about account recovery, log in on multiple devices, testing, ...
_
Also there are some passwordless auth methods:
- Through SMS or email.
- WebAuthn. It could be a good way to eliminate passwords and usernames.
- https://webauthn.guide/
- https://webauthn.io/
- https://webauthn.me/
- https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API
- https://www.w3.org/TR/webauthn/
- https://github.com/herrjemand/awesome-webauthn
- https://crates.io/crates/webauthn-rs + related articles on https://fy.blackhats.net.au
- https://medium.com/webauthnworks/introduction-to-webauthn-api-5fd1fb46c285
- https://sec.okta.com/articles/2020/04/webauthn-great-and-it-sucks
- https://levelup.gitconnected.com/what-is-webauthn-logging-in-with-touch-id-and-windows-hello-on-the-web-c886b58f99c3
_
So let's wait a bit until we see clear requirements and how technologies like WebAuthn work in practice before we try to design special auth libraries and finalize MoonZoon API.
MoonZoon will allow you from the beginning to send a token in the request's header so you have at least a basic foundation to integrate, for instance, JWT auth.
Your opinions on the chat are very welcome!
Other related articles:
-
"Why another backend framework? Are you mad??"
- In the context of my goal to remove all accidental complexity, I treat most popular backend frameworks as low-level building blocks. You are able to write everything with them, but you still have to think about REST API endpoints, choose and connect a database, manage actors manually, setup servers, somehow test serverless functions, etc. Moon will be based on one of those frameworks - this way you don't have to care about low-level things, however you can when you need more control.
-
"Those are pretty weird actors!"
- Yeah, actually, they are called Virtual Actors. I recommend to read Orleans – Virtual Actors.
-
"Why virtual actors and not microservices / event sourcing / standard actors / CRDTs / blockchain / orthogonal persistence / ..?"
- Virtual actors allow you to start with a small simple project and they won't stop you while you grow.
- You can combine virtual actors with other concepts - why don't leverage event sourcing to manage the actor state?
- There is a chance the architecture and security will be improved by implementing actors as isolated single-threaded Wasm modules.
- There are open-source virtual actor frameworks battle-tested in production. And there are also related research papers - especially for Orleans.
-
"What about transactions, backups, autoscaling, deploying, storage configuration, ..?"
- I try to respect Gall’s Law - "A complex system that works is invariably found to have evolved from a simple system that worked."
- So let's start with a single server, simple implementation and minimum features. "Deploy to Heroku" button would be nice.
- There are research papers about virtual actors and things like transactions. It will take time but the architecture will allow to add many new features when needed.