I want to build a simple counter with Suave.
[<EntryPoint>]
let main argv =
let mutable counter = 0;
let app =
choose
[
GET
>=> choose
[
path "/" >=> OK "Hello, world. ";
path "/count" >=> OK (string counter)
]
POST
>=> choose
[
path "/increment"
>=> (fun context -> async {
counter <- counter + 1
return Some context
})
]
]
startWebServer defaultConfig app
0
However, with my current solution, the count at /count never updates.
I think this is because the WebPart is computed when the app is launched, instead of for each request.
What is the best way to achieve this in Suave?
You are right in the assumption that Webparts are values, so computed once. (See this).
You need to use a closure to get what you want:
path "/count" >=> (fun ctx ->
async {
let c = counter in return! OK (string c) ctx
})
Related
What is the best practice is to "register" the http client in one place, so it can be reused from this Elmish update function? Instead of having to create it for every request.
let update message model =
match message with
| SetMessage s -> { model with x = s }
| Loading -> { model with x = "Doing something long ..." }
let handleClick model dispatch _ =
dispatch Loading
async {
let url = Uri "https://api.github.com"
-- FIXME: too expensive to do this on per-update basis
use httpClient = new HttpClient(BaseAddress = url)
let! resp = httpClient.GetAsync "/users/srid" |> Async.AwaitTask
let! s = resp.Content.ReadAsStringAsync() |> Async.AwaitTask
dispatch (SetMessage s)
} |> Async.Start
I feel like this would normally go in Startup.fs. I use a client-only Bolero web app, so this would look like:
builder.Services.AddSingleton<HttpClient>(new HttpClient (BaseAddress=apiBase))
But then the question becomes ... how do I access it from my program in F#? What is the idiomatic way?
Probably the best way would either be to add HttpClient as another field in your model or as another parameter to your update function.
let update (client:HttpClient) message model = // Your code
let url = Uri "https://api.github.com"
let httpClient = new HttpClient(BaseAddress = url)
In general you shouldn't "do work" in your view and, by extension, event handlers. Instead, you should use the Elmish Cmd module something like this:
let update httpClient message model =
match message with
| SetMessage s ->
{ model with x = s }, Cmd.none
| GetMessageAsync ->
let cmd =
let getHttp () =
async {
let! resp = httpClient.GetAsync "/users/srid" |> Async.AwaitTask
return! resp.Content.ReadAsStringAsync() |> Async.AwaitTask
}
Cmd.OfAsync.perform getHttp () (fun s -> SetMessage s)
{ model with x = "Doing something long ..." }, cmd
let handleClick model dispatch _ =
dispatch GetMessageAsync
I have the following Program.fs:
let webApp = choose [ setStatusCode 404 >=> text "Not Found" ]
let errorHandler (ex : Exception) (logger : ILogger) =
logger.LogError(ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> text ex.Message
let configureApp (app : IApplicationBuilder) =
app.UseHealthChecks(PathString("/health")) |> ignore
let env = app.ApplicationServices.GetService<IHostingEnvironment>()
(match env.IsDevelopment() with
| true -> app.UseDeveloperExceptionPage()
| false -> app.UseGiraffeErrorHandler errorHandler)
.UseHttpsRedirection()
.UseStaticFiles()
.UseGiraffe(webApp)
let configureServices (services : IServiceCollection) =
services.AddHealthChecks() |> ignore
services.AddCors() |> ignore
services.AddGiraffe() |> ignore
let configureLogging (builder : ILoggingBuilder) =
builder.AddFilter(fun l -> l.Equals LogLevel.Error)
.AddConsole()
.AddDebug() |> ignore
[<EntryPoint>]
let main _ =
let contentRoot = Directory.GetCurrentDirectory()
let webRoot = Path.Combine(contentRoot, "WebRoot")
WebHostBuilder()
.UseKestrel()
.UseContentRoot(contentRoot)
.UseIISIntegration()
.UseWebRoot(webRoot)
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
I would like that the default endpoint target the health check (ie. /health), how is that possible using F# Giraffe?
I followed the article given in comment: https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#redirection
and ended up on the following choose:
let webApp = choose [
GET >=>
choose [
route "/" >=> redirectTo false "/health"
]
setStatusCode 404 >=> text "Not Found" ]
I am trying out the example snippet titled "Adding client-side functionality" from the following page :
https://developers.websharper.com/docs/v4.x/fs/overview
It looks a bit outdated and doesn't compile as is, so based on the original repository where that code was snipped from, this is what I have now.
namespace TestSuaveWs
open WebSharper
open WebSharper.Sitelets
open WebSharper.UI
open WebSharper.UI.Html
open WebSharper.UI.Client
module Server =
[<Rpc>]
let DoWork (s: string) =
async {
return System.String(List.ofSeq s |> List.rev |> Array.ofList)
}
[<JavaScript>]
module Client =
open WebSharper.JavaScript
open WebSharper.Html.Client
let Main () =
let input = Input [ Attr.Value "" ]
let output = H1 []
Div [
input
Button [ Text "Send" ]
|>! OnClick (fun _ _ ->
async {
let! data = Server.DoWork input.Value
output.Text <- data
}
|> Async.Start
)
HR []
H4 [ Class "text-muted" ] -- Text "The server responded:"
Div [ Class "jumbotron" ] -< [ output ]
]
module TheSite =
open WebSharper.UI.Server
[<Website>]
let MySite =
Application.SinglePage (fun ctx ->
Content.Page(
Body = [
h1 [] [ text "Say Hi to the server" ]
div [] [ client <# Client.Main() #> ]
]
)
)
open global.Suave
open Suave.Web
open WebSharper.Suave
let webPart = WebSharperAdapter.ToWebPart(MySite, RootDirectory="../..")
Then there's the main program.
namespace TestSuaveWs
module Main =
open System
open System.Threading
open Suave
[<EntryPoint>]
let main argv =
let cts = new CancellationTokenSource()
let conf = { defaultConfig with cancellationToken = cts.Token }
let listening, server = startWebServerAsync conf TheSite.webPart
Async.Start(server, cts.Token)
printfn "Make requests now"
Console.ReadKey true |> ignore
cts.Cancel()
0
The program runs, and I can see the text "Say Hi to the server" on localhost:8080, but there is nothing below that text. A picture on the page with the example shows what it should look like. There's supposed to be a text input field, a button, and a reply text.
When I open Developer Tools in my Chrome browser, I can see that there's a bunch of similar messages, differing only in the javascript filename, that says "Failed to load resource: the server responded with a status of 404 WebSharper.Main.min.js:1 (Not Found)"
There are no *.js files in the bin\Debug folder. I am running in VS 2019, with .NET Framework 4.7.1.
What am I missing?
I wasn't aware that this very example was available as a template named "WebSharper 4 Suave-hosted Site", which I only found after downloading and installing the WebSharper vsix. That template spun up a project doing exactly what I tried to achieve. So that's the answer. I wish this was hinted at in the documentation page.
I am quite new to using WebSharper and I might be doing things the wrong way.
My goal is to be able to update the contents of my page as a result of user actions by updating a Var<Doc> variable representing a portion of the page to be updated. I'd be happy to know if I could update a Var<Doc> from server-side code and have it reflect in the user's browser.
Below is a quick example:
let TestPage ctx =
let clientPart = Var.Create <| Doc.Empty
clientPart .Value <- div [] [ text "This content is dynamically inserted" ]
Templating.Main ctx EndPoint.Home "Home" [
h1 [] [text "Below is a dynamically inserted content:"]
div [] [ client <# clientPart .View |> Doc.EmbedView #> ]
]
The error I receive is:
System.Exception: Error during RPC JSON conversion ---> System.Exception: Failed to look up translated field name for write' in type WebSharper.UI.Elt with fields: docNode, elt, rvUpdates, updates
The WebSharper 4 documentation regarding Views also states:
It will only be run while the resulting View is included in the document using one of these methods:
Doc.BindView
Doc.EmbedView
textView
and etc.
A similar error is produced if I try this instead:
type SomeTemplate = Template<"SomeTemplate.html">
clientDoc.Value <- SomeTemplate().Doc()
In the above code, Templating.Main is the same as in the default WebSharper project:
module Templating =
...
let Main ctx action (title: string) (body: Doc list) =
let t = MainTemplate().Title(title).MenuBar(MenuBar ctx action).With("Body", body)
let doc : Doc = t.Doc()
doc |> Content.Page
Here is an example calling an RPC on the server side and storing it into a client Var<>:
module ServerFunctions =
let mutable ServerState = ("Zero", 0)
let [< Rpc >] addToState n = async {
let state, counter = ServerState
let newCounter = counter + n
let newState = if newCounter = 0 then "Zero" else "NonZero"
ServerState <- newState, newCounter
return newState
}
[< JavaScript >]
module ClientFunctions =
open WebSharper
open WebSharper.UI
open WebSharper.UI.Html
open ServerFunctions
let zeroState = Var.Create "do not know"
let clientDoc() =
div [] [
h1 [] [ text "State of zero on server:" ]
h2 [] [ text zeroState.V ]
Doc.Button "increment" [] (fun () -> async { let! state = addToState 1
zeroState.Set state
} |> Async.Start)
Doc.Button "decrement" [] (fun () -> async { let! state = addToState -1
zeroState.Set state
} |> Async.Start)
]
module Server =
open global.Owin
open Microsoft.Owin.Hosting
open Microsoft.Owin.StaticFiles
open Microsoft.Owin.FileSystems
open WebSharper.Owin
open WebSharper.UI.Server
open WebSharper.UI.Html
type EndPointServer =
| [< EndPoint "/" >] Hello
| About
let url = "http://localhost:9006/"
let rootdir = #"..\website"
let site() = WebSharper.Application.MultiPage(fun context (s:EndPointServer) ->
printfn "Serving page: %A" s
Content.Page(
Title= ( sprintf "Test %A" s)
, Body = [ h1 [] [ text <| sprintf "%A" s ]
Html.client <# ClientFunctions.clientDoc() #> ])
)
So I have my server set up very simply. If the path is of the form /article/something, it should serve up the static file something.html within the folder static. For some reason, the Files.file webpart is apparently returning None. I tacked on the OK "File Displayed" webpart to verify that this is the case. The OK never executes.
let app =
choose [
pathScan "/article/%s" (fun article ->
let name = sprintf "%s.html" article
Console.WriteLine name
Files.file name >=> OK "File Displayed")
]
let config =
{ defaultConfig with homeFolder = Some (Path.GetFullPath "./static") }
[<EntryPoint>]
let main args =
startWebServer config app
0
Interestingly enough, the Console.WriteLine name line executes perfectly and I see something.html in the console window when I execute this. It appears the problem is exclusively Files.file name returning None.
The file something.html definitely exists in the static folder, so that's not the problem .
Any ideas on what might be causing this?
Here are some parts to troubleshoot static file serving issues
let troubleShootExtensionPart extensionToCheck :WebPart =
fun ctx ->
match extensionToCheck with
| null | "" -> ServerErrors.INTERNAL_ERROR "Extension Error not supplied, part is not set up correctly"
| x when not <| x.StartsWith "." -> ServerErrors.INTERNAL_ERROR "Extensions start with a '.', part is not set up correctly"
| _ ->
let mtm = ctx.runtime.mimeTypesMap
match mtm extensionToCheck with
| None ->
sprintf "%s is not supported by the mime types map, compose your mime type with the `defaultMimeTypesMap`" extensionToCheck
|> RequestErrors.FORBIDDEN
| Some x ->
sprintf "%s is supported and uses '%s', compression on? : %A" extensionToCheck x.name x.compression
|> OK
|> fun wp -> wp ctx
example consumption with a wildcard so if no routes match you get some diagnostic info
#if DEBUG
pathScan "/checkExtension/%s" (fun name -> troubleShootExtensionPart name)
// catch all
(fun ctx -> sprintf "404, also homeFolder resolves to %s" (Path.GetFullPath ".") |> RequestErrors.NOT_FOUND |> fun wp -> wp ctx)
#endif