Skip to content

Commit 9000428

Browse files
authored
Merge pull request #424 from krymtkts/feature/dev-server
Refactor dev server live reload to use SSE instead of WebSockets
2 parents f18f7a9 + 18c5d16 commit 9000428

File tree

3 files changed

+72
-57
lines changed

3 files changed

+72
-57
lines changed

src/App.fsproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
</ItemGroup>
1818
<ItemGroup>
1919
<PackageReference Include="Fable.Browser.Dom" Version="2.20.0" />
20-
<PackageReference Include="Fable.Browser.WebSocket" Version="1.4.0" />
2120
<PackageReference Include="Fable.Browser.WebStorage" Version="1.3.0" />
2221
<PackageReference Include="Fable.Core" Version="4.5.0" />
2322
<PackageReference Include="Fable.Promise" Version="3.2.0" />

src/Dev.fs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
11
module Dev
22

33
open Browser.Dom
4-
open Browser.WebSocket
4+
open Fable.Core
55

6-
let private initLiveReloading _ =
7-
// NOTE: don't use string interpolation here, it will break the code because of importing String module.
8-
let ws = WebSocket.Create <| "ws://" + window.location.host + "/websocket"
6+
[<Emit("typeof EventSource !== 'undefined'")>]
7+
let private hasEventSource: bool = jsNative
8+
9+
[<AllowNullLiteral>]
10+
type private IEventSource =
11+
abstract addEventListener: string * (obj -> unit) -> unit
12+
abstract close: unit -> unit
13+
abstract onmessage: (obj -> unit) with get, set
14+
15+
[<Emit("new EventSource($0)")>]
16+
let private createEventSource (_url: string) : IEventSource = jsNative
17+
18+
let private initLiveReloadingViaSse () =
19+
let es = createEventSource "/sse"
920

10-
ws.onmessage <-
11-
fun _ ->
12-
ws.close (1000, "reload")
13-
window.location.reload ()
21+
let reload (_: obj) =
22+
es.close ()
23+
window.location.reload ()
1424

15-
window.addEventListener ("beforeunload", (fun _ -> ws.close ()))
25+
es.addEventListener ("refresh", reload)
26+
es.onmessage <- reload
27+
28+
window.addEventListener ("beforeunload", (fun _ -> es.close ()))
29+
30+
let private initLiveReloading _ =
31+
// SSE only.
32+
// If the browser doesn't support EventSource, do nothing.
33+
if hasEventSource then
34+
initLiveReloadingViaSse ()
1635

1736
window.addEventListener ("load", initLiveReloading)

test/DevServer.fs

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ open Suave
88
open Suave.Filters
99
open Suave.Operators
1010
open Suave.Sockets
11-
open Suave.Sockets.Control
12-
open Suave.Utils
13-
open Suave.WebSocket
1411
open System
1512
open System.Net
13+
open System.Net.Sockets
1614
open System.Threading
15+
open System.Threading.Tasks
1716

1817
let port =
1918
let rec findPort port =
@@ -25,14 +24,16 @@ let port =
2524

2625
findPort 8080us
2726

27+
[<RequireQualifiedAccess>]
28+
[<Struct>]
2829
type BuildEvent =
2930
| BuildFable
3031
| BuildMd
3132
| BuildStyle
3233
| Noop
3334

34-
let (handleWatcherEvents: FileChange seq -> unit), socketHandler =
35-
let refreshEvent = new Event<_>()
35+
let (handleWatcherEvents: FileChange seq -> unit), sseHandler =
36+
let refreshEvent = new Event<unit>()
3637

