ocamlcommunicationreactive-programminginstant-messagingocsigen

Eliom client to client messaging - Eref scope issue


I have been trying to gain a better understanding of the Eliom functionality for communication lately and to do so I tried to build a simple web page that allows users to send messages to each other.

The web page works fine if I log in as one user via Firefox and a second user via Chrome. But when I log in two different users in the same browser and try to send a message from one user to the other (i.e. from one tab to another tab), any message sent is displayed on all tabs, not the intended recipient's tab only.

I believe that I may have some issues with the chosen scope for erefs or where I am setting/getting the scope and erefs (toplevel vs. service definition).

I am trying to correct my mistake so that two users can be logged into two different tabs of the same browser and send messages back and forth to each other and the messages are only displayed on the correct users tabs.

Note: Some of this code is taken from the Eliom site tutorial at: http://ocsigen.org/tuto/4.2/manual/how-to-implement-a-notification-system

My .eliom file:

(* Example website login: localhost:8080/?user_num=1 *)

{shared{
  open Eliom_lib
  open Eliom_content
  open Html5
  open Html5.F
  open Eliom_registration
  open Eliom_parameter
}}

module Channel_example_app =
  Eliom_registration.App (
    struct
      let application_name = "channel_example"
    end)

let main_service =
  Eliom_service.App.service ~path:[] ~get_params:(string "user_num") ()

let new_message_action =
  Eliom_service.Http.post_coservice'
    ~post_params:(string "from_user_id" ** string "to_user_id" ** string "msg") ()

(* Set the scope used by all erefs *)
let eref_scope = Eliom_common.default_process_scope

(* Create a channel eref *)
let channel_ref =
  Eliom_reference.Volatile.eref_from_fun
    ~scope:eref_scope
    (fun () ->
      let (s, notify) = Lwt_stream.create () in
      let c = Eliom_comet.Channel.create s in
      (c, notify)
    )

(* Reactive string eref *)
let react_string_ref =
  Eliom_reference.Volatile.eref_from_fun
    ~scope:eref_scope
    (fun () ->
      let (client_string, send_client_string) :
          (string React.E.t * (?step:React.step -> string -> unit) ) =
        React.E.create ()
      in
      (client_string, send_client_string)
    )

(* Reactive string to display the users session group *)
let react_session_group_ref =
  Eliom_reference.Volatile.eref_from_fun
    ~scope:eref_scope
    (fun () ->
      let (session_group_string, send_session_group_string) :
          (string React.E.t * (?step:React.step -> string -> unit) ) =
        React.E.create ()
      in
      (session_group_string, send_session_group_string)
    )

(* Reactive string to display the users session group size *)
let react_session_group_size_ref =
  Eliom_reference.Volatile.eref_from_fun
    ~scope:eref_scope
    (fun () ->
      let (session_group_size_string, send_session_group_size_string) :
          (string React.E.t * (?step:React.step -> string -> unit) ) =
        React.E.create ()
      in
      (session_group_size_string, send_session_group_size_string)
    )

(* Send a message from one client to another *)
let notify from_user_id to_user_id s =
  (* Get the session group state for the user *)
  let state =
    Eliom_state.Ext.volatile_data_group_state     ~scope:Eliom_common.default_group_scope to_user_id in
    (* Iterate on all sessions from the group *)
    Eliom_state.Ext.iter_volatile_sub_states ~state
    (fun state ->
      (* Iterate on all client process states in the session *)
      Eliom_state.Ext.iter_volatile_sub_states ~state
      (fun state ->
        let (_, notify) = Eliom_reference.Volatile.Ext.get state channel_ref in
        notify (Some ("Hello from " ^ from_user_id ^ "! You are user " ^ to_user_id ^ "\n\n" ^ s))
      )
    )

(* Action for a client to send a message *)
let () =
  Eliom_registration.Action.register
    ~options:`NoReload
    ~service:new_message_action
    (fun () (from_user_id, (to_user_id, msg)) ->
      Lwt.return @@ notify from_user_id to_user_id msg
    )

(* Post form for one user to send a message to another user *)
let client_message_form =
  Eliom_content.Html5.F.post_form ~service:new_message_action ~port:8080
  (
    fun (from_user_id, (to_user_id, msg)) ->
      [p [pcdata "To:"];
       string_input ~input_type:`Text ~name:to_user_id ();
       p [pcdata "From:"];
       string_input ~input_type:`Text ~name:from_user_id ();
       p [pcdata "Send a message here:"];
       string_input ~input_type:`Text ~name:msg ();
       button ~button_type:`Submit [pcdata "Send Message"]
      ]
  )

let () =
  Channel_example_app.register
    ~service:main_service
    (fun user_num () ->
      (* Set the session group to which the erefs belong *)
      Eliom_state.set_volatile_data_session_group
        ~set_max:1
        ~scope:Eliom_common.default_session_scope
        ~secure:true
        user_num;
      let (channel, _) = Eliom_reference.Volatile.get channel_ref in
      let my_client_string, my_send_client_string = Eliom_reference.Volatile.get react_string_ref in
      let my_send_client_string' =
        server_function Json.t<string> (fun s -> Lwt.return @@ my_send_client_string s)
      in
      let c_down = Eliom_react.Down.of_react my_client_string in
      (* When a message is received on the channel, push it as a reactive event *)
      let _ =
        {unit{
          Lwt.async
            (fun () ->
              Lwt_stream.iter (fun (s : string) -> ignore @@ %my_send_client_string' s) %channel
            )
        }}
      in
      let my_session_group =
        match
          Eliom_state.get_volatile_data_session_group
            ~scope:Eliom_common.default_session_scope
            ~secure:true ()
        with
        | None -> "No session group"
        | Some sg -> sg
      in
      let my_session_group_size =
        match
          Eliom_state.get_volatile_data_session_group_size
            ~scope:Eliom_common.default_session_scope
            ~secure:true ()
        with
        | None -> "0"
        | Some gs -> string_of_int gs
      in
      Lwt.return
        (Eliom_tools.F.html
           ~title:"channel_example"
           ~css:[["css";"channel_example.css"]]
           Eliom_content.Html5.F.(body [
             h2 [pcdata ("Your are logged in as user " ^ user_num)];
             client_message_form ();
             p [pcdata "Your message is:"];
             C.node {{R.pcdata (React.S.hold "No message yet" %c_down)}};
             p [pcdata ("I am a part of the session group named " ^ my_session_group)];
             p [pcdata ("My session group size is " ^ my_session_group_size)]
           ])))

Solution

  • The problem came from using the notify function which loops through all tabs. I used the Hashtable / Weak Hashtable structure from Eliom Base App and it corrected all the communication issues. The key was altering the notify function as follows:

    let notify ?(notforme = false) ~id ~to_user ~msg =
      Lwt.async (fun () ->
        I.fold
          (fun (userid_o, ((_, _, send_e) as nn)) (beg : unit Lwt.t) ->
            if notforme && nn == Eliom_reference.Volatile.get notif_e
            then Lwt.return ()
            else
              lwt () = beg in
              let content = if Some to_user = userid_o then Some msg else None in
              match content with
              | Some content -> send_e (id, content); Lwt.return ()
              | None -> Lwt.return ()
          )
          id (Lwt.return ()))