{"id":23187959,"url":"https://github.com/champii/comet","last_synced_at":"2025-08-18T17:33:49.011Z","repository":{"id":61841731,"uuid":"552081500","full_name":"Champii/Comet","owner":"Champii","description":"Reactive Isomorphic FullStack Web Framework for Rust","archived":false,"fork":false,"pushed_at":"2023-04-28T12:47:14.000Z","size":360,"stargazers_count":42,"open_issues_count":1,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-12-17T02:42:17.895Z","etag":null,"topics":["framework","full-stack","isomorphic","reactive","rust","wasm","web"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Champii.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-10-15T19:16:24.000Z","updated_at":"2024-07-17T07:08:20.000Z","dependencies_parsed_at":"2023-01-29T19:15:51.106Z","dependency_job_id":null,"html_url":"https://github.com/Champii/Comet","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Champii%2FComet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Champii%2FComet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Champii%2FComet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Champii%2FComet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Champii","download_url":"https://codeload.github.com/Champii/Comet/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230260729,"owners_count":18198591,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","full-stack","isomorphic","reactive","rust","wasm","web"],"created_at":"2024-12-18T11:11:35.613Z","updated_at":"2024-12-18T11:11:36.274Z","avatar_url":"https://github.com/Champii.png","language":"Rust","readme":"# Comet\n\n[![Documentation Status](https://readthedocs.org/projects/ansicolortags/badge/?version=latest)](https://docs.rs/comet-web/0.1.6/comet)\n[![GitHub license](https://img.shields.io/github/license/Champii/Comet.svg)](https://github.com/Champii/Comet/blob/master/LICENSE.md)\n[![GitHub release](https://img.shields.io/github/tag/Champii/Comet.svg)](https://GitHub.com/Champii/Comet/tags/)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)\n\nReactive isomorphic rust web framework.\n\n## Index\n\n  1. [Introduction](#introduction)\n  2. [Features](#features)\n  3. [Getting started](#getting-started)\n  4. [Quick tour](#quick-tour)\n  5. [Todo list](#todo-list)\n\n---\n\n## Introduction\n\nWork in progress, this is still an early naive prototype.\nDon't expect anything to work properly, expect things to break often.\n\nComet is a framework for the web build with Rust + Wasm \u003c3. It takes its inspiration from MeteorJS, Seed-rs, Yew and others.\n\nThis crate aims to be an all-in-one all-inclusive battery-included isomorphic reactive framework.\n\n      - You keep saying 'Isomorphic', but why ?\n\nIn this context, Isomorphic means that you only write one program for both client and server.  \nOne crate. One. For both. Yes.  \nThis means that we rely a lot on macros and code generation, with all the good and the bad this could bring,\nbut it allows for a great deal of features, close to no boilerplate, and a little quality of life improvement on different aspects.\n\n      - Ok, and how is it reactive then ?\n\nIt is reactive in many sense, first by its `component` system, that encapsulate little bits of logic into an HTML templating system,\nand which can bind your struct's methods directly to JS events, triggering a render of only the components that changed. \nThere is also a reactive layer on top of a `PostgreSQL` database, that permits to watch for some queries to change over time and \nto send push notifications over websocket to every client watching for thoses change, triggering a render when needed.\n\nVisit the [examples](https://github.com/Champii/Comet/tree/master/examples) folder.\n\n---\n\n## Features\n\n - Isomorphic client/server\n - Reactive view\n - Virtual dom\n - Client cache\n - Reactive database with PostgreSQL\n - Auto database generation every time your structs change (Alpha)\n - Websocket\n - Auto procol generation\n - Remote procedure calls\n - (Almost) Zero boilerplate\n\n---\n\n## Getting started\n\n### Install Comet Binary and dependencies\n\n```bash\n$\u003e cargo install comet-cli\n```\n\nYou will need to install and run an instance of PostgreSQL.\n\nIf not found on your system, Comet will install these following crates using `cargo install` on the first run:\n - `wasm-pack`\n - `diesel-cli`\n\n### Create a simple incrementing counter \n\n```bash\n$\u003e comet new my_counter \u0026\u0026 cd my_counter\n```\n\nThere is already the dependency setup in the Cargo.toml:\n\n```toml\ncomet-web = \"0.1.6\"\n```\n\nThis newly generated project contains all you need to get started. Your journey starts with `src/main.rs`.  \nConveniently, this generated file is already the simpliest incrementing counter you can think of:\n\n\n```rust\nuse comet::prelude::*;\n\npub struct Counter {\n    pub value: i32,\n}\n\ncomponent! {\n    Counter {\n        button click: self.value += 1 {\n            self.value \n        }\n    }\n}\n\ncomet::run!(Counter { value: 0 });\n```\n\n### Run it\n\nSetup your database address as an env variable\n\n/!\\ Warning: This database will be COMPLETELY WIPED at startup and everytime your models change  \nThis is not ideal but, hey ! This is still a work in progress :p\n\n```bash\n$\u003e export DATABASE_URL=\"postgres://your_user:your_password@localhost/your_db\"\n```\n\nActually run your project\n\n```bash\n$\u003e comet run\n```\n\nThis will download and install the tools it needs to build and run your crate.\n\n```bash\n[✓] Installing wasm-pack\n[✓] Installing diesel-cli\n[✓] Diesel setup\n[✓] Migrating database\n[✓] Patching schema\n[✓] Building client\n[✓] Building server\n[✓] Running\n -\u003e Listening on 0.0.0.0:8080\n```\n\nThen go to [http://localhost:8080](http://localhost:8080)\n\n---\n\n## Quick tour\n\n  - [Easy definition of the dom](#easy-definition-of-the-dom)\n  - [Use conditional rendering and loops](#use-conditional-rendering-and-loops)\n  - [Bind your variables to `input` fields that react to events](#bind-your-variables-to-input-fields-that-react-to-events)\n  - [Embed your components between them](#embed-your-components-between-them)\n  - [Database persistence for free](#database-persistence-for-free)\n  - [Remote procedure calls](#remote-procedure-calls)\n  - [Database queries](#database-queries)\n  - [HTML view](#html-view)\n  - [Full chat example](#full-chat-example)\n\n\n### Easy definition of the dom\n\n```rust\nuse comet::prelude::*;\n\nstruct MyStruct {\n    my_value: String,\n    my_height: u32,\n}\n\ncomponent! {\n    MyStruct {\n\t// Here #my_id defined the id,\n\t// and the dot .class1 and .class2 add some classes to the element\n\t// The #id must always preceed the classes, if any\n\tdiv #my_id.class1.class2 {\n\t    span {\n\t\t// You can access your context anywhere\n\t\tself.my_value.as_str()\n\t    }\n\t    // Define style properties\n\t    div style: { height: self.my_height } {\n\t\t\"Another child\"\n\t    }\n\t}\n    }\n};\n\n```\n\n### Use conditional rendering and loops\n\n```rust\nuse comet::prelude::*;\n\nstruct MyComponent {\n    show: bool,\n    value: HashMap\u003cString, i32\u003e,\n}\n\ncomponent! {\n    MyComponent {\n\tdiv {\n\t    div {\n\t\t// Conditional rendering with if\n\t\tif self.show {\n\t\t    \"Visible !\"\n\t\t}\n\t\tbutton click: self.show = !self.show {\n\t\t    \"Toggle\"\n\t\t}\n\t    }\n\t    div {\n\t\t// Use a for-like loop.\n\t\tfor (key, value) in self.value {\n\t\t    div {\n\t\t\tkey.as_str()\n\t\t\tvalue\n\t\t    }\n\t\t}\n\t\tbutton click: self.value.push(42)  {\n\t\t    \"Add a number\"\n\t\t}\n\t    }\n\t}\n    }\n}\n```\n\n### Bind your variables to `input` fields that react to events\n\nThis is exclusive to `input` and `select` fields for now  \nEach binding should be unique, as in a different variable for each one, or you will experience conflicts\n\n```rust\nuse comet::prelude::*;\n\nstruct MyStruct {\n    value: String,\n    current_id: i32,\n}\n\ncomponent! {\n    MyStruct {\n\tdiv {\n\t    input bind: self.value {}\n            select bind: self.current_id {\n                option value: 0 {\n                    \"-- Choose a value --\"\n                }\n                for id in 1..9 {\n                    option value: (id) {\n                        id\n                    }\n                }\n            }\n\t    self.value.as_str()\n            self.current_id\n\t}\n    }\n}\n```\n\n### Embed your components between them\n\n```rust\nuse comet::prelude::*;\n\nstruct Child {\n    value: String,\n}\n\ncomponent! {\n    Child {\n\tdiv {\n\t    self.value\n\t}\n    }\n}\n\nstruct Parent {\n    // You need to wrap your components with a Shared\u003cT\u003e that is basically an Arc\u003cRwLock\u003cT\u003e\u003e\n    // This is necessary for your states to persist and be available between each render\n    child: Shared\u003cChild\u003e,\n}\n\ncomponent! {\n    Parent {\n\tdiv {\n\t    // To include a component, just include it like any other variable\n\t    self.child.clone()\n\t}\n    }\n}\n```\n\n### Database persistence for free\n\nAll the previous examples until now were client-side only. Its time to introduce some persistance.\n\nDeriving with the `#[model]` macro gives you access to many default DB methods implemented for your types:  \n```\n    - async Self::fetch(i32)  -\u003e Result\u003cT, String\u003e;  \n    - async Self::list()      -\u003e Result\u003cVec\u003cT\u003e, String\u003e;  \n    - async self.save()       -\u003e Result\u003c(), String\u003e;\n    - async Self::delete(i32) -\u003e Result\u003c(), String\u003e;\n```\n\nThe `String` error type is meant to change into a real error type soon.\n\nYou have a way to add your own database query methods, please read [Database queries](#database-queries) below.\n\n```rust\nuse comet::prelude::*;\n\n// You just have to add this little attribute to your type et voila !\n// It will add a field `id: i32` to the struct, for database storing purpose\n// Also, when adding/changing a field to this struct, the db will \n// automatically update its schema and generate new diesel bindings\n#[model]\nstruct Todo {\n    title: String,\n    completed: bool,\n}\n\nimpl Todo {\n    pub async fn toggle(\u0026mut self) {\n        self.completed = !self.completed;\n\n        // This will save the model in the db\n        self.save().await;\n    }\n}\n\ncomponent! {\n    Todo {\n\tdiv {\n\t    self.id\n\t    self.title.as_str()\n\t    self.completed\n            button click: self.toggle().await {\n               \"Toggle\"\n\t    }\n\t}\n    }\n}\n\n// This will create a new Todo in db every time this program runs\ncomet::run!(Todo::default().create().await.unwrap());\n```\n\n### Remote procedure calls\n\nNote: The structs involved in the `#[rpc]` macro MUST be accessible from the root module (i.e. `src/main.rs`)\n\n```rust\nuse comet::prelude::*;\n\n// If you have other mods that use `#[rpc]`, you have to import them explicitly\n// in the root (assuming this file is the root). This is a limitation that will not last, hopefully\nmod other_mod;\nuse other_mod::OtherComponent;\n\n#[model]\n#[derive(Default)]\npub struct Counter {\n    pub count: i32,\n}\n\n// This attribute indicates that all the following methods are to be treated as RPC\n// These special methods are only executed server side\n// The only difference with the similar method above is that the `self.count +=1` is done server side,\n// and the `self` sent back to the client\n#[rpc]\nimpl Counter {\n    // The RPC methods MUST be async (at least for now)\n    pub async fn remote_increment(\u0026mut self) {\n        self.count += 1;\n\t\n        self.save().await;\n    }\n}\n\ncomponent! {\n    Counter {\n\tbutton click: self.remote_increment().await {\n\t    self.count\n\t}\n    }\n}\n\ncomet::run!(Counter::default().create().await.unwrap());\n```\n\n### Database queries\n\nThe most simple way to define a new database query is with the macro `#[sql]`, that uses `#[rpc]` underneath.\n\nAll your models have been augmented with auto-generated diesel bindings, so you can use a familiar syntax.\nThere will be a way to give raw SQL in the near future.\n\n```rust\nuse comet::prelude::*;\n\n#[model]\n#[derive(Default, Debug)]\npub struct Todo {\n    pub title: String,\n    pub completed: bool,\n}\n\n#[sql]\nimpl Todo {\n    // Use the watch macro to get back your data whenever the result set change in DB\n    // Only valid for select statement for now\n    #[watch]\n    pub async fn db_get_all(limit: u16) -\u003e Vec\u003cTodo\u003e {\n\t// The diesel schema has been generated for you\n        use crate::schema::todos;\n\n        // You don't have to actually execute the query, all the machinery\n\t// of creating a db connection and feeding it everywhere have been \n\t// abstracted away so you can concentrate on what matters\n        todos::table.select(todos::all_columns).limit(limit as i64)\n    }\n}\n```\n\n### HTML view\n\nUntil now, we always used components to manage our views and logic.  \nWhenever you define a component using the `component!` macro, you define bits of HTML directly inside the macro.  \nUnder the hood, we call the `html!` macro that is a lot simpler in term of features.\n\n```rust\n// You can define basic function that return an HTML\npub async fn my_view(my_arg: MyType) -\u003e Html {\n    html! {\n        div {\n            my_arg.my_property\n        }\n    }\n}\n\n// Then you can call it from a component, or another existing html view.\ncomponent! {\n    SomeComponent {\n        div {\n            my_view(self.some_value).await\n        }\n    }\n}\n```\n\nPlease note that the `html!` macro does not support input bindings (`bind`) or event bindings (`click`, `change`), \nat least for now.\n\n### Full chat example\n\nThis is a client/server fully reactive chat room\n\nThere is a more elaborate multi-channel chat in the examples folder\n\n```rust\nuse comet::prelude::*;\n\n#[model]\npub struct Message {\n    pub sender: String,\n    pub content: String,\n}\n\n#[sql]\nimpl Message {\n    #[watch]\n    pub async fn list_watch() -\u003e Vec\u003cMessage\u003e {\n        use crate::schema::messages;\n        messages::table.select(messages::all_columns)\n    }\n}\n\ncomponent! {\n    Message {\n        div {\n            self.sender.to_owned() + \": \" + \u0026self.content\n        }\n    }\n}\n\n#[derive(Default)]\npub struct App {\n    pub sender: String,\n    pub content: String,\n}\n\nimpl App {\n    async fn send_message(\u0026mut self) {\n        let mut message = Message {\n            id: -1,\n            sender: self.sender.clone(),\n            content: self.content.clone(),\n        };\n\n        self.content = \"\".into();\n\n        message.save().await.unwrap();\n    }\n}\n\ncomponent! {\n    App {\n        div {\n            Message::list_watch().await\n            input bind: self.sender {}\n            input bind: self.content {}\n            button click: self.send_message().await {\n                \"Send\"\n            }\n        }\n    }\n}\n\ncomet::run!(App::default());\n```\n\n---\n\n## Todo List\n- Function Component\n- Allow for iterators inside html\n- Have a ComponentId that allows to fetch the corresponding root dom element\n- Find a way for global inter-component message passing\n- Use the cache for non-watched rpc queries (because this cause a lot of traffic on each redraw)\n- Find a way to have a global state\n- Postgres pool and reusable connections\n- Implement ToVirtualNode for Result\u003cT, Error\u003e\n- Add an extensible error system\n\n- Separate all the reusable features in different crates:\n  - [ ] Comet crate\n    - [ ] The view system\n      - [ ] The html macro\n      - [ ] The component macro\n    - [ ] The isomorphic db model through websocket\n      - [ ] The #[model] proc macro that generates basic model queries\n      - [ ] An abstract ws server/client\n        - [ ] The auto-proto macro\n        - [X] The reactive/listening part of the db [reactive-postgres-rs](https://github.com/Champii/reactive-postgres-rs)\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchampii%2Fcomet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchampii%2Fcomet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchampii%2Fcomet/lists"}