3738
let buildFable () =
3839
let cmd = "fable src"
@@ -59,24 +60,26 @@ let (handleWatcherEvents: FileChange seq -> unit), socketHandler =
5960
|> Seq.map (fun e ->
6061
let fi = FileInfo.ofPath e.FullPath
6162

62-
Trace.traceImportant $"%s{fi.FullName} was changed."
63+
Trace.traceImportant $"%s{fi.FullName} was changed. ext: %s{fi.Extension}"
6364

64-
match fi.FullName with
65-
| x when x.EndsWith(".fs") -> BuildFable
66-
| x when x.EndsWith(".md") || x.EndsWith(".yml") || x.EndsWith(".yaml") -> BuildMd
67-
| x when x.EndsWith(".scss") -> BuildStyle
68-
| _ -> Noop)
65+
match fi.Extension with
66+
| ".fs" -> BuildEvent.BuildFable
67+
| ".md"
68+
| ".yml"
69+
| ".yaml" -> BuildEvent.BuildMd
70+
| ".scss" -> BuildEvent.BuildStyle
71+
| _ -> BuildEvent.Noop)
6972
|> Set.ofSeq
7073

7174
let fableOrMd =
72-
match [ BuildFable; BuildMd ] |> List.map es.Contains with
75+
match [ BuildEvent.BuildFable; BuildEvent.BuildMd ] |> List.map es.Contains with
7376
| [ true; true ] -> buildFable ()
7477
| [ _; true ] -> buildMd ()
7578
| [ true; _ ] -> buildFable ()
7679
| _ -> Ok false
7780

7881
let style =
79-
match es |> Set.contains BuildStyle with
82+
match es |> Set.contains BuildEvent.BuildStyle with
8083
| true -> buildStyle ()
8184
| _ -> Ok false
8285

@@ -87,39 +90,33 @@ let (handleWatcherEvents: FileChange seq -> unit), socketHandler =
8790
printfn "refresh event is triggered."
8891
| _ -> printfn "refresh event not triggered."
8992

90-
let socketHandler (ws: WebSocket) _ =
91-
let rec refreshLoop (ct: CancellationToken) =
92-
task {
93-
ct.ThrowIfCancellationRequested()
94-
do! refreshEvent.Publish |> Async.AwaitEvent
95-
96-
printfn "refresh client."
97-
let seg = ASCII.bytes "refreshed" |> ByteSegment
98-
let! _ = ws.send Text seg true
99-
100-
return! refreshLoop ct
101-
}
102-
103-
let rec mainLoop (cts: CancellationTokenSource) =
104-
socket {
105-
let! msg = ws.read ()
106-
107-
match msg with
108-
| Close, _, _ ->
109-
// use _ = cts
110-
cts.Cancel()
111-
112-
let emptyResponse = [||] |> ByteSegment
113-
do! ws.send Close emptyResponse true
114-
printfn "WebSocket connection closed gracefully."
115-
| _ -> return! mainLoop cts
116-
}
117-
118-
let cts = new CancellationTokenSource()
119-
refreshLoop cts.Token |> ignore
120-
mainLoop cts
121-
122-
handleWatcherEvents, socketHandler
93+
let sseHandler (conn: Connection) : Task<unit> =
94+
task {
95+
try
96+
// NOTE: Tell the browser how long to wait before retrying the connection.
97+
do! EventSource.retry conn 1000u
98+
99+
// NOTE: Initial event so the client can confirm it is connected.
100+
do! EventSource.eventType conn "connected"
101+
do! EventSource.data conn "ok"
102+
do! EventSource.dispatch conn
103+
104+
while true do
105+
do!
106+
Async.StartAsTask(
107+
refreshEvent.Publish |> Async.AwaitEvent,
108+
cancellationToken = CancellationToken.None
109+
)
110+
111+
do! EventSource.eventType conn "refresh"
112+
do! EventSource.data conn "refreshed"
113+
do! EventSource.dispatch conn
114+
with
115+
| :? OperationCanceledException -> ()
116+
| :? SocketException -> ()
117+
}
118+
119+
handleWatcherEvents, sseHandler
123120

124121
let suaveConfig (home: string) (ct: CancellationToken) =
125122
let home = IO.Path.GetFullPath home
@@ -149,7 +146,7 @@ let webpart (root: string) : WebPart =
149146

150147
choose [
151148

152-
path "/websocket" >=> handShake socketHandler
149+
path "/sse" >=> EventSource.handShake sseHandler
153150

154151
GET
155152
>=> Writers.setHeader "Cache-Control" "no-cache, no-store, must-revalidate"

0 commit comments

Comments
 (0)