Skip to content

Commit ee4a0f8

Browse files
authored
Bump Suave from 2.6.2 to 3.4.6 (#1272)
Migrate the fsdocs watch server to Suave 3's Task-based socket API: the websocket handler and reload broadcast now use ValueTask/Result, the removed Suave.Logging module is dropped, and the server task from startWebServerAsync is already hot so it no longer needs Async.Start. While migrating, fix a crash in watch mode: clients that disconnect without a close handshake stayed registered, and broadcasting to such a dead socket throws ObjectDisposedException in Suave 3, killing the whole watch process. Clients are now deregistered whenever their connection ends, and the broadcast tolerates stale sockets. Also in watch mode, the logo now links to the locally hosted site root instead of the production URL (even when <FsDocsLogoLink> is set), and the console no longer logs websocket connection chatter on every page reload. Release builds are unaffected. Bump Fun.Build from 1.0.4 to 1.1.18 and make build.fsx directly executable via a dotnet fsi shebang. Release notes: 22.2.0.
1 parent d591b93 commit ee4a0f8

4 files changed

Lines changed: 60 additions & 19 deletions

File tree

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<PackageVersion Include="Microsoft.Build.Tasks.Core" Version="" PrivateAssets="all" />
1616
<PackageVersion Include="Ionide.ProjInfo" Version="0.74.2" />
1717
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
18-
<PackageVersion Include="Suave" Version="2.6.2" />
18+
<PackageVersion Include="Suave" Version="3.4.6" />
1919
<PackageVersion Include="System.Memory" Version="4.6.3" />
2020
<PackageVersion Include="System.Text.Json" Version="10.0.8" />
2121
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />

RELEASE_NOTES.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22

3-
## [Unreleased]
3+
## [22.2.0] - 2026-08-31
4+
5+
### Changed
6+
* Bump `Suave` from 2.6.2 to 3.4.6. The `fsdocs watch` websocket handling was migrated to Suave 3's `Task`-based socket API, the removed `Suave.Logging` usage was dropped, and clients that disconnect without a close handshake are now deregistered so a live-reload broadcast can no longer crash the watch server with an `ObjectDisposedException`.
7+
* During `fsdocs watch`, the logo now links to the locally hosted site root (e.g. `http://localhost:8901/`) instead of the production URL, even when `<FsDocsLogoLink>` is set. Release builds are unaffected.
8+
* The `fsdocs watch` console no longer logs websocket connection chatter ("New websocket connection", "WebSocket disconnected", ...) on every page reload.
49

510
### Removed
611
* Remove `docs/Dockerfile` (used for mybinder.org Binder integration) and mybinder badge links from documentation pages. The Binder integration relied on a deprecated .NET 7 SDK image and a deprecated `Microsoft.dotnet-interactive` version; mybinder.org support is discontinued.

build.fsx

100644100755
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
#r "nuget: Fun.Build, 1.0.4"
1+
#!/usr/bin/env -S dotnet fsi --
2+
#r "nuget: Fun.Build, 1.1.18"
23
#r "nuget: Fake.IO.FileSystem, 6.0.0"
34
#r "nuget: Ionide.KeepAChangelog, 0.1.8"
45

src/fsdocs-tool/BuildCommand.fs

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ open Suave.Sockets.Control
2727
open Suave.WebSocket
2828
open Suave.Operators
2929
open Suave.Filters
30-
open Suave.Logging
3130
open FSharp.Formatting.Markdown
3231

3332

@@ -860,30 +859,50 @@ module Serve =
860859

861860
let connectedClients = ConcurrentDictionary<WebSocket, unit>()
862861

863-
let socketHandler (webSocket: WebSocket) (context: HttpContext) =
864-
context.runtime.logger.info (Message.eventX "New websocket connection")
862+
let socketHandler (webSocket: WebSocket) (_context: HttpContext) : SocketOp<unit> =
865863
connectedClients.TryAdd(webSocket, ()) |> ignore
866864

867-
socket {
868-
let! msg = webSocket.read ()
865+
Threading.Tasks.ValueTask<Result<unit, Sockets.Error>>(
866+
task {
867+
try
868+
// Block until the client sends a message or disconnects.
869+
let! msg = (webSocket.read ()).AsTask()
870+
871+
match msg with
872+
| Ok(Close, _, _) ->
873+
let emptyResponse = [||] |> ByteSegment
874+
let! _ = (webSocket.send Close emptyResponse true).AsTask()
875+
()
876+
| _ -> ()
877+
with _ ->
878+
()
869879

870-
match msg with
871-
| Close, _, _ ->
872-
context.runtime.logger.info (Message.eventX "Closing connection")
880+
// Deregister the client however the connection ended, so reload
881+
// broadcasts never touch a dead (and possibly recycled) socket.
873882
connectedClients.TryRemove webSocket |> ignore
874-
let emptyResponse = [||] |> ByteSegment
875-
do! webSocket.send Close emptyResponse true
876-
| _ -> ()
877-
}
883+
884+
// Return Ok even when the client vanished without a close handshake,
885+
// otherwise Suave writes a "WebSocket disconnected" line to the console.
886+
return Ok()
887+
}
888+
)
878889

879890
let broadCastReload (msg: string) =
880891
let msg = msg |> Encoding.UTF8.GetBytes |> ByteSegment
881892

882893
connectedClients.Keys
883894
|> Seq.map (fun client ->
884895
async {
885-
let! _ = client.send Text msg true
886-
()
896+
try
897+
let! result = (client.send Text msg true).AsTask() |> Async.AwaitTask
898+
899+
match result with
900+
| Ok() -> ()
901+
| Result.Error _ -> connectedClients.TryRemove client |> ignore
902+
with _ ->
903+
// Suave 3 throws (e.g. ObjectDisposedException) when the client
904+
// disconnected without a close handshake; drop the stale client.
905+
connectedClients.TryRemove client |> ignore
887906
})
888907
|> Async.Parallel
889908
|> Async.Ignore
@@ -1312,7 +1331,8 @@ module Serve =
13121331
>=> Writers.setHeader "Expires" "0"
13131332
>=> Files.browseHome ]
13141333

