diff --git a/dream.opam b/dream.opam
index 07e14a0b..307cabe5 100644
--- a/dream.opam
+++ b/dream.opam
@@ -60,11 +60,13 @@ depends: [
"fmt" {>= "0.8.7"} # `Italic.
"graphql_parser"
"graphql-lwt"
+ "lambdasoup" {>= "0.6.1"}
"lwt"
"lwt_ppx" {>= "1.2.2"}
"lwt_ssl"
"logs" {>= "0.5.0"}
"magic-mime"
+ "markup" {>= "1.0.2"}
"mirage-clock" {>= "3.0.0"} # now_d_ps : unit -> int * int64.
"mirage-crypto" {>= "0.8.1"} # AES-256-GCM.
"mirage-crypto-rng"
@@ -85,7 +87,6 @@ depends: [
"crunch" {with-test}
"js_of_ocaml" {with-test}
"js_of_ocaml-ppx" {with-test}
- "lambdasoup" {with-test}
"ppx_expect" {with-test & >= "v0.15.0"} # Formatting changes.
"ppx_yojson_conv" {with-test}
"reason" {with-test}
diff --git a/example/w-live-reload/README.md b/example/w-live-reload/README.md
index 0e66c545..d9d9600d 100644
--- a/example/w-live-reload/README.md
+++ b/example/w-live-reload/README.md
@@ -2,83 +2,20 @@
-This example shows a simple live reloading setup. It works by injecting a script
-into the `
` of HTML documents. The script opens a WebSocket back to the
-server. If the WebSocket gets closed, the script tries to reconnect. When the
-server comes back up, the client is able to reconnect, and reloads itself.
+This example shows a simple live reloading setup using the `Dream.livereload`
+middleware. It works by injecting a script into the `` of HTML documents.
+The script opens a WebSocket back to the server. If the WebSocket gets closed,
+the script tries to reconnect. When the server comes back up, the client is able
+to reconnect and reloads itself.
-```js
-var socketUrl = "ws://" + location.host + "/_live-reload"
-var socket = new WebSocket(socketUrl);
-
-socket.onclose = function(event) {
- const intervalMs = 100;
- const attempts = 100;
- let attempt = 0;
-
- function reload() {
- ++attempt;
-
- if(attempt > attempts) {
- console.error("Could not reconnect to server");
- return;
- }
-
- reconnectSocket = new WebSocket(socketUrl);
-
- reconnectSocket.onerror = function(event) {
- setTimeout(reload, intervalMs);
- };
-
- reconnectSocket.onopen = function(event) {
- location.reload();
- };
- };
-
- reload();
-};
-```
-
-The injection is done by a small middleware:
-
-```ocaml
-
-let inject_live_reload_script inner_handler request =
- let%lwt response = inner_handler request in
-
- match Dream.header "Content-Type" response with
- | Some "text/html; charset=utf-8" ->
- let%lwt body = Dream.body response in
- let soup =
- Markup.string body
- |> Markup.parse_html ~context:`Document
- |> Markup.signals
- |> Soup.from_signals
- in
-
- begin match Soup.Infix.(soup $? "head") with
- | None ->
- Lwt.return response
- | Some head ->
- Soup.create_element "script" ~inner_text:live_reload_script
- |> Soup.append_child head;
- response
- |> Dream.with_body (Soup.to_string soup)
- |> Lwt.return
- end
-
- | _ ->
- Lwt.return response
-```
-
-The example server just wraps a single page at `/` with the middleware. The page
+The example server just wraps a single page at `/` with the `Dream.livereload` middleware. The page
displays a tag that changes each time it is loaded:
```ocaml
let () =
Dream.run
@@ Dream.logger
- @@ inject_live_reload_script
+ @@ Dream.livereload
@@ Dream.router [
Dream.get "/" (fun _ ->
@@ -87,11 +24,6 @@ let () =
|> Printf.sprintf "Good morning, world! Random tag: %s"
|> Dream.html);
- Dream.get "/_live-reload" (fun _ ->
- Dream.websocket (fun socket ->
- let%lwt _ = Dream.receive socket in
- Dream.close_websocket socket));
-
]
```
@@ -129,7 +61,7 @@ changes.
**See also:**
- [**`k-websocket`**](../k-websocket#files) introduces WebSockets.
-- [**`w-fswatch`**](../w-fswatch#files) rebuilds and restarts a server each
+- [**`w-watch`**](../w-watch#files) rebuilds and restarts a server each
time its source code changes.
diff --git a/example/w-live-reload/live_reload.ml b/example/w-live-reload/live_reload.ml
index 999c53b3..d186314d 100644
--- a/example/w-live-reload/live_reload.ml
+++ b/example/w-live-reload/live_reload.ml
@@ -1,67 +1,7 @@
-let live_reload_script = {js|
-
-var socketUrl = "ws://" + location.host + "/_live-reload"
-var socket = new WebSocket(socketUrl);
-
-socket.onclose = function(event) {
- const intervalMs = 100;
- const attempts = 100;
- let attempt = 0;
-
- function reload() {
- ++attempt;
-
- if(attempt > attempts) {
- console.error("Could not reconnect to server");
- return;
- }
-
- reconnectSocket = new WebSocket(socketUrl);
-
- reconnectSocket.onerror = function(event) {
- setTimeout(reload, intervalMs);
- };
-
- reconnectSocket.onopen = function(event) {
- location.reload();
- };
- };
-
- reload();
-};
-
-|js}
-
-let inject_live_reload_script inner_handler request =
- let%lwt response = inner_handler request in
-
- match Dream.header response "Content-Type" with
- | Some "text/html; charset=utf-8" ->
- let%lwt body = Dream.body response in
- let soup =
- Markup.string body
- |> Markup.parse_html ~context:`Document
- |> Markup.signals
- |> Soup.from_signals
- in
-
- begin match Soup.Infix.(soup $? "head") with
- | None ->
- Lwt.return response
- | Some head ->
- Soup.create_element "script" ~inner_text:live_reload_script
- |> Soup.append_child head;
- Dream.set_body response (Soup.to_string soup);
- Lwt.return response
- end
-
- | _ ->
- Lwt.return response
-
let () =
Dream.run
@@ Dream.logger
- @@ inject_live_reload_script
+ @@ Dream.livereload ()
@@ Dream.router [
Dream.get "/" (fun _ ->
@@ -70,9 +10,4 @@ let () =
|> Printf.sprintf "Good morning, world! Random tag: %s"
|> Dream.html);
- Dream.get "/_live-reload" (fun _ ->
- Dream.websocket (fun socket ->
- let%lwt _ = Dream.receive socket in
- Dream.close_websocket socket));
-
]
diff --git a/src/dream.ml b/src/dream.ml
index 7409ac00..a497fcd0 100644
--- a/src/dream.ml
+++ b/src/dream.ml
@@ -17,6 +17,7 @@ module Formats = Dream_pure.Formats
module Graphql = Dream__graphql.Graphql
module Helpers = Dream__server.Helpers
module Http = Dream__http.Http
+module Livereload = Dream__server.Livereload
module Message = Dream_pure.Message
module Method = Dream_pure.Method
module Origin_referrer_check = Dream__server.Origin_referrer_check
@@ -216,6 +217,7 @@ let csrf_tag = Tag.csrf_tag ~now
let no_middleware = Message.no_middleware
let pipeline = Message.pipeline
+let livereload = Livereload.livereload
diff --git a/src/dream.mli b/src/dream.mli
index 4d122e2f..3ce21cf9 100644
--- a/src/dream.mli
+++ b/src/dream.mli
@@ -1304,6 +1304,17 @@ val no_middleware : middleware
Dream.no_middleware
]} *)
+val livereload : ?script:string -> ?path:string -> unit -> middleware
+(** Adds live reloading to your Dream application.
+
+ It works by injecting a script in the HTML pages sent to clients that will
+ initiate a WebSocket.
+
+ When the server restarts, the WebSocket connection is lost, at which point,
+ the client will try to reconnect every 500ms for 5s. If within these 5s the
+ client is able to reconnect to the server, it will trigger a reload of the
+ page. *)
+
val pipeline : middleware list -> middleware
(** Combines a sequence of middlewares into one, such that these two lines are
equivalent:
diff --git a/src/server/dune b/src/server/dune
index d7dd67b7..9fdc34cf 100644
--- a/src/server/dune
+++ b/src/server/dune
@@ -6,6 +6,7 @@
dream.cipher
dream-pure
fmt
+ lambdasoup
logs
lwt
magic-mime
diff --git a/src/server/livereload.ml b/src/server/livereload.ml
new file mode 100644
index 00000000..0ca5f0c6
--- /dev/null
+++ b/src/server/livereload.ml
@@ -0,0 +1,78 @@
+(* This file is part of Dream, released under the MIT license. See LICENSE.md
+ for details, or visit https://github.com/aantron/dream.
+
+ Copyright 2021 Anton Bachin *)
+
+module Message = Dream_pure.Message
+
+let default_script ?(retry_interval_ms = 500) ?(max_retry_ms = 5000)
+ ?(route = "/_livereload") () =
+ Printf.sprintf
+ {js|
+var socketUrl = "ws://" + location.host + "%s"
+var s = new WebSocket(socketUrl);
+
+s.onopen = function(even) {
+ console.log("WebSocket connection open.");
+};
+
+s.onclose = function(even) {
+ console.log("WebSocket connection closed.");
+ const innerMs = %i;
+ const maxMs = %i;
+ const maxAttempts = Math.round(maxMs / innerMs);
+ let attempts = 0;
+ function reload() {
+ attempts++;
+ if(attempts > maxAttempts) {
+ console.error("Could not reconnect to dev server.");
+ return;
+ }
+
+ s2 = new WebSocket(socketUrl);
+
+ s2.onerror = function(event) {
+ setTimeout(reload, innerMs);
+ };
+
+ s2.onopen = function(event) {
+ location.reload();
+ };
+ };
+ reload();
+};
+
+s.onerror = function(event) {
+ console.error("WebSocket error observed:", event);
+};
+|js}
+ route retry_interval_ms max_retry_ms
+
+let livereload ?(script = default_script ()) ?(path = "/_livereload") ()
+ (next_handler : Message.request -> Message.response Lwt.t)
+ (request : Message.request) : Message.response Lwt.t =
+ match Message.target request with
+ | target when target = path ->
+ Helpers.websocket (fun socket ->
+ Lwt.bind (Helpers.receive socket) (fun _ ->
+ Message.close_websocket socket))
+ | _ -> (
+ let%lwt response = next_handler request in
+ match Message.header response "Content-Type" with
+ | Some "text/html" | Some "text/html; charset=utf-8" -> (
+ let%lwt body = Message.body response in
+ let soup =
+ Markup.string body
+ |> Markup.parse_html ~context:`Document
+ |> Markup.signals
+ |> Soup.from_signals
+ in
+ let open Soup.Infix in
+ match soup $? "head" with
+ | None -> Lwt.return response
+ | Some head ->
+ Soup.create_element "script" ~inner_text:script
+ |> Soup.append_child head;
+ Message.set_body response (Soup.to_string soup);
+ Lwt.return response)
+ | _ -> Lwt.return response)