Search code examples
f#suave

Suave in watch mode (during development)


I am working on Suave 1.0 + Angular 2.0 sample app and very interesting to start Suave server in watch mode, so the server watch file changes (js,css,html) in root folder and sub-folders and automatically send refresh command to all open browser tabs with my application when any file is changed.

lite-server from Angular 2 5min Quckstark can do this and it is very handy.

I think that most of watch pieces can be found in latest Steffen Forkmann's post but it is not very clean how to send refresh to the open browser tabs.

Please provide complete code of similar implementation with Suave.


Solution

  • The code of Suave server should looks similar to this

    #r "packages/Suave/lib/net40/suave.dll"
    #r "packages/FAKE/tools/FakeLib.dll"
    
    open Fake
    open Suave
    open Suave.Operators
    open Suave.Sockets.Control
    open Suave.WebSocket
    open Suave.Utils
    open Suave.Files
    open Suave.RequestErrors
    open Suave.Filters
    open System
    open System.Net
    
    
    let port =
        let rec findPort port =
            let portIsTaken =
                System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners()
                |> Seq.exists (fun x -> x.Port = int(port))
            if portIsTaken then findPort (port + 1us) else port
        findPort 8083us
    
    let logger = Logging.Loggers.ConsoleWindowLogger Logging.LogLevel.Verbose
    
    let refreshEvent = new Event<_>()
    
    let handleWatcherEvents (events:FileChange seq) =
        for e in events do
            let fi = fileInfo e.FullPath
            traceImportant <| sprintf "%s was changed." fi.Name
        refreshEvent.Trigger()
    
    let socketHandler (webSocket : WebSocket) =
      fun cx -> socket {
        while true do
          let! refreshed =
            Control.Async.AwaitEvent(refreshEvent.Publish)
            |> Suave.Sockets.SocketOp.ofAsync
          do! webSocket.send Text (ASCII.bytes "refreshed") true
      }
    
    let cfg =
      { defaultConfig with
          homeFolder = Some (__SOURCE_DIRECTORY__)
          bindings =
            [ HttpBinding.mk HTTP IPAddress.Loopback port ]
          listenTimeout = TimeSpan.FromMilliseconds 3000. }
    
    let app : WebPart =
      choose [
        Filters.log logger logFormat >=> never
        Filters.path "/websocket" >=> handShake socketHandler
        Filters.GET >=> Filters.path "/" >=> file "index.html"
        Writers.setHeader "Cache-Control" "no-cache, no-store, must-revalidate"
          >=> Writers.setHeader "Pragma" "no-cache"
          >=> Writers.setHeader "Expires" "0"
          >=> browseHome
        NOT_FOUND "Found no handlers."
      ]
    
    
    let watcher =
        !! ("app/*.js")
          ++ ("*.html")
        |> WatchChanges handleWatcherEvents
    
    try
        System.Diagnostics.Process.Start(sprintf "http://localhost:%d/index.html" port) |> ignore
        startWebServer cfg app
    finally
        watcher.Dispose()
    

    So we setup watcher that handle changes in js(generated by TypeScript) and html files and send refresh command to the client, but in the same time we need to add following code to the head section of index.html to handle refresh on the client side

    <!-- 3. Listen on refresh events from the server -->
    <script language="javascript" type="text/javascript">
        function init()
        {
            websocket = new WebSocket("ws://"+window.location.host+"/websocket");
            websocket.onmessage = function(evt) { location.reload(); };
        }
        window.addEventListener("load", init, false);
    </script>
    

    The full demo app you can find here