dream icon indicating copy to clipboard operation
dream copied to clipboard

discussion: implementing phoenix like websocket channels

Open tcoopman opened this issue 3 years ago • 7 comments

I'm looking and playing around a bit with dream and I have a hobby project that would definitely benefit from something like channels in dream.

So I created this issue to start some discussion around how to implement this for dream. I haven't thought deeply about this yet, but I was wondering if you could give some input on what a good way of tackling this would be.

Questions I have at the moment:

  • What would be the best way of handling runtime state
  • I don't see an example of a long running websocket (the examples all send Dream.close_websocket) how would that work?
  • I would need a reference to the websocket so that I can broadcast, reply to other people,... Not sure what the best way to do that either

tcoopman avatar Jun 21 '21 12:06 tcoopman

I don't see an example of a long running websocket (the examples all send Dream.close_websocket) how would that work?

You can simply pass the reference that you get to the WebSocket to any other function, store it in data structures, etc. There's no reason why you must close it inside that callback.

I would need a reference to the websocket so that I can broadcast, reply to other people,... Not sure what the best way to do that either

I think the above answers this, as well, but let me know if not.

What would be the best way of handling runtime state

What are the options? You should be able to handle it in any way in OCaml. A simple example would be some kind of mutable map from channel names to WebSocket lists, so (string, Dream.websocket list) Hashtbl.t. There might be drawbacks to that depending on what you want to implement exactly, but that's at least one way to start off with.

aantron avatar Jun 21 '21 19:06 aantron

Thanks, so, I'll create a recursive function and a mutable structure (Hashtbl for example), something like this:

module Channel = struct
  let joined = Hashtbl.create ...
  let join .... = update Hashtbl
end

let rec websocket_listener websocket =
  match%lwt Dream.receive websocket with
  | Some msg ->
    let key = key_from_message msg in
    let%lwt () = Channel.join ~key ~websocket in
    ws_loop websocket
  | _ ->
    Dream.close_websocket websocket

tested something similar to this and it seems to work.

Would this be something that would be useful to document in a simple chat example?

tcoopman avatar Jun 22 '21 10:06 tcoopman

Yes, if you'd like to make an example (that is still about as simple as most examples), I'd merge it :)

aantron avatar Jun 22 '21 13:06 aantron

I've been thinking a bit more about how to model this, feedback is welcome:

type topic =
  | Topic of string
  | WithSubtopic of (string * string)

type payload = Payload of string

type answers = answer list
and answer =
  | Reply of string
  | Broadcast of string
  | Stop of string

type 'a channel =
  { handle_join : topic -> payload -> 'a * answers
  ; handle_message : 'a -> payload -> 'a * answers
  }

val channels : (string * 'a channel) list -> Dream.websocket -> unit Lwt.t

The idea is that you can do something like this:

          Dream.websocket
      @@ Socket.channels
           [ ( "public_chat"
             , { handle_join = (fun topic payload -> (initial state, [ replies ]))
               ; handle_message = (fun state payload -> (new state, [ replies ]))
               } )
           ; ( "private_chat:123"
             , { handle_join = (fun topic payload -> (initial state, [ replies ]))
               ; handle_message = (fun state payload -> (new state, [ replies ]))
               } )
           ] )

This is a first brain dump, so it's not complete and it has at least one big flaw at the moment. A type 'a channel is no good, each channel can have a different type. I'll need an other structure for that.

tcoopman avatar Jun 25 '21 08:06 tcoopman

The use of channels will force all the channels in one call to Socket.channels to have the same type parameter, so this might be more reasonable than I think you are suggesting :)

EDIT: that is, your concern about the type parameter may be unfounded, and it's actually fine to have it... if I have not misunderstood.

aantron avatar Jun 25 '21 09:06 aantron

My original idea was that every Channel could have it's own state (and thus also decide on how the state would look like). So for example a topic chat:123 would have some state and product:xxx would have some other.

Although maybe that's not really a requirement that I have at the moment...

I'm first going to try to play with this a bit and focus on the answer. The idea is that you can reply: Broadcast hi and it's a message that goes to everyone in the channel chat:123

tcoopman avatar Jun 25 '21 13:06 tcoopman

I've made my current WIP public: https://github.com/tcoopman/dream-channels

tcoopman avatar Jul 08 '21 07:07 tcoopman