From b601c64ff6bc5c7dc22aaf784fed2c29e6b7f4a1 Mon Sep 17 00:00:00 2001 From: Thibaut Mattio Date: Wed, 28 Apr 2021 21:11:59 +0200 Subject: [PATCH 1/2] Add hot reloading example --- example/w-hot-reloading/README.md | 17 +++++ example/w-hot-reloading/dune | 4 ++ example/w-hot-reloading/dune-project | 1 + example/w-hot-reloading/esy.json | 10 +++ example/w-hot-reloading/hot_reloading.ml | 89 ++++++++++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 example/w-hot-reloading/README.md create mode 100644 example/w-hot-reloading/dune create mode 100644 example/w-hot-reloading/dune-project create mode 100644 example/w-hot-reloading/esy.json create mode 100644 example/w-hot-reloading/hot_reloading.ml diff --git a/example/w-hot-reloading/README.md b/example/w-hot-reloading/README.md new file mode 100644 index 00000000..1be131f2 --- /dev/null +++ b/example/w-hot-reloading/README.md @@ -0,0 +1,17 @@ +# `w-stress-response` + +
+ +This example demonstrates how to setup hot reloading of the client HTML content. + +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. + +This example plays very well with `w-fswatch`, which demonstrates how to restart the server every time the filesystem is modified. +When integrating the two examples, one is able to have a setup where the clients' pages are reloaded every time a file is modified. + +
+ +[Up to the example index](../#examples) diff --git a/example/w-hot-reloading/dune b/example/w-hot-reloading/dune new file mode 100644 index 00000000..c432d8c4 --- /dev/null +++ b/example/w-hot-reloading/dune @@ -0,0 +1,4 @@ +(executable + (name hot_reloading) + (libraries dream lambdasoup) + (preprocess (pps lwt_ppx))) diff --git a/example/w-hot-reloading/dune-project b/example/w-hot-reloading/dune-project new file mode 100644 index 00000000..929c696e --- /dev/null +++ b/example/w-hot-reloading/dune-project @@ -0,0 +1 @@ +(lang dune 2.0) diff --git a/example/w-hot-reloading/esy.json b/example/w-hot-reloading/esy.json new file mode 100644 index 00000000..2e1b62ec --- /dev/null +++ b/example/w-hot-reloading/esy.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "@opam/dream": "aantron/dream:dream.opam", + "@opam/dune": "^2.0", + "ocaml": "4.12.x" + }, + "scripts": { + "start": "dune exec --root . ./hot_reloading.exe" + } +} diff --git a/example/w-hot-reloading/hot_reloading.ml b/example/w-hot-reloading/hot_reloading.ml new file mode 100644 index 00000000..23541ebd --- /dev/null +++ b/example/w-hot-reloading/hot_reloading.ml @@ -0,0 +1,89 @@ +let hotreload_script + ?(retry_interval_ms = 500) + ?(max_retry_ms = 5000) + ?(route = "/_hotreload") + () + = + 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 inject_hotreload_script + ?(reload_script = hotreload_script ()) + () + (next_handler : Dream.request -> Dream.response Lwt.t) + (request : Dream.request) + : Dream.response Lwt.t + = + let%lwt response = next_handler request in + match Dream.header "Content-Type" response with + | Some "text/html" | Some "text/html; charset=utf-8" -> + let%lwt body = Dream.body response in + let soup = Soup.parse body in + let open Soup.Infix in + (match soup $? "head" with + | None -> + Lwt.return response + | Some head -> + Soup.create_element "script" ~inner_text:reload_script + |> Soup.append_child head; + Lwt.return (Dream.with_body (Soup.to_string soup) response)) + | _ -> + Lwt.return response + +let hotreload_route ?(path = "/_hotreload") () = + Dream.get path (fun _ -> + Dream.websocket (fun socket -> + Lwt.bind (Dream.receive socket) (fun _ -> + Dream.close_websocket socket))) + +let () = + Dream.run + @@ Dream.logger + @@ inject_hotreload_script () + @@ Dream.router [ + hotreload_route (); + Dream.get "/" + (fun _ -> + Dream.html "Good morning, world!"); + ] + @@ Dream.not_found From 5c35504e88c444122ae6139a0dd7ee815fcb9c48 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 3 May 2021 12:36:05 +0200 Subject: [PATCH 2/2] Rename hot reload to live reload --- .../README.md | 4 ++-- example/{w-hot-reloading => w-live-reloading}/dune | 2 +- .../dune-project | 0 .../{w-hot-reloading => w-live-reloading}/esy.json | 2 +- .../live_reloading.ml} | 14 +++++++------- 5 files changed, 11 insertions(+), 11 deletions(-) rename example/{w-hot-reloading => w-live-reloading}/README.md (86%) rename example/{w-hot-reloading => w-live-reloading}/dune (75%) rename example/{w-hot-reloading => w-live-reloading}/dune-project (100%) rename example/{w-hot-reloading => w-live-reloading}/esy.json (72%) rename example/{w-hot-reloading/hot_reloading.ml => w-live-reloading/live_reloading.ml} (89%) diff --git a/example/w-hot-reloading/README.md b/example/w-live-reloading/README.md similarity index 86% rename from example/w-hot-reloading/README.md rename to example/w-live-reloading/README.md index 1be131f2..954ae54e 100644 --- a/example/w-hot-reloading/README.md +++ b/example/w-live-reloading/README.md @@ -1,8 +1,8 @@ -# `w-stress-response` +# `w-live-reload`
-This example demonstrates how to setup hot reloading of the client HTML content. +This example demonstrates how to setup live reloading of the client HTML content. It works by injecting a script in the HTML pages sent to clients that will initiate a WebSocket. diff --git a/example/w-hot-reloading/dune b/example/w-live-reloading/dune similarity index 75% rename from example/w-hot-reloading/dune rename to example/w-live-reloading/dune index c432d8c4..dafcbcdf 100644 --- a/example/w-hot-reloading/dune +++ b/example/w-live-reloading/dune @@ -1,4 +1,4 @@ (executable - (name hot_reloading) + (name live_reloading) (libraries dream lambdasoup) (preprocess (pps lwt_ppx))) diff --git a/example/w-hot-reloading/dune-project b/example/w-live-reloading/dune-project similarity index 100% rename from example/w-hot-reloading/dune-project rename to example/w-live-reloading/dune-project diff --git a/example/w-hot-reloading/esy.json b/example/w-live-reloading/esy.json similarity index 72% rename from example/w-hot-reloading/esy.json rename to example/w-live-reloading/esy.json index 2e1b62ec..ed4a56fc 100644 --- a/example/w-hot-reloading/esy.json +++ b/example/w-live-reloading/esy.json @@ -5,6 +5,6 @@ "ocaml": "4.12.x" }, "scripts": { - "start": "dune exec --root . ./hot_reloading.exe" + "start": "dune exec --root . ./live_reloading.exe" } } diff --git a/example/w-hot-reloading/hot_reloading.ml b/example/w-live-reloading/live_reloading.ml similarity index 89% rename from example/w-hot-reloading/hot_reloading.ml rename to example/w-live-reloading/live_reloading.ml index 23541ebd..26234c4e 100644 --- a/example/w-hot-reloading/hot_reloading.ml +++ b/example/w-live-reloading/live_reloading.ml @@ -1,7 +1,7 @@ -let hotreload_script +let livereload_script ?(retry_interval_ms = 500) ?(max_retry_ms = 5000) - ?(route = "/_hotreload") + ?(route = "/_livereload") () = Printf.sprintf @@ -47,8 +47,8 @@ s.onerror = function(event) { retry_interval_ms max_retry_ms -let inject_hotreload_script - ?(reload_script = hotreload_script ()) +let inject_livereload_script + ?(reload_script = livereload_script ()) () (next_handler : Dream.request -> Dream.response Lwt.t) (request : Dream.request) @@ -70,7 +70,7 @@ let inject_hotreload_script | _ -> Lwt.return response -let hotreload_route ?(path = "/_hotreload") () = +let livereload_route ?(path = "/_livereload") () = Dream.get path (fun _ -> Dream.websocket (fun socket -> Lwt.bind (Dream.receive socket) (fun _ -> @@ -79,9 +79,9 @@ let hotreload_route ?(path = "/_hotreload") () = let () = Dream.run @@ Dream.logger - @@ inject_hotreload_script () + @@ inject_livereload_script () @@ Dream.router [ - hotreload_route (); + livereload_route (); Dream.get "/" (fun _ -> Dream.html "Good morning, world!");