rawhat / mist

gleam HTTP server. because it glistens on a web
Apache License 2.0
301 stars 11 forks source link

Example of using mist.Custom #54

Open B-R-P opened 2 months ago

B-R-P commented 2 months ago

When handle_ws_message get mist.Custom as message. I want to broadcast message to all clients. How can I do it? I want to create public chat (Every client can send and receive)

connellr023 commented 2 months ago

I have been trying to do this as well. Here is some code I have been working with:

pub fn start(
  req: Request(Connection),
  selector: Option(Selector(CustomWebsocketMessage)),
  queue_subject: Subject(QueueActorMessage)
) -> Response(ResponseData) {
  mist.websocket(
    request: req,
    on_init: fn(_) {
      io.println("New connection initialized")

      let state = WebsocketActorState(
        name: None,
        subject: process.new_subject(),
        room_subject: None,
        queue_subject: queue_subject
      )

      #(state, selector)
    },
    // continued

I want to pass around a subject to other actors so that they will be able to send messages back to the websocket actor through Custom(CustomWebsocketMessage). However, everytime I do this, I get an error that looks like:

Actor discarding unexpected message: #(//erl(#Ref<0.1228312467.3287285767.83889>), Custom(Disconnect))

connellr023 commented 2 months ago

Hey @B-R-P I just wanted to let you know I figured out how to use the Custom variant of WebsocketMessage to receive messages from other actors in your application. I will share below:

My main file:

pub fn main() {
  let queue_actor = queue_actor.start() // Ignore this its for my own applicaiton
  let selector = process.new_selector() // Create a selector in the main process. This will be used to select custom messages (in my case I have a CustomWebsocketMessage type defined elsewhere

  let assert Ok(_) = fn(req: Request(Connection)) -> Response(ResponseData) {
    case request.path_segments(req) {
      ["api", "connect"] -> websocket_actor.start(req, selector, queue_actor)
      _ -> response.new(404) |> response.set_body(Bytes(bytes_builder.new()))
    }
  }
  |> mist.new
  |> mist.port(port)
  |> mist.start_http

  process.sleep_forever()
}

The /api/connect route calls the function below to upgrade to ws connection.

pub fn start(
  req: Request(Connection),
  selector: Selector(CustomWebsocketMessage),
  queue_subject: Subject(QueueActorMessage) // Ignore this
) -> Response(ResponseData) {
  mist.websocket(
    request: req,
    on_init: fn(_) {
      io.println("New connection initialized")

      // Create a new subject for the current websocket process that other actors will be able to send messages to
      let ws_subject = process.new_subject()

      // Register it to the CustomWebsocketMessage selector
      let new_selector = selector
      |> process.selecting(ws_subject, function.identity) // Use function.identity since this is the only type I am selecting

      let state = WebsocketActorState(
        name: None, // Ignore
        ws_subject: ws_subject, // Save the subject in the state
        room_subject: None, // Ignore
        queue_subject: queue_subject // Ignore
      )

      #(state, Some(new_selector))
    },
    on_close: fn(state) {
      io.println("A connection was closed")
      state |> on_shutdown

      Nil
    },
    handler: handle_message
  )
}

fn handle_message(
  state: WebsocketActorState,
  connection: WebsocketConnection,
  message: WebsocketMessage(CustomWebsocketMessage)
) -> Next(CustomWebsocketMessage, WebsocketActorState) {
  case message {
    Custom(message) -> case message {
      JoinRoom(room_subject) -> {
        // Custom implementation
      }
      SendMessage(message) -> //...
      _ -> state |> actor.continue
    }
    // ....
}

Hopefully this helps. The mist documentation should be updated to show an example as this was not trivial to figure out.

To answer your main question, if you wanted every client to send and receive messages like in a chat room, you could implement an actor that keeps a List(Subject(CustomwebSocketMessage)) and when the websocket actor receives a message from the client that they want to send a global message, the websocket actor will relay it to the chat room actor which will send the Custom SendMessage message to every Subject(CustomwebsocketMessage) in the List.

rawhat commented 2 months ago

I think #53 would address the usage of the Custom message type in an example. Covering things like "topics" or "broadcasting" I think are probably a bit beyond the scope of this library. You could approach them in many different ways, potentially with other libraries (pub-sub, registries, etc).

You don't need to pass in a Selector to your start function there. It looks like your code is pretty standard gleam/otp/actor structure, which is good. I think this is probably just a general lack of documentation / discovery, rather than just a mist issue.

But I will work on a more thorough example with some basic use-cases around this type of thing.

connellr023 commented 1 month ago

Yes, this was certainly an issue with lack of documentation (seems to be the only hole in otherwise well documented library). I have managed to implement a chat application that I believe is something like what @B-R-P was thinking of only using mist.

The repo is: here

People are welcome to reference it to get a better understanding of how to use this library and Gleam in general. Hopefully it helps.