Skip to main content

Crate injoint

Crate injoint 

Source
Expand description

Injoint is a library for creating and managing WebSocket connections in declarative, inspired by state reducer approach.

§About Injoint

Core concept of injoint is Joint structure - a wrapper for all multithreaded asynchronous functionality that is necessary to run a WebSocket server. It is responsible for managing the state of the application, handling incoming messages, and dispatching them to the appropriate reducers for each room individually.

§Joint implementations

Joint structure is heuristically abstract and need to be implemented around some real-life conception, for example, websockets or mpsc. injoint library currently provides these implementations for AbstractJoint:

  • WebsocketJoint - common implementation around asynchronous websocket connection using tokio and tungstenite libraries.
  • AxumWSJoint - another implementation around websocket that can be integrated into axum router.
  • MPSCJoint - implementation around tokio::sync::mpsc channels.

§Usage

Example of minimalistic websocket chat server taken from GitHub repository:

use injoint::codegen::{reducer_actions, Broadcastable};
use injoint::joint::ws::WebsocketJoint;
use serde::Serialize;
use std::collections::HashMap;

// message struct, used in State
#[derive(Serialize, Debug, Clone, Broadcastable)]
struct Message {
    pub author: u64,
    pub content: String,
}

// chat state struct
#[derive(Serialize, Debug, Default, Clone, Broadcastable)]
struct State {
    users: HashMap<u64, String>,
    messages: Vec<Message>,
}

// state reducer, statically injected to `WebsocketJoint`
#[derive(Default, Serialize, Clone, Broadcastable)]
struct Reducer {
    state: State,
}

impl Reducer {
    pub fn new() -> Self {
        Reducer {
            state: State {
                users: HashMap::new(),
                messages: Vec::new(),
            },
        }
    }
}

// using `reducer_actions` macro to generate boilerplate
// code implementing actions and their dispatching
#[reducer_actions(State)]
impl Reducer {
    async fn identify_user(&mut self, client_id: u64, name: String) -> Result<String, String> {
        if self.state.users.contains_key(&client_id) {
            return Err("User already identified".to_string());
        }
        self.state.users.insert(client_id, name.clone());
        Ok(name)
    }

    async fn send_message(&mut self, client_id: u64, text: String) -> Result<String, String> {
        if !self.state.users.contains_key(&client_id) {
            return Err("User not identified".to_string());
        }
        self.state.messages.push(Message {
            author: client_id,
            content: text.clone(),
        });
        Ok(text)
    }
}

#[tokio::main]
async fn main() {
    // initialize default reducer state
    let reducer = Reducer::new();

    // create root websocket joint instance
    let mut joint = WebsocketJoint::<Reducer>::new(reducer);

    // bind address to listen on
    joint.bind_addr("127.0.0.1:3000").await.unwrap();

   // start listening loop handling incoming connections
    joint.listen().await;
}
§And then just build and run it with
cargo run
§As websocket client, you may send one of four types of methods:
  • Create - create a new room example:
{
"message": {
    "type": "Create",
},
"client_token": "" // doesn't affect builtin logic, you can use this token to identify your client
}
  • Join - join an existing room by id example:
{
"message": {
    "type": "Join",
	"data": 0 // room id
},
"client_token": ""
}
  • Action - perform one of actions defined in your Reducer example:
{
"message": {
    "type": "Action",
	"data": "{\"type\":\"ActionIdentifyUser\",\"data\":\"quasarity\"}" // action payload
 },
 "client_token": ""
 }
  • Leave - leave current room example:
{
"message": {
    "type": "Jeave",
},
"client_token": ""
}
§And server will respond with one of four types of messages:
  • RoomCreated - room created successfully example:
{
"status": "RoomCreated",
"message": 0 // room id
}
  • RoomJoined - joined existing room successfully example:
{
"status": "RoomJoined",
"message": 0 // client id
}
  • StateSent - state sent successfully, sent to each client individually example:
{
"status": "StateSent",
"message": "{
       "users": {
           "0": "quasarity"
       },
       "messages": [
            {
                "author": 0,
                "content": "Hello, world!"
            }
        ]}"
    }
}
  • Action - action performed successfully, sent to each client in room example:
{
	"status": "Action",
	"message": {
		"author": 0,
		"data": "quasarity",
		"state": {
			"messages": [],
			"users": {
				"0": "quasarity",
			}
		},
		"status": "ActionIdentifyUser"
	}
}
  • RoomLeft - left current room successfully example:
{
"status": "RoomLeft",
"message": 0 // client id
}

Modules§

codegen
This module contains the code generation macros for injoint.
connection
Connection is a structure that represents a connection to a client.
dispatcher
Dispatcher is a structure that handles incoming messages and dispatches them to the appropriate reducer.
joint
Joint is a structure that represents a joint implementation for real-time communication.
message
Message is a structure that represents a message sent over the WebSocket connection.
response
Response is a structure that represents a response sent back to the client.
utils
State is a structure that represents the state of the application.