1315-
startWebServerAsync serverConfig app |> snd |> Async.Start
1334+
// In Suave 3.x the server part of the tuple is a hot Task, no explicit start needed.
1335+
startWebServerAsync serverConfig app |> snd |> ignore
13161336

13171337
/// Helpers for generating llms.txt and llms-full.txt content.
13181338
module internal LlmsTxt =
@@ -1648,6 +1668,21 @@ type CoreBuildOptions(watch) =
16481668
// See https://github.com/ionide/proj-info/issues/123
16491669
System.Environment.SetEnvironmentVariable("DOTNET_HOST_PATH", prevDotnetHostPath)
16501670

1671+
// In watch mode the logo must link to the locally hosted site, even when
1672+
// <FsDocsLogoLink> is set to a production URL for the published site.
1673+
let overrideLogoLinkForWatch substitutions =
1674+
if watch then
1675+
substitutions
1676+
|> List.map (fun (pk, v) ->
1677+
if pk = ParamKeys.``fsdocs-logo-link`` then
1678+
(pk, root)
1679+
else
1680+
(pk, v))
1681+
else
1682+
substitutions
1683+
1684+
let docsSubstitutions = overrideLogoLinkForWatch docsSubstitutions
1685+
16511686
if crackedProjects.Length > 0 then
16521687
printfn ""
16531688
printfn "Inputs for API Docs:"
@@ -1725,7 +1760,7 @@ type CoreBuildOptions(watch) =
17251760
XmlFile = None
17261761
SourceRepo = sourceRepo
17271762
SourceFolder = Some sourceFolder
1728-
Substitutions = Some projectParameters
1763+
Substitutions = Some(overrideLogoLinkForWatch projectParameters)
17291764
MarkdownComments = this.mdcomments || projectMarkdownComments
17301765
Warn = projectWarn
17311766
PublicOnly = not this.nonpublic

0 commit comments

Comments
 (0)