Warning: Reason support is experimental. We are looking for beta-tester and contributors.

Communication between the client and the server

Table of contents

Besides injections and client values, as described in the documentation of our PPX syntax extension, there are multiple ways for the client and the server to exchange values.

Remote Procedure Calls

Eliom provides an easy way to call server functions from the client: Eliom_pervasives.server_function (and Eliom_pervasives.server_function).

A function 'a -> 'b Lwt.t can be wrapped on the server by Eliom_pervasives.server_function. When the result is injected to the client, it appears as a plain function 'a -> 'b Lwt.

It is necessary to provide an instance of Deriving_Json for the argument type, to safely send the argument from the client to the server.

Every call to server_function creates a new non-attached POST coservice. It is thus advisable to apply it only once and bind it to an identifier, if it is to be used several times.


let%server log str = Lwt_io.write_line Lwt_io.stdout str

let%server rpc_log = server_function [%derive.json: string] log

let%client () =
    (* NB The service underlying the server_function isn't available
       on the client before loading the page. *)
    (fun () ->
         (fun () -> ~%rpc_log "hello from the client to the server!"))

Exceptions raised in the server-side function cannot be handled directly on the client; it is impossible to marshal them in OCaml to send them to the client. Instead, if an exception is raised in the server function, the function application fails (in Lwt) on the client with the exception Eliom_lib.Exception_on_server whose argument describes the original exception (according to Printexc.to_string).

Client requesting data

Server functions are implemented using special services that take and return OCaml values. In this section we will see how to define services returning OCaml data.

These services are registered using Eliom_registration.Ocaml and can be called using Eliom_client.call_ocaml_service.

Such services cannot be visited by the browser as web pages. You usually want POST coservices for this use case. This corresponds to remote function calls, that are typically handled by non-attached POST coservices.

Example of use:

open Eliom_content

let pi_service =
    (fun () () -> Lwt.return 3.1415926535)

let _ =
    (fun () () ->
       ignore [%client
         (Lwt.ignore_result (
            let%lwt pi =
              Eliom_client.call_ocaml_service ~service:~%pi_service () ()
            Lwt.return (
                (Js.string ("pi = "^ string_of_float pi))))
          : unit)
             (head (title (pcdata "pi")) [])
             (body [])))

Since the client-side representation of values differs from the server-side representation, there are restrictions on what can be sent. The restrictions are the same as for the ~%variable mechanism. (See chapter Wrapping.)

Client sending data

The client can send OCaml values as parameters to services. To do that, declare the expected parameter type using Eliom_parameter.ocaml.

Since the server cannot trust the client to send correctly-formed data, Eliom is not using the standard OCaml marshalling mechanism. (The server needs to be able to check that the value is of the expected type.) For this reason, you must declare the types of the data you want to be able to send to the server using our Ppx_deriving syntax extension:

  type some_type = (int * string list) [@@deriving json]
  type another_type =
    | A of some_type
    | B of another_type
    [@@deriving json]

This type can now be used as a parameter for a service:

open Eliom_content

let s =
    ~get_params:(Eliom_parameter.ocaml "param" [%derive.json: another_type])
    (fun v () ->
       Lwt.return Html5.D.(
           (head (title (pcdata "title")) [])
           (body [
              match v with
              | A _ -> pcdata "A"
              | B _ -> pcdata "B"

let _ =
    (fun () () ->
       Lwt.return Html5.D.(
           (head (title (pcdata "title")) [])
              [p ~a:[a_onclick
                         (fun _ ->
                            Lwt.async (fun () ->
                              Eliom_client.change_page ~service:~%s
                                (A (1, ["s"])) ()))
                 [pcdata "Click to send Ocaml data"]])))

It works for the datatypes you define, and the data types from OCaml's standard library. For types defined in third-party libraries, have a look at deriving's documentation and Js_of_ocaml's Deriving_Json.

Server sending data

Eliom provides mechanisms to allow the server to send data to a client. We call this mechanism Comet, it is also sometimes called HTTP push.

The simple low level version above which all other following mechanisms are implemented is provided in the Eliom_comet.Channel module.

Comet defines channels which can convey data. A channel is created using a Lwt stream. It is a kind of cooperative lazy list.

The two main methods to create a stream are through the functions Lwt_stream.from and Lwt_stream.create.

val from : (unit -> 'a option Lwt.t) -> 'a t
val create : unit -> 'a t * ('a option -> unit)

Function Lwt_stream.from makes possible to create a stream where a new value is added each time a function returns. Function Lwt_stream.create returns a stream and a function to push new values to the stream.

On client side the type Eliom_comet.Channel.t is just a Lwt stream Lwt_stream.t.

There are 3 kind of channels depending on how you want to send data.

  • Channels created with Eliom_comet.Channel.create have a buffer with a limited size. Message are read from the stream as soon as they are available, i.e. for stream created with Lwt_stream.from, that means that the function is called another time as soon as the previous one terminate. For stream created with Lwt_stream.create, this is as soon as they are pushed. If the client missed too many messages (more than the size of the buffer) it will receive an exception Eliom_comet.Channel_full when reading data from the stream.
  • Channels created with Eliom_comet.Channel.create_newest have no buffering and can loose messages, but the client will always receive the last value: For instance if many messages are sent in a short time, the client may receive only the last one. Those channels never raise Eliom_comet.Channel_full.

Channels can be closed on client side by cancelling a thread waiting for data on it.

Like services, channels have a scope (only site or client process). The constraints vary with respect to the scope you choose:

  • Channels created with scope Eliom_common.site_scope or using Eliom_comet.Channel.create_newest are stateless channels: the memory consumption does not depend on the number of users requesting data on it. When the channels are not reachable from the server code, they are garbage-collected and closed. Named stateless channels can be accessed from other servers.
  • Channels created with scope Eliom_common.default_process_scope must be created inside a service handler. They are assigned to a particular client process. Different channels created with the same stream do not share memory. They are closed when requested or when the client process is closed. It is possible to know when a client stop requesting data on those channels using Eliom_comet.Channel.wait_timeout. Be careful about memory consumption when using client process channels.

Comet configuration

The server can push data to a client only when the client has an open HTTP connection waiting for answer. As of now, a comet request can only last at most 10 seconds. After that, the client can either do a new request or stale for some time: this is the activity behaviour. This can be configured on client side, using the functions from Eliom_comet.Configuration

For instance if you receive data which doesn't need frequent update, you could set the time between different requests quite high and stop requesting data as soon as the browser looses the focus.

open Eliom_comet.Configuration
let slow_c = new_configuration () in
set_active_until_timeout slow_c false;
set_time_between_request slow_c 60.

If you need more reactivity for a few seconds, do:

open Eliom_comet.Configuration
let fast_c = new_configuration () in
set_set_always_active fast_c true;
set_set_time_between_request fast_c 0.;
ignore (Lwt_js.sleep 10. >|= (fun () -> drop_configuration fast_c))

The original setting will be reset after the drop.

Reactive values

A common usage of comet is for the server to update a value available on client side. A convenient way to implement this is to use reactive programming. Eliom provides a reactive interface for channels, using the react. library.

To share a React event or signal with the client, use functions Eliom_react.Down.of_react or Eliom_react.S.Down.of_react

On client side the value returned by those functions is directly a React event or signal.

The contrary is also available using Eliom_react.Up.create.

Since this is implemented using comet, tuning comet configuration will also affect the behaviour of shared react variables.

Client-Server shared bus

It is sometimes useful to have a bidirectional channels shared between multiple clients. This is the purpose of buses. Those are created using Eliom_bus.create. Since the server will also receive data on the bus, the description of the type (using deriving) is needed to create a bus.

Like comet channels, the behaviour of buses can be tuned using the module Eliom_comet.Configuration. There are additionnal configurations available on buses to tune the client side buffering.

Another Server sending data (Comet on another server)

It is possible to access a named stateless channel created on another server. It has to be declared using Eliom_comet.Channel.external_channel. The declaration of the channel must match exactly the creation. The server generating the page and the server that created the channel must run exactly the same version of Eliom. By default a browser can't do requests to a different server, to allow that the server serving the channel must allow Cross-Origin Resource Sharing using the CORS Ocsigenserver extension.