Comet
Reactive isomorphic rust web framework.
Index
Introduction
Work in progress, this is still a naive early prototype.
Comet is a framework for the web build with Rust+Wasm<3. It takes its inspiration from MeteorJS, Seed-rs, Yew and others.
This crate aims to be an all-in-one all-inclusive battery-included isomorphic reactive framework.
- You keep saying 'Isomorphic', but why ?
In this context, Isomorphic means that you only write one program for both client and server.
One crate. One. For both. Yes.
This means that we rely a lot on macros and code generation, with all the good and the bad this could bring,
but it allows for a great deal of features, close to no boilerplate, and a little quality of life improvement on different aspects.
- Ok, and how is it reactive then ?
It is reactive in many sense, first by its component system, that encapsulate little bits of logic into an HTML templating system,
and which can bind your struct's methods directly to JS events, triggering a render of only the components that changed.
There is also a reactive layer on top of a PostgreSQL database, that permits to watch for some queries to change over time and
to send push notifications over websocket to every client watching for thoses change, triggering a render when needed.
Visit the examples folder.
Features
- Isomorphic client/server
- Reactive view
- Reactive database with PostgreSQL
- Remote procedure calls
- Auto database generation every time your structs change
- Websocket
- Auto procol generation
- Convenient wrapper binary
- (Almost) Zero boilerplate
- Clean Codebase (Yeaaah, ok, this one is a lie)
- Fast (Soon™)
- Client cache (Soon™)
Getting started
Install Comet Binary and dependencies
You will need to install and run an instance of PostgreSQL.
If not found on your system, Comet will install these following crates using cargo install on the first run:
wasm-packdiesel-cli
Create a simple incrementing counter
&&
This newly generated project contains all you need to get started. Your journey starts with src/main.rs.
Conveniently, this generated file is already the simpliest incrementing counter you can think of:
// The mandatory imports
use *;
// This macro takes two arguments:
// - A type for which we will implement `Component`
// - And a root HTML element
// We implement `Component` for a simple integer.
component!
// This is where all the magic happens
// We run the application with an instance of our i32 component that starts with the value 0
comet!;
Run it
Setup your database address as an env variable
/!\ Warning: This database will be COMPLETELY WIPED at startup and everytime your models change
This is not ideal but, hey ! This is still a work in progress :p
Actually run your project
This will download and install the tools it needs to build and run your crate.
Then go go to http://localhost:8080
Quick tour
- Easy definition of the dom
- Use conditional rendering and loops
- Bind your variables to
inputfields that react to events - Embed your components between them
- Database persistence for free
- Remote procedure calls
- Database queries
Easy definition of the dom
component! ;
Use conditional rendering and loops
component!
Bind your variables to input fields that react to events
This is exclusive to input fields for now
The whole component is re-rendered on input's blur event (unfocus).
Each binding should be unique, as in a different variable for each one
component!
Embed your components between them
component!
component!
Database persistence for free
All the previous examples until now were client-side only. Its time to introduce some persistance.
Deriving with the #[model] macro gives you access to many default DB methods implemented for your types:
- async Self::fetch(i32) -> Result<T, String>;
- async Self::list() -> Result<Vec<T>, String>;
- async self.save() -> Result<(), String>;
- async Self::delete(i32) -> Result<(), String>;
The String error type is meant to change into a real error type soon.
You have a way to add your own database query methods, please read Database queries below.
// You just have to add this little attribute to your type et voila !
// It will add a field `id: i32` to the struct, for database storing purpose
// Also, when adding/changing a field to this struct, the db will
// automatically update its schema and generate new diesel bindings
component!
// This will create a new Todo in db every time this program runs
comet!;
Remote procedure calls
Note: The structs involved in the #[rpc] macro MUST be accessible from the root module (i.e. src/main.rs)
use *;
// If you have other mods that use `#[rpc]`, you have to import them explicitly
// in the root (assuming this file is the root). This is a limitation that will not last, hopefully
use OtherComponent;
// This attribute indicate that all the following methods are to be treated as RPC
// These special methods are only executed server side
component!
comet!;
Database queries
When dealing with Database queries, it is obvious that they should only be executed server side.
The most simple way to define a new one is with the macro #[sql], that uses #[rpc] underneath.
All your models have been augmented with auto-generated diesel bindings, so you can use a familiar syntax. There will be a way to give raw SQL in the near future.
Soon there will also be a #[watch] attribute that will trigger the reactive redraw when your model change
Todo List
-
Allow for iterators inside html
-
Allow to mix attributes, styles and events
-
Client cache (with local wasm sql ?)
-
Have a ComponentId that allows to fetch the corresponding root dom element
-
Have some QueryId
- Every user-defined raw queries will have a QueryId known from both client and server.
- These queries are parametrized, and only these parameters and the QueryId transit from the client to the server
- The client register any query's QueryHash (params+query_id) with every ComponentId that triggered it
- The client check the cache if this query exists, if so return the data and render
- Else, forward the query and the RequestId to the server
- Bind it to the watch query server side
- When triggered, the changes are passed back along with the original RequestId and QueryId
- Then the client update its local store and trigger the render of the component's element that originated the request
-
Have some RequestId to implement sync/async RPC-like communication
-
Find a way for global inter-component message passing
-
Allow for real time value binding for input element without losing focus (might need a real virtual dom for this one)
-
Separate all the reusable features in different crates:
- Comet crate
- The view system
- The html macro
- The component macro
- The isomorphic db model through websocket
- The #[model] proc macro that generates basic model queries
- An abstract ws server/client
- The auto-proto macro
- The reactive/listening part of the db reactive-postgres-rs
- The view system
- Comet